diff --git a/.changeset/config.json b/.changeset/config.json
index db181c1..55ababc 100644
--- a/.changeset/config.json
+++ b/.changeset/config.json
@@ -7,5 +7,7 @@
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
- "ignore": []
+ "ignore": [
+ "@sourceacademy/web-data-display"
+ ]
}
\ No newline at end of file
diff --git a/.yarnrc.yml b/.yarnrc.yml
new file mode 100644
index 0000000..7c82625
--- /dev/null
+++ b/.yarnrc.yml
@@ -0,0 +1,4 @@
+packageExtensions:
+ "@mantine/hooks@^9.0.0":
+ dependencies:
+ react-dom: "*"
\ No newline at end of file
diff --git a/package.json b/package.json
index 929d3fe..21074af 100644
--- a/package.json
+++ b/package.json
@@ -21,11 +21,14 @@
"@changesets/cli": "^2.31.0",
"@sourceacademy/conductor": "^0.3.0",
"@sourceacademy/plugin-directory": "source-academy/plugin-directory#main",
+ "@testing-library/jest-dom": "^6.9.1",
"@types/node": "^25.9.1",
"@vitest/coverage-istanbul": "^4.1.9",
+ "canvas": "^3.2.3",
"commander": "^15.0.0",
"eslint": "^10.0.3",
"eslint-config-prettier": "^10.1.8",
+ "jsdom": "^29.1.1",
"prettier": "^3.8.1",
"tsx": "^4.22.3",
"typescript": "^6.0.3",
diff --git a/src/common/data-display/README.md b/src/common/data-display/README.md
new file mode 100644
index 0000000..6ab340d
--- /dev/null
+++ b/src/common/data-display/README.md
@@ -0,0 +1,82 @@
+
Data Visualization
+
+
A language agnostic interface to visualise lists, trees and other data structures
+
+
+
+
+
+
+
+## Features
+- [Box-and-pointer diagrams](https://sourceacademy.org/sicpjs/2.2) to visualize lists and array structures
+- Visualisations for trees (including binary trees or more general trees)
+
+## Installation
+To add the common types to your project, add the following
+```bash
+yarn add @sourceacademy/common-data-display
+# OR
+npm i @sourceacademy/common-data-display
+# OR
+pnpm add @sourceacademy/common-data-display
+```
+
+## Structure
+This package ([`@sourceacademy/common-data-display`](https://github.com/source-academy/plugins/tree/main/src/common/data-display)) contains the common constants and types required for both the host and runner-side plugin implementations.
+
+These include
+- the IDs of the runner-side plugin, the web-side plugin and the channel they communicate over.
+- the types of the language-agnostic data the runner-side plugin has to convert language values to
+
+## Usage
+After installation, import the types from the `@sourceacademy/common-data-display` package.
+
+```ts
+import type { Data } from '@sourceacademy/common-data-display';
+...
+function serialiseData(value: LanguageValue): Data {
+ switch (value.type) {
+ ...
+ case 'number':
+ return { type: 'string', value: value.value.toString() };
+ ...
+ }
+}
+```
+
+## Data Types
+The `Data` type is the simplest possible representation of the data visualisable by the tool. Since the same value may be displayed in two different ways (the JS number `3` is displayed as `3` in Source, but the same number is used by the [Python sublanguage](https://github.com/source-academy/py-slang) to represent `3.0`), language-specific values are stringified into their representation.
+
+For example,
+### Source
+- `3` serialises to `'3'`
+- `true` serialises to `'true'`
+- `'a'` serialises to `"'a'"`
+
+### Python 1-4
+_Note: py-slang is internally powered by JavaScript. Hence, the serialisation of the internal JS values is implied below_
+- `3n` becomes `'3'`
+- `3` becomes `'3.0'`
+- `true` becomes `'True'`
+
+The `Data` type is a discriminated union with four subtypes. Each subtype is an object with a `type` field, and optionally a `value` field
+| Name | Description | `type` field | `value` field |
+|------------------|-------------|--------------|---------------|
+| `StringValue` | Stores a representation of an internal language value. This would be the text present in the boxes of the diagram | `'string'` | A `string` type with the string representation. _Note: serialised strings will contain their quotes, i.e., `'a'` -> `"'a'"`_ |
+| `ArrayValue` | Represents an array of data objects. This would represent a singular box | `'array'` | A `Data[]` array. These would be the elements of the box |
+| `EmptyListValue` | Represents the linked-list termination value (`null`, `None`, etc.). Would be shown as a diagonal line in the diagram | `'null'`| N/A |
+| `FunctionValue` | Represents a function (closure, built-in function, etc.). Represented as two external dotted circles | `'function'` | N/A |
+
+Native JS types aren't directly passed over the channels since functions cannot be passed from a web worker to the main thread.
+
+## Configuration Types
+The `Config` type contains language-specific configuration required for the web plugin including
+- `sicpTextbookName`: The name section of the SICP book which contains hierarchical data, as well as box-and-pointer diagrams
+- `sicpTextbookUrl`: The URL of the aforementioned section
+- `functionCallText`: The function call displayed as an example. For example, `draw_data(x1, x2, ..., xn)`. The suffix after `x` in `x` and `xn` are automatically converted to subscript.
+
+## Further reading
+- To use the runner-side plugin, install [`@sourceacademy/runner-data-display`](https://github.com/source-academy/plugins/tree/main/src/runner/data-display)
+- To use the web plugin, check out the [Github repo](https://github.com/source-academy/plugins/tree/main/src/web/data-display)
+- The [plugins wiki](https://github.com/source-academy/plugins/wiki) provides information on how plugins communicate.
\ No newline at end of file
diff --git a/src/common/data-display/docs/image.png b/src/common/data-display/docs/image.png
new file mode 100644
index 0000000..6bb0ec9
Binary files /dev/null and b/src/common/data-display/docs/image.png differ
diff --git a/src/common/data-display/manifest.json b/src/common/data-display/manifest.json
new file mode 100644
index 0000000..e263f73
--- /dev/null
+++ b/src/common/data-display/manifest.json
@@ -0,0 +1,3 @@
+{
+ "type": "installable"
+}
diff --git a/src/common/data-display/package.json b/src/common/data-display/package.json
new file mode 100644
index 0000000..de04b9b
--- /dev/null
+++ b/src/common/data-display/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "@sourceacademy/common-data-display",
+ "version": "0.0.1",
+ "packageManager": "yarn@4.6.0",
+ "description": "A common set of types and utilities for displaying box-and-pointer diagrams",
+ "scripts": {
+ "build": "rollup -c",
+ "prepack": "yarn build"
+ },
+ "license": "ISC",
+ "files": [
+ "dist"
+ ],
+ "main": "dist/index.cjs",
+ "module": "dist/index.mjs",
+ "types": "dist/index.d.ts",
+ "exports": {
+ ".": {
+ "import": "./dist/index.mjs",
+ "require": "./dist/index.cjs",
+ "types": "./dist/index.d.ts"
+ }
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "devDependencies": {
+ "@rollup/plugin-node-resolve": "^16.0.3",
+ "@rollup/plugin-terser": "^1.0.0",
+ "@rollup/plugin-typescript": "^12.3.0",
+ "rollup": "^4.60.2",
+ "tslib": "^2.8.1",
+ "typescript": "^6.0.3",
+ "vitest": "^4.1.9"
+ }
+}
diff --git a/src/common/data-display/rollup.config.mjs b/src/common/data-display/rollup.config.mjs
new file mode 100644
index 0000000..238a07c
--- /dev/null
+++ b/src/common/data-display/rollup.config.mjs
@@ -0,0 +1,21 @@
+import nodeResolve from "@rollup/plugin-node-resolve";
+import terser from "@rollup/plugin-terser";
+import typescript from "@rollup/plugin-typescript";
+
+/**
+ * @type {import('rollup').RollupOptions}
+ */
+export default {
+ input: "src/index.ts",
+ output: [
+ {
+ file: "dist/index.cjs",
+ format: "cjs",
+ },
+ {
+ file: "dist/index.mjs",
+ format: "esm",
+ },
+ ],
+ plugins: [nodeResolve(), typescript(), terser()],
+};
diff --git a/src/common/data-display/src/__tests__/common.test.ts b/src/common/data-display/src/__tests__/common.test.ts
new file mode 100644
index 0000000..722a1ae
--- /dev/null
+++ b/src/common/data-display/src/__tests__/common.test.ts
@@ -0,0 +1,5 @@
+import { test, expect } from "vitest";
+
+test("strings work", () => {
+ expect("test").toBe("test");
+});
diff --git a/src/common/data-display/src/index.ts b/src/common/data-display/src/index.ts
new file mode 100644
index 0000000..d283848
--- /dev/null
+++ b/src/common/data-display/src/index.ts
@@ -0,0 +1,31 @@
+/**
+ * The web plugin's ID
+ */
+export const WEB_ID = "__web_data_display";
+
+/**
+ * The runner plugin's ID
+ */
+export const RUNNER_ID = "__runner_data_display";
+
+/**
+ * The data channel ID for the data display plugin
+ */
+export const DATA_CHANNEL_ID = "data_display_data_channel";
+/**
+ * The config channel ID for the data display plugin
+ */
+export const CONFIG_CHANNEL_ID = "data_display_config_channel";
+
+export type StringValue = { type: "string"; value: string };
+export type FunctionValue = { type: "function" };
+export type ArrayValue = { type: "array"; value: Data[] };
+export type EmptyListValue = { type: "null" };
+
+export type Data = StringValue | FunctionValue | ArrayValue | EmptyListValue;
+
+export type Config = {
+ sicpTextbookName: string;
+ sicpTextbookUrl: string;
+ functionCallText: string;
+};
diff --git a/src/common/data-display/tsconfig.json b/src/common/data-display/tsconfig.json
new file mode 100644
index 0000000..c7482f9
--- /dev/null
+++ b/src/common/data-display/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../tsconfig.json",
+ "exclude": ["./dist"],
+ "include": ["./src"],
+ "compilerOptions": {
+ "declaration": true,
+ "outDir": "./dist",
+ "rootDir": "./src"
+ }
+}
diff --git a/src/runner/data-display/README.md b/src/runner/data-display/README.md
new file mode 100644
index 0000000..31cd67a
--- /dev/null
+++ b/src/runner/data-display/README.md
@@ -0,0 +1,89 @@
+
Data Visualization
+
+
A language agnostic interface to visualise lists, trees and other data structures
+
+
+
+
+
+
+## Features
+- [Box-and-pointer diagrams](https://sourceacademy.org/sicpjs/2.2) to visualize lists and array structures
+- Visualisations for trees (including binary trees or more general trees)
+
+## Installation
+To add the base runner plugin to your project, add the following
+```bash
+yarn add @sourceacademy/runner-data-display
+# OR
+npm i @sourceacademy/runner-data-display
+# OR
+pnpm add @sourceacademy/runner-data-display
+```
+
+## Structure
+This package ([`@sourceacademy/runner-data-display`](https://github.com/source-academy/plugins/tree/main/src/runner/data-display)) contains the base runner plugin required for sending data to the host-side plugin.
+
+This is encompassed by the `BaseDataDisplayRunnerPlugin` abstract class.
+
+### API Reference
+The abstract class is generic, where the type parameter `T` represents the language value type.
+| Name | Description |
+|---------------|-------------|
+| `abstract getConfig(): Config` | It returns the language-specific [`Config`](https://github.com/source-academy/plugins/tree/main/src/common/data-display/README.md#configuration-types) type. |
+| `abstract serialiseData(data: T): Data` | It serialises the language value type into the language-agnostic [`Data`](https://github.com/source-academy/plugins/tree/main/src/common/data-display/README.md#data-types) type. Ensure it works on circular objects, similar to the example below |
+| `sendData(data: T): void` | It is meant to be called by the evaluator when it encounters the `draw_data` function. It serialises a language value and sends it to the web plugin|
+
+## Usage
+After installation, import the class from the `@sourceacademy/runner-data-display` package.
+
+```ts
+import { BaseDataDisplayRunnerPlugin } from "@sourceacademy/runner-data-display";
+...
+export class DataDisplayRunnerPlugin extends BaseDataDisplayRunnerPlugin {
+ serialiseData(value: Value): Data {
+ const objCache = new Map();
+ function helper(value: Value): Data {
+ if (objCache.has(value)) {
+ return objCache.get(value)!;
+ }
+ switch (value.type) {
+ case "number":
+ case "bool":
+ case "complex":
+ case "bigint":
+ case "string":
+ case "error":
+ return { type: "string", value: toPythonString(value, true) };
+ case "builtin":
+ case "closure":
+ case "function":
+ case "multi_lambda":
+ return { type: "function" };
+ case "list":
+ const listData: Data[] = [];
+ objCache.set(value, { type: "array", value: listData });
+ for (const item of value.value) {
+ listData.push(helper(item));
+ }
+ return { type: "array", value: listData };
+ case "none":
+ return { type: "null" };
+ }
+ }
+ return helper(value);
+ }
+ getConfig(): Config {
+ return {
+ sicpTextbookName: "Structure and Interpretation of Computer Programs, JavaScript Edition, Chapter 2, Section 2",
+ sicpTextbookUrl: "https://sourceacademy.org/sicpjs/2.2",
+ functionCallText: "draw_data(x1, x2, ..., xn)"
+ }
+ }
+}
+```
+
+## Further reading
+- To use the common types and IDs, install [`@sourceacademy/common-data-display`](https://github.com/source-academy/plugins/tree/main/src/common/data-display)
+- To use the web plugin, check out the [Github repo](https://github.com/source-academy/plugins/tree/main/src/web/data-display)
+- The [plugins wiki](https://github.com/source-academy/plugins/wiki) provides information on how plugins communicate.
\ No newline at end of file
diff --git a/src/runner/data-display/manifest.json b/src/runner/data-display/manifest.json
new file mode 100644
index 0000000..e263f73
--- /dev/null
+++ b/src/runner/data-display/manifest.json
@@ -0,0 +1,3 @@
+{
+ "type": "installable"
+}
diff --git a/src/runner/data-display/package.json b/src/runner/data-display/package.json
new file mode 100644
index 0000000..643a40c
--- /dev/null
+++ b/src/runner/data-display/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "@sourceacademy/runner-data-display",
+ "version": "0.0.1",
+ "packageManager": "yarn@4.6.0",
+ "description": "A runner plugin for displaying box-and-pointer diagrams",
+ "scripts": {
+ "build": "rollup -c",
+ "prepack": "yarn build"
+ },
+ "license": "ISC",
+ "files": [
+ "dist"
+ ],
+ "main": "dist/index.cjs",
+ "module": "dist/index.mjs",
+ "types": "dist/index.d.ts",
+ "exports": {
+ ".": {
+ "import": "./dist/index.mjs",
+ "require": "./dist/index.cjs",
+ "types": "./dist/index.d.ts"
+ }
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "peerDependencies": {
+ "@sourceacademy/conductor": ">=0.3.0"
+ },
+ "devDependencies": {
+ "@rollup/plugin-node-resolve": "^16.0.3",
+ "@rollup/plugin-terser": "^1.0.0",
+ "@rollup/plugin-typescript": "^12.3.0",
+ "@sourceacademy/common-data-display": "workspace:^",
+ "@sourceacademy/conductor": ">=0.3.0",
+ "rollup": "^4.60.2",
+ "tslib": "^2.8.1",
+ "typescript": "^6.0.3",
+ "vitest": "^4.1.9"
+ }
+}
diff --git a/src/runner/data-display/rollup.config.mjs b/src/runner/data-display/rollup.config.mjs
new file mode 100644
index 0000000..238a07c
--- /dev/null
+++ b/src/runner/data-display/rollup.config.mjs
@@ -0,0 +1,21 @@
+import nodeResolve from "@rollup/plugin-node-resolve";
+import terser from "@rollup/plugin-terser";
+import typescript from "@rollup/plugin-typescript";
+
+/**
+ * @type {import('rollup').RollupOptions}
+ */
+export default {
+ input: "src/index.ts",
+ output: [
+ {
+ file: "dist/index.cjs",
+ format: "cjs",
+ },
+ {
+ file: "dist/index.mjs",
+ format: "esm",
+ },
+ ],
+ plugins: [nodeResolve(), typescript(), terser()],
+};
diff --git a/src/runner/data-display/src/index.ts b/src/runner/data-display/src/index.ts
new file mode 100644
index 0000000..96d8c3b
--- /dev/null
+++ b/src/runner/data-display/src/index.ts
@@ -0,0 +1,34 @@
+import type { IPlugin, IChannel, IConduit } from "@sourceacademy/conductor/conduit";
+import {
+ DATA_CHANNEL_ID,
+ CONFIG_CHANNEL_ID,
+ RUNNER_ID,
+ type Data,
+ type Config,
+} from "@sourceacademy/common-data-display";
+
+export abstract class BaseDataDisplayRunnerPlugin implements IPlugin {
+ readonly id: string = RUNNER_ID;
+ static readonly channelAttach = [DATA_CHANNEL_ID, CONFIG_CHANNEL_ID];
+ private readonly __dataChannel: IChannel;
+ private readonly __configChannel: IChannel;
+
+ constructor(
+ _conduit: IConduit,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ [dataChannel, configChannel]: IChannel[],
+ ) {
+ this.__dataChannel = dataChannel;
+ this.__configChannel = configChannel;
+ // Provide the config to the web plugin when it requests it
+ this.__configChannel.subscribe(() => {
+ this.__configChannel.send(this.getConfig());
+ });
+ }
+
+ abstract getConfig(): Config;
+ abstract serialiseData(data: T): Data;
+ sendData(data: T) {
+ this.__dataChannel.send(this.serialiseData(data));
+ }
+}
diff --git a/src/runner/data-display/tsconfig.json b/src/runner/data-display/tsconfig.json
new file mode 100644
index 0000000..c7482f9
--- /dev/null
+++ b/src/runner/data-display/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../tsconfig.json",
+ "exclude": ["./dist"],
+ "include": ["./src"],
+ "compilerOptions": {
+ "declaration": true,
+ "outDir": "./dist",
+ "rootDir": "./src"
+ }
+}
diff --git a/src/tsconfig.json b/src/tsconfig.json
index 2461137..8d0d8f4 100644
--- a/src/tsconfig.json
+++ b/src/tsconfig.json
@@ -26,6 +26,7 @@
"sourceMap": false,
/* The strict flag enables a wide range of type checking behavior that results in stronger guarantees of program correctness. */
"strict": true,
+ "strictNullChecks": true,
/* The target setting changes which JS features are downleveled and which are left intact. */
"target": "es6",
diff --git a/src/web/data-display/README.md b/src/web/data-display/README.md
new file mode 100644
index 0000000..f097909
--- /dev/null
+++ b/src/web/data-display/README.md
@@ -0,0 +1,57 @@
+
Data Visualization
+
+
A language agnostic interface to visualise lists, trees and other data structures
+
+
+
+
+
+## Features
+- [Box-and-pointer diagrams](https://sourceacademy.org/sicpjs/2.2) to visualize lists and array structures
+- Visualisations for trees (including binary trees or more general trees)
+
+## Installation
+In production, use the [plugin directory](https://github.com/source-academy/plugin-directory) hosted on [GitHub Pages](https://source-academy.github.io/plugins/directory.json). It contains a list of all the plugins available in this monorepo. Your host plugin should have the capability to load plugins from the plugin directory (the Source Academy frontend does).
+
+For locally hosting the plugin repository, run `yarn build` at the root of the repository. Then, serve the plugin directory with
+```bash
+yarn dlx serve --cors dist/ -p 1915
+```
+The plugin directory will be online at `http://localhost:1915/directory.json`.
+For rebuilding the plugin during development, run `yarn build` again
+
+## Structure
+This package ([`@sourceacademy/web-data-display`](https://github.com/source-academy/plugins/tree/main/src/web/data-display)) contains the host-side plugin to display data. Most of the code has been extracted from the [frontend](https://github.com/source-academy/frontend).
+
+The entry point is `src/index.tsx`, which contains the actual host plugin. It subscribes to a data channel, which receives the [`Data`](../../common/data-display/README.md#data-types) type. It serialises the data into a native JS format which is then displayed in a tab.
+
+It also subscribes to a configuration channel
+
+## Serialisation
+
+The [`Data`](../../common/data-display/README.md#data-types) type is serialised again into a native JS structure for the internal data visualizer.
+
+Let `s(x)` be the serialisation function
+- `s({ type: 'array', value: v })` returns `v.map(s)`
+- `s({ type: 'function' })` returns `() => {}`. This is because the data visualiser doesn't need any other metadata from a function, only the fact that the value is a function. It displays all functions as two dotted circles.
+- `s({ type: 'null' })` returns `null`
+- `s({ type: 'string', value: v })` returns `v`
+
+### Circular references
+The above simplification would fail on circular arrays. To prevent infinite recursion, a mapping from all the `Data` types to the native JS type is created.
+When an array is passed into the function, a preemptive array is added to the cache
+```js
+cache[data] = [];
+```
+Then the elements of `data.v` are mapped over, and pushed to the original array
+```js
+cache[data].push(...data.v.map(s))
+```
+
+This ensures that if the `data` object is referenced in any of the elements/subelements of `data.v`,
+it just passes the to-be final array.
+
+## Further reading
+- To use the runner-side plugin, install [`@sourceacademy/runner-data-display`](https://github.com/source-academy/plugins/tree/main/src/runner/data-display)
+- To use the common types and IDs, install [`@sourceacademy/common-data-display`](https://github.com/source-academy/plugins/tree/main/src/common/data-display)
+- The [plugins wiki](https://github.com/source-academy/plugins/wiki) provides information on how plugins communicate.
\ No newline at end of file
diff --git a/src/web/data-display/manifest.json b/src/web/data-display/manifest.json
new file mode 100644
index 0000000..3463661
--- /dev/null
+++ b/src/web/data-display/manifest.json
@@ -0,0 +1,3 @@
+{
+ "type": "external"
+}
diff --git a/src/web/data-display/package.json b/src/web/data-display/package.json
new file mode 100644
index 0000000..3ed24a3
--- /dev/null
+++ b/src/web/data-display/package.json
@@ -0,0 +1,59 @@
+{
+ "name": "@sourceacademy/web-data-display",
+ "version": "0.0.1",
+ "packageManager": "yarn@4.6.0",
+ "description": "A web plugin for displaying box-and-pointer diagrams",
+ "scripts": {
+ "build": "rollup -c",
+ "prepack": "yarn build"
+ },
+ "license": "ISC",
+ "files": [
+ "dist"
+ ],
+ "main": "dist/index.js",
+ "module": "./dist/index.mjs",
+ "types": "dist/index.d.ts",
+ "exports": {
+ ".": {
+ "import": "./dist/index.mjs",
+ "require": "./dist/index.js",
+ "types": "./dist/index.d.ts"
+ }
+ },
+ "peerDependencies": {
+ "@sourceacademy/common-tabs": "^0.0.1",
+ "@sourceacademy/conductor": ">=0.3.0",
+ "react": "^19",
+ "react-dom": "^19"
+ },
+ "devDependencies": {
+ "@blueprintjs/core": "^6.16.0",
+ "@blueprintjs/icons": "^6.11.0",
+ "@mantine/hooks": "^9.3.1",
+ "@rollup/plugin-commonjs": "^29.0.3",
+ "@rollup/plugin-node-resolve": "^16.0.3",
+ "@rollup/plugin-replace": "^6.0.3",
+ "@rollup/plugin-terser": "^1.0.0",
+ "@rollup/plugin-typescript": "^12.3.0",
+ "@sourceacademy/common-data-display": "workspace:^",
+ "@sourceacademy/common-tabs": "workspace:^",
+ "@sourceacademy/conductor": ">=0.3.0",
+ "@testing-library/dom": "^10.4.1",
+ "@testing-library/jest-dom": "^6.9.1",
+ "@testing-library/react": "^16.3.2",
+ "@types/react": "^19.2.17",
+ "canvas": "^3.2.3",
+ "classnames": "^2.5.1",
+ "i18next": "^26.3.1",
+ "konva": "^10.3.0",
+ "react": "^19.2.4",
+ "react-dom": "^19.2.7",
+ "react-i18next": "^17.0.8",
+ "react-konva": "^19.0.7",
+ "rollup": "^4.60.2",
+ "tslib": "^2.8.1",
+ "typescript": "^6.0.3",
+ "vitest": "^4.1.9"
+ }
+}
diff --git a/src/web/data-display/rollup.config.mjs b/src/web/data-display/rollup.config.mjs
new file mode 100644
index 0000000..d4f1626
--- /dev/null
+++ b/src/web/data-display/rollup.config.mjs
@@ -0,0 +1,43 @@
+import nodeResolve from "@rollup/plugin-node-resolve";
+import terser from "@rollup/plugin-terser";
+import typescript from "@rollup/plugin-typescript";
+import commonjs from "@rollup/plugin-commonjs";
+import replace from "@rollup/plugin-replace";
+
+/**
+ * @type {import('rollup').RollupOptions}
+ */
+export default {
+ input: "src/index.tsx",
+ output: [
+ {
+ file: "dist/index.js",
+ format: "cjs",
+ inlineDynamicImports: true,
+ },
+ {
+ file: "dist/index.mjs",
+ format: "esm",
+ inlineDynamicImports: true,
+ },
+ ],
+ plugins: [
+ replace({
+ preventAssignment: true,
+ "process.env.NODE_ENV": JSON.stringify("production"),
+ }),
+ nodeResolve({ preferBuiltins: false, browser: true }),
+ commonjs(),
+ typescript(),
+ terser(),
+ ],
+ external: [
+ "react",
+ "react-dom",
+ "react/jsx-runtime",
+ "@blueprintjs/core",
+ "@blueprintjs/icons",
+ "react-konva",
+ "konva",
+ ],
+};
diff --git a/src/web/data-display/src/DataVisualizer.tsx b/src/web/data-display/src/DataVisualizer.tsx
new file mode 100644
index 0000000..f72772e
--- /dev/null
+++ b/src/web/data-display/src/DataVisualizer.tsx
@@ -0,0 +1,216 @@
+import type { ArrayValue, Data } from "@sourceacademy/common-data-display";
+import { Config } from "./utils/Config";
+import { type Step, DataVizMode } from "./DataVisualizerTypes";
+import { Tree } from "./tree/Tree";
+import { DataTreeNode } from "./tree/TreeNode";
+import { is_list, length_list, reduce_list } from "./utils/list";
+
+/**
+ * The data visualizer class.
+ * Exposes three function: init, drawData, and clear.
+ *
+ * init is used by SideContentDataVisualizer as a hook.
+ * drawData is the draw_data function in source.
+ * clear is used by WorkspaceSaga to reset the visualizer after every "Run" button press
+ */
+export default class DataVisualizer {
+ private static empty() {}
+ private static setSteps: (step: Step[]) => void = DataVisualizer.empty;
+ public static dataRecords: Data[][] = [];
+ public static isRedraw = false;
+ private static _instance = new DataVisualizer();
+ private static mode: DataVizMode = DataVizMode.NORMAL;
+ public static TreeDepth = 0;
+ public static isBinTree = false;
+ public static isGenTree = false;
+ public static nodeCount: number[] = [];
+ public static nodeColor: number[] = [];
+ public static longestNodePos: number = 0;
+ public static colorMap: WeakMap = new WeakMap();
+ public static posMap: WeakMap = new WeakMap();
+
+ private steps: Step[] = [];
+ private nodeLabel = 0;
+ private nodeToLabelMap: Map = new Map();
+
+ private constructor() {}
+
+ public static isBinaryTree(structures: Data, type: string | null = null): boolean {
+ if (structures.type === "null") {
+ return true;
+ }
+ if (!is_list(structures) || length_list(structures) !== 3) {
+ return false;
+ }
+
+ if (type === null) {
+ type = structures.value[0].type;
+ }
+ if (structures.value[0].type !== type) {
+ return false;
+ }
+
+ return reduce_list((acc, x) => acc && this.isBinaryTree(x, type), true, structures.value[1]);
+ }
+
+ public static isGeneralTree(structures: Data): boolean {
+ if (structures.type === "null") {
+ return true;
+ }
+ if (!is_list(structures)) {
+ return false;
+ }
+ return reduce_list(
+ (acc, x) => acc && (!is_list(x) || this.isGeneralTree(x)),
+ true,
+ structures.value[1],
+ );
+ }
+
+ public static initializeTreeMetaData(
+ structures: Data,
+ depth: number,
+ nodePos: number,
+ newNode: boolean,
+ ): number {
+ if (structures.type !== "array") {
+ return 0;
+ }
+ // nodeCount keeps track of the current index of nodes at each depth
+ if (this.getMode() === DataVizMode.GENERAL_TREE) {
+ if (this.nodeCount[depth] === undefined) {
+ this.nodeCount[depth] = 0;
+ }
+ this.posMap.set(structures, this.nodeCount[depth]);
+ if (this.nodeCount[depth] > this.longestNodePos) {
+ this.longestNodePos = this.nodeCount[depth];
+ }
+ this.nodeCount[depth]++;
+ }
+ if (this.getMode() === DataVizMode.BINARY_TREE || this.getMode() === DataVizMode.GENERAL_TREE) {
+ if (this.nodeColor[depth] === undefined) {
+ this.nodeColor[depth] = depth;
+ }
+ if (newNode) {
+ this.nodeColor[depth]++;
+ }
+ this.colorMap.set(structures, this.nodeColor[depth]);
+ }
+
+ this.TreeDepth = Math.max(this.TreeDepth, depth);
+ this.initializeTreeMetaData(structures.value[0], depth + 1, 0, true);
+ this.initializeTreeMetaData(structures.value[1], depth, nodePos + 1, false);
+ return depth;
+ }
+
+ public static init(setSteps: (step: Step[]) => void): void {
+ DataVisualizer.setSteps = setSteps;
+ setSteps(DataVisualizer._instance.steps);
+ }
+
+ /**
+ * Set the visualization mode. This ensures only one mode is active at a time.
+ * @param mode - 'normal' for original view, 'binTree' for binary tree, 'tree' for general tree
+ */
+ public static setMode(mode: DataVizMode): void {
+ DataVisualizer.mode = mode;
+ }
+
+ public static getMode(): DataVizMode {
+ return DataVisualizer.mode;
+ }
+
+ public static hasCycle(structures: Data, visited: WeakSet = new WeakSet()): boolean {
+ if (structures.type !== "array") {
+ return false;
+ }
+ if (visited.has(structures)) {
+ return true;
+ }
+ visited.add(structures);
+ return structures.value.some(x => this.hasCycle(x, visited));
+ }
+
+ public static drawData(structures: Data[]): void {
+ if (!DataVisualizer.setSteps) {
+ throw new Error("Data visualizer not initialized");
+ }
+ if (!DataVisualizer.isRedraw) {
+ this.dataRecords.push(structures);
+ }
+ const root = structures[0];
+ const isCyclic = this.hasCycle(root);
+ DataVisualizer.nodeCount = [];
+ DataVisualizer.nodeColor = [];
+ this.nodeColor[0] = -1;
+ DataVisualizer.longestNodePos = 0;
+ DataVisualizer.TreeDepth = 0;
+ if (isCyclic) {
+ DataVisualizer.isBinTree = false;
+ DataVisualizer.isGenTree = false;
+ } else {
+ DataVisualizer.isBinTree = this.isBinaryTree(root);
+ DataVisualizer.isGenTree = this.isGeneralTree(root);
+ if (DataVisualizer.isBinTree || DataVisualizer.isGenTree) {
+ this.initializeTreeMetaData(root, 0, 0, false);
+ }
+ }
+ DataVisualizer._instance.addStep(structures);
+
+ DataVisualizer.setSteps(DataVisualizer._instance.steps);
+ }
+
+ public static clearWithData(): void {
+ DataVisualizer.longestNodePos = 0;
+ DataVisualizer.dataRecords = [];
+ DataVisualizer.isRedraw = false;
+ DataVisualizer.clear();
+ }
+
+ public static clear(): void {
+ DataVisualizer._instance = new DataVisualizer();
+ this.nodeCount = [];
+ this.TreeDepth = 0;
+ DataVisualizer.setSteps(DataVisualizer._instance.steps);
+ }
+
+ public static displaySpecialContent(dataNode: DataTreeNode): number {
+ return DataVisualizer._instance.displaySpecialContent(dataNode);
+ }
+
+ private displaySpecialContent(dataNode: DataTreeNode): number {
+ if (this.nodeToLabelMap.has(dataNode)) {
+ return this.nodeToLabelMap.get(dataNode) ?? 0;
+ } else {
+ this.nodeToLabelMap.set(dataNode, this.nodeLabel);
+ return this.nodeLabel++;
+ }
+ }
+
+ private addStep(structures: Data[]) {
+ const step = structures.map((xs, index) => this.createDrawing(xs, index));
+ this.steps.push(step);
+ }
+
+ private createDrawing(xs: Data, key: number): React.ReactElement {
+ const treeDrawer = Tree.fromSourceStructure(xs).draw();
+
+ // To account for overflow to the left side due to a backward arrow
+ const leftMargin = Config.StrokeWidth / 2;
+
+ // To account for overflow to the top due to a backward arrow
+ const topMargin = Config.StrokeWidth / 2;
+
+ return treeDrawer.draw(leftMargin, topMargin, key);
+ }
+
+ static redraw() {
+ this.isRedraw = true;
+ this.clear();
+ try {
+ DataVisualizer.dataRecords.forEach(this.drawData);
+ } finally {
+ this.isRedraw = false;
+ }
+ }
+}
diff --git a/src/web/data-display/src/DataVisualizerTypes.ts b/src/web/data-display/src/DataVisualizerTypes.ts
new file mode 100644
index 0000000..25a5da1
--- /dev/null
+++ b/src/web/data-display/src/DataVisualizerTypes.ts
@@ -0,0 +1,9 @@
+// Drawing-related types
+export type Drawing = React.ReactElement;
+export type Step = Drawing[];
+
+export enum DataVizMode {
+ NORMAL = "normal",
+ BINARY_TREE = "binary-tree",
+ GENERAL_TREE = "general-tree",
+}
diff --git a/src/web/data-display/src/SideContentDataVisualizer.tsx b/src/web/data-display/src/SideContentDataVisualizer.tsx
new file mode 100644
index 0000000..20fe5a8
--- /dev/null
+++ b/src/web/data-display/src/SideContentDataVisualizer.tsx
@@ -0,0 +1,314 @@
+import {
+ AnchorButton,
+ Button,
+ ButtonGroup,
+ Card,
+ Checkbox,
+ Classes,
+ Icon,
+ Tooltip,
+} from "@blueprintjs/core";
+import { IconNames } from "@blueprintjs/icons";
+import { useHotkeys, type HotkeyItem } from "@mantine/hooks";
+import type { Tab } from "@sourceacademy/common-tabs";
+import classNames from "classnames";
+import i18n from "i18next";
+import { useEffect, useState } from "react";
+
+import { Trans, initReactI18next, useTranslation } from "react-i18next";
+import DataVisualizer from "./DataVisualizer";
+import { DataVizMode, type Step } from "./DataVisualizerTypes";
+import type { Config } from "@sourceacademy/common-data-display";
+import React from "react";
+
+type Props = {
+ workspaceLocation: string;
+ config: Config;
+};
+export function ItalicLink({ href, children }: { href: string; children?: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+
+const translations = {
+ defaultText: "The data visualizer helps you to visualize data structures.",
+ instructions:
+ "It is activated by calling the function <0/>, where <1/> would be the <2/> data structure that you want to visualize and <3/> is the number of structures.",
+ reference: "The data visualizer uses box-and-pointer diagrams, as introduced in <0 />.",
+ label: "Data Visualizer",
+ previous: "Previous",
+ next: "Next",
+ call: "Call",
+ structure: "Structure",
+ views: {
+ original: "Original View",
+ binaryTree: "Binary Tree View",
+ generalTree: "General Tree View",
+ },
+};
+i18n.use(initReactI18next).init({
+ resources: {
+ en: { translation: translations },
+ },
+ fallbackLng: "en",
+ interpolation: {
+ escapeValue: false,
+ },
+});
+
+function SideContentDataVisualizer({ workspaceLocation, config }: Props) {
+ const [steps, setSteps] = useState([]);
+ const [currentStep, setCurrentStep] = useState(0);
+ const { t } = useTranslation();
+
+ useEffect(() => {
+ DataVisualizer.init(steps => {
+ setSteps(steps);
+ setCurrentStep(0);
+ });
+ }, [workspaceLocation]);
+
+ const onPrevButtonClick = () => {
+ setCurrentStep(prev => Math.max(0, prev - 1));
+ };
+
+ const onNextButtonClick = () => {
+ setCurrentStep(prev => Math.min(steps.length - 1, prev + 1));
+ };
+
+ const onViewModeClick = (prevStep: number) => {
+ setCurrentStep(prevStep);
+ };
+
+ const step: Step | undefined = steps[currentStep];
+ const firstStep = currentStep === 0;
+ const finalStep = !steps || currentStep === steps.length - 1;
+
+ const hotkeyBindings: HotkeyItem[] = [
+ ["ArrowLeft", onPrevButtonClick],
+ ["ArrowRight", onNextButtonClick],
+ ];
+ useHotkeys(hotkeyBindings);
+
+ return (
+
+ {steps.length > 1 ? (
+
+
+
+ {t("call")} {currentStep + 1}/{steps.length}
+
+
+
+ ) : null}
+ {steps.length > 0 ? (
+
+ {step?.map((elem, i) => (
+
1 ? 0 : "0 auto" }}>
+ {" "}
+ {/* To center element when there is only one */}
+
+ {step.length > 1 && (
+
+ The data visualizer helps you to visualize data structures.
+
+
+ It is activated by calling the function
+
+ draw_data(
+ x
+
+ 1
+
+ ,
+ x
+
+ 2
+
+ , ...,
+ x
+
+ n
+
+ )
+
+ , where
+
+ x
+
+ k
+
+
+ would be the
+
+ k
+
+ th
+
+
+ data structure that you want to visualize and
+
+ n
+
+ is the number of structures.
+
+
+ The data visualizer uses box-and-pointer diagrams, as introduced in
+
+
+ SICP JS Section 2.2
+
+
+ .
+
+
+
+`;
diff --git a/src/web/data-display/src/__tests__/snapshots.test.tsx b/src/web/data-display/src/__tests__/snapshots.test.tsx
new file mode 100644
index 0000000..e370d98
--- /dev/null
+++ b/src/web/data-display/src/__tests__/snapshots.test.tsx
@@ -0,0 +1,187 @@
+import { expect, test } from "vitest";
+import makeDataVisualizerTabFrom from "../SideContentDataVisualizer";
+import { render } from "@testing-library/react";
+import type { Data } from "@sourceacademy/common-data-display";
+import { Children, isValidElement, type ReactElement, type ReactNode } from "react";
+import DataVisualizer from "../DataVisualizer";
+import type { Step } from "../DataVisualizerTypes";
+
+const MOCK_CONFIG = {
+ sicpTextbookName: "SICP JS Section 2.2",
+ sicpTextbookUrl: "https://sourceacademy.org/sicpjs/2.2.html",
+ functionCallText: "draw_data(x1, x2, ..., xn)",
+};
+
+type DrawableSnapshot = {
+ type: string;
+ key?: string;
+ props?: Record;
+ children?: unknown[];
+};
+
+function elementTypeName(type: unknown): string {
+ if (typeof type === "string") {
+ return type;
+ }
+ if (typeof type === "function") {
+ const component = type as { displayName?: string; name?: string };
+ return component.displayName ?? component.name ?? "Unknown";
+ }
+ if (typeof type === "object" && type !== null && "type" in type) {
+ return elementTypeName(type.type);
+ }
+ return "Unknown";
+}
+
+function summarizeTreeNode(node: unknown): Record {
+ const treeNode = node as {
+ actualNode?: unknown;
+ children?: unknown[] | null;
+ constructor?: { name?: string };
+ data?: unknown;
+ drawableX?: number;
+ drawableY?: number;
+ nodeColor?: number;
+ nodePos?: number;
+ };
+ const type = treeNode.constructor?.name ?? "Unknown";
+
+ switch (type) {
+ case "DataTreeNode":
+ return { type, data: treeNode.data };
+ case "AlreadyParsedTreeNode":
+ return { type, actualNode: summarizeTreeNodeReference(treeNode.actualNode) };
+ default:
+ return summarizeTreeNodeReference(node);
+ }
+}
+
+function summarizeTreeNodeReference(node: unknown): Record {
+ const treeNode = node as {
+ children?: unknown[] | null;
+ constructor?: { name?: string };
+ drawableX?: number;
+ drawableY?: number;
+ nodeColor?: number;
+ nodePos?: number;
+ };
+ return {
+ type: treeNode.constructor?.name ?? "Unknown",
+ childCount: treeNode.children?.length ?? 0,
+ nodeColor: treeNode.nodeColor,
+ nodePos: treeNode.nodePos,
+ drawableX: treeNode.drawableX,
+ drawableY: treeNode.drawableY,
+ };
+}
+
+function snapshotValue(key: string, value: unknown): unknown {
+ if (key === "nodes" && Array.isArray(value)) {
+ return value.map(summarizeTreeNode);
+ }
+ if (isValidElement(value)) {
+ return snapshotElement(value);
+ }
+ if (Array.isArray(value)) {
+ return value.map(item => snapshotValue("", item));
+ }
+ if (typeof value === "function") {
+ return `[Function ${value.name}]`;
+ }
+ if (typeof value === "object" && value !== null) {
+ return Object.fromEntries(
+ Object.entries(value).map(([childKey, childValue]) => [
+ childKey,
+ snapshotValue(childKey, childValue),
+ ]),
+ );
+ }
+ return value;
+}
+
+function snapshotProps(props: Record): Record | undefined {
+ const entries = Object.entries(props)
+ .filter(([key]) => key !== "children")
+ .map(([key, value]) => [key, snapshotValue(key, value)] as const);
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
+}
+
+function snapshotElement(element: ReactElement): DrawableSnapshot {
+ const props = element.props as Record;
+ const children = Children.toArray(props.children as ReactNode).map(child =>
+ isValidElement(child) ? snapshotElement(child) : child,
+ );
+ const snapshot: DrawableSnapshot = {
+ type: elementTypeName(element.type),
+ };
+
+ if (element.key != null) {
+ snapshot.key = "" + element.key;
+ }
+
+ const propsSnapshot = snapshotProps(props);
+ if (propsSnapshot !== undefined) {
+ snapshot.props = propsSnapshot;
+ }
+
+ if (children.length > 0) {
+ snapshot.children = children;
+ }
+
+ return snapshot;
+}
+
+function snapshotDrawingDrawables(drawing: ReactElement) {
+ const stageProps = drawing.props as Record;
+ const layer = Children.toArray(stageProps.children as ReactNode)[0];
+ if (!isValidElement(layer)) {
+ throw new Error("Expected drawing stage to contain a Konva layer");
+ }
+
+ const layerProps = layer.props as Record;
+ return {
+ stage: snapshotProps(stageProps),
+ layer: snapshotProps(layerProps),
+ drawables: Children.toArray(layerProps.children as ReactNode).map(child => {
+ if (!isValidElement(child)) {
+ throw new Error("Expected drawer drawable to be a React element");
+ }
+ return snapshotElement(child);
+ }),
+ };
+}
+test("loading screen (english)", () => {
+ const result = makeDataVisualizerTabFrom("playground", MOCK_CONFIG).body;
+ const { container } = render(result);
+ expect(container).toMatchSnapshot();
+});
+
+test("actual konva drawables", async () => {
+ const result = await new Promise(resolve => {
+ DataVisualizer.init(steps => {
+ if (steps.length == 3) {
+ resolve(steps);
+ }
+ });
+ DataVisualizer.drawData([
+ {
+ type: "array",
+ value: [
+ { type: "string", value: "1" },
+ { type: "string", value: "2" },
+ ],
+ },
+ ]);
+ DataVisualizer.drawData([
+ {
+ type: "array",
+ value: [{ type: "null" }],
+ },
+ ]);
+ const arr: Data = { type: "array", value: [{ type: "string", value: "1" }] };
+ arr.value[1] = arr;
+ DataVisualizer.drawData([arr]);
+ });
+ const drawables = result.map(step => step.map(snapshotDrawingDrawables));
+ expect(drawables).toMatchSnapshot();
+});
diff --git a/src/web/data-display/src/drawable/ArrayDrawable.tsx b/src/web/data-display/src/drawable/ArrayDrawable.tsx
new file mode 100644
index 0000000..fc7acbb
--- /dev/null
+++ b/src/web/data-display/src/drawable/ArrayDrawable.tsx
@@ -0,0 +1,84 @@
+import { PureComponent } from "react";
+import { Group, Line, Rect, Text } from "react-konva";
+
+import { Config } from "../utils/Config";
+import DataVisualizer from "../DataVisualizer";
+import { toText } from "../utils/utils";
+import { DataTreeNode, TreeNode } from "../tree/TreeNode";
+import { NullDrawable } from "./Drawable";
+import { is_list } from "../utils/list";
+
+type ArrayProps = {
+ nodes: TreeNode[];
+ x: number;
+ y: number;
+ color: string;
+};
+
+/**
+ * Represents an array in a tree.
+ */
+class ArrayDrawable extends PureComponent {
+ render() {
+ const createChildText = (node: DataTreeNode, index: number) => {
+ const nodeValue = node.data;
+ if (!is_list(nodeValue)) {
+ const textValue: string | undefined = toText(nodeValue);
+ const textToDisplay = textValue ?? "*" + DataVisualizer.displaySpecialContent(node);
+ return (
+
+ );
+ } else if (nodeValue.type == "null") {
+ const props = {
+ x: index * Config.BoxWidth,
+ y: 0,
+ };
+ return ;
+ } else {
+ return null;
+ }
+ };
+
+ return (
+
+ {/* Outer rectangle */}
+
+ {/* Vertical lines in the box */}
+ {this.props.nodes.length > 1 &&
+ Array.from(Array(this.props.nodes.length - 1), (e, i) => {
+ return (
+
+ );
+ })}
+ {this.props.nodes.map(
+ (child, index) => child instanceof DataTreeNode && createChildText(child, index),
+ )}
+
+ );
+ }
+}
+
+export default ArrayDrawable;
diff --git a/src/web/data-display/src/drawable/ArrowDrawable.tsx b/src/web/data-display/src/drawable/ArrowDrawable.tsx
new file mode 100644
index 0000000..fcc8bc3
--- /dev/null
+++ b/src/web/data-display/src/drawable/ArrowDrawable.tsx
@@ -0,0 +1,63 @@
+import { memo } from "react";
+import { Arrow } from "react-konva";
+
+import { Config } from "../utils/Config";
+import DataVisualizer from "../DataVisualizer";
+import { DataVizMode } from "../DataVisualizerTypes";
+
+type Props = {
+ from: { x: number; y: number };
+ to: { x: number; y: number };
+};
+
+/**
+ * Represents an arrow used to connect a parent node and a child node.
+ *
+ * Used with ArrayDrawable and FunctionDrawable.
+ */
+function ArrowDrawable(props: Props) {
+ if (
+ DataVisualizer.getMode() === DataVizMode.BINARY_TREE ||
+ DataVisualizer.getMode() === DataVizMode.GENERAL_TREE
+ ) {
+ // Binary Tree View and General Tree View
+ return (
+
+ );
+ } else {
+ // OriginalView
+ return (
+
+ );
+ }
+}
+
+export default memo(ArrowDrawable);
diff --git a/src/web/data-display/src/drawable/BackwardArrowDrawable.tsx b/src/web/data-display/src/drawable/BackwardArrowDrawable.tsx
new file mode 100644
index 0000000..698632d
--- /dev/null
+++ b/src/web/data-display/src/drawable/BackwardArrowDrawable.tsx
@@ -0,0 +1,66 @@
+import { memo } from "react";
+import { Arrow } from "react-konva";
+
+import { Config } from "../utils/Config";
+
+type Props = {
+ from: { x: number; y: number };
+ to: { x: number; y: number };
+};
+
+/**
+ * Represents an arrow used to connect a parent node and a child node that has been drawn before,
+ * that is positioned to the top left of the parent node.
+ */
+function BackwardArrowDrawable({ from, to }: Props) {
+ /*
+ * Connects a box to a previously known box, the arrow path is more complicated.
+ *
+ * After coming out of the starting box, it moves to the left,
+ * Then goes to the correct y-value and turns to reach the top of the end box.
+ * It then directly points to the end box. All turnings are 90 degress.
+ *
+ * │
+ * ▲ │
+ * └─────┘
+ */
+
+ // The starting coordinate is the centre of the starting box
+ // The ending coordinate is along the top edge of the ending box, and Config.ArrowSpaceHorizontal pixels from the left edge
+ const bottomY = from.y + Config.BoxHeight / 2 + Config.ArrowMarginBottom;
+
+ /** The x coordinate of the left most part of the backward arrow */
+ const leftX = to.x - Config.ArrowMarginHorizontal;
+
+ /** The y coordinate of the top most part of the backward arrow */
+ const topY = to.y - Config.ArrowMarginTop;
+
+ const path = [
+ from.x,
+ from.y,
+ from.x,
+ bottomY,
+ leftX,
+ bottomY,
+ leftX,
+ topY,
+ to.x + Config.ArrowPointerOffsetHorizontal,
+ topY,
+ to.x + Config.ArrowPointerOffsetHorizontal,
+ to.y + Config.ArrowPointerOffsetVertical,
+ ];
+
+ return (
+
+ );
+}
+
+export default memo(BackwardArrowDrawable);
diff --git a/src/web/data-display/src/drawable/Drawable.ts b/src/web/data-display/src/drawable/Drawable.ts
new file mode 100644
index 0000000..aad0b32
--- /dev/null
+++ b/src/web/data-display/src/drawable/Drawable.ts
@@ -0,0 +1,5 @@
+export { default as ArrayDrawable } from "./ArrayDrawable";
+export { default as ArrowDrawable } from "./ArrowDrawable";
+export { default as BackwardArrowDrawable } from "./BackwardArrowDrawable";
+export { default as FunctionDrawable } from "./FunctionDrawable";
+export { default as NullDrawable } from "./NullDrawable";
diff --git a/src/web/data-display/src/drawable/FunctionDrawable.tsx b/src/web/data-display/src/drawable/FunctionDrawable.tsx
new file mode 100644
index 0000000..e530bff
--- /dev/null
+++ b/src/web/data-display/src/drawable/FunctionDrawable.tsx
@@ -0,0 +1,64 @@
+import { PureComponent } from "react";
+import { Circle, Group } from "react-konva";
+
+import { Config } from "../utils/Config";
+
+type FunctionProps = {
+ x: number;
+ y: number;
+};
+
+/**
+ * Represents a function object drawn using two circles.
+ */
+class FunctionDrawable extends PureComponent {
+ render() {
+ return (
+
+ {/* Left circle */}
+
+
+ {/* Right circle */}
+
+
+ {/* Left dot */}
+
+
+ {/* Right dot */}
+
+
+ );
+ }
+}
+
+export default FunctionDrawable;
diff --git a/src/web/data-display/src/drawable/NullDrawable.tsx b/src/web/data-display/src/drawable/NullDrawable.tsx
new file mode 100644
index 0000000..9750190
--- /dev/null
+++ b/src/web/data-display/src/drawable/NullDrawable.tsx
@@ -0,0 +1,32 @@
+import { PureComponent } from "react";
+import { Line } from "react-konva";
+
+import { Config } from "../utils/Config";
+
+type NullProps = {
+ x: number;
+ y: number;
+};
+
+/**
+ * Represents the diagonal line drawn over the tail of a pair
+ * when the tail is an empty box.
+ *
+ * Used in conjunction with ArrayDrawable.
+ */
+class NullDrawable extends PureComponent {
+ render() {
+ return (
+
+ );
+ }
+}
+
+export default NullDrawable;
diff --git a/src/web/data-display/src/index.tsx b/src/web/data-display/src/index.tsx
new file mode 100644
index 0000000..f422ec7
--- /dev/null
+++ b/src/web/data-display/src/index.tsx
@@ -0,0 +1,44 @@
+import type { ITabService } from "@sourceacademy/common-tabs";
+import {
+ checkIsPluginClass,
+ type IChannel,
+ type IConduit,
+ type IPlugin,
+} from "@sourceacademy/conductor/conduit";
+import makeDataVisualizerTabFrom from "./SideContentDataVisualizer";
+import DataVisualizer from "./DataVisualizer";
+import {
+ CONFIG_CHANNEL_ID,
+ DATA_CHANNEL_ID,
+ WEB_ID,
+ type Config,
+ type Data,
+} from "@sourceacademy/common-data-display";
+@checkIsPluginClass
+export default class DisplayDataWebPlugin implements IPlugin {
+ id = WEB_ID;
+ static channelAttach = [DATA_CHANNEL_ID, CONFIG_CHANNEL_ID];
+
+ private __dataChannel: IChannel;
+ private __configChannel: IChannel;
+
+ constructor(
+ _conduit: IConduit,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ [dataChannel, configChannel]: IChannel[],
+ tabService: ITabService,
+ ) {
+ this.__dataChannel = dataChannel;
+ this.__configChannel = configChannel;
+ this.__configChannel.subscribe(msg => {
+ if (msg === null) return;
+ const tab = makeDataVisualizerTabFrom("playground", msg);
+ tabService.registerTab(tab);
+ tabService.showTab(tab.id);
+ });
+ this.__configChannel.send(null);
+ this.__dataChannel.subscribe(data => {
+ DataVisualizer.drawData([data]);
+ });
+ }
+}
diff --git a/src/web/data-display/src/tree/AlreadyParsedTreeNode.ts b/src/web/data-display/src/tree/AlreadyParsedTreeNode.ts
new file mode 100644
index 0000000..5e263cf
--- /dev/null
+++ b/src/web/data-display/src/tree/AlreadyParsedTreeNode.ts
@@ -0,0 +1,14 @@
+import { TreeNode } from "./BaseTreeNode";
+import { DrawableTreeNode } from "./DrawableTreeNode";
+
+/**
+ * Represents a node that is already parsed earlier in the tree.
+ */
+export class AlreadyParsedTreeNode extends TreeNode {
+ public actualNode: DrawableTreeNode;
+
+ constructor(actualNode: DrawableTreeNode) {
+ super();
+ this.actualNode = actualNode;
+ }
+}
diff --git a/src/web/data-display/src/tree/ArrayTreeNode.tsx b/src/web/data-display/src/tree/ArrayTreeNode.tsx
new file mode 100644
index 0000000..d2013ea
--- /dev/null
+++ b/src/web/data-display/src/tree/ArrayTreeNode.tsx
@@ -0,0 +1,50 @@
+import { Group } from "react-konva";
+
+import { Config } from "../utils/Config";
+import { ArrayDrawable, ArrowDrawable } from "../drawable/Drawable";
+import { DrawableTreeNode } from "./DrawableTreeNode";
+
+/**
+ * Represents a node corresponding to a Source pair or array.
+ */
+export class ArrayTreeNode extends DrawableTreeNode {
+ Colors: string[] = ["#d81d1d", "#e46510", "#25a232", "#0d54ed", "#e6148f", "#ad0ede"];
+ createDrawable(
+ x: number,
+ y: number,
+ parentX: number,
+ parentY: number,
+ colorIndex: number,
+ ): React.ReactElement {
+ let color = "";
+ color = colorIndex === -1 ? "black" : this.Colors[colorIndex % this.Colors.length];
+ const arrayProps = { nodes: this.children ?? [], x, y, color };
+ const arrayDrawable = ;
+
+ this._drawable = (
+
+ {arrayDrawable}
+ {(parentX !== x || parentY !== y) && (
+
+ )}
+
+ );
+ this.drawableX = x;
+ this.drawableY = y;
+ return this._drawable;
+ }
+}
diff --git a/src/web/data-display/src/tree/BaseTreeNode.ts b/src/web/data-display/src/tree/BaseTreeNode.ts
new file mode 100644
index 0000000..ae35c4d
--- /dev/null
+++ b/src/web/data-display/src/tree/BaseTreeNode.ts
@@ -0,0 +1,13 @@
+export class TreeNode {
+ public children: TreeNode[] | null;
+ public nodePos: number = 0;
+ public nodeColor: number = 0;
+ // Note: Only specific subclasses (e.g., DataTreeNode) are expected to populate `data`.
+ public data?: unknown;
+
+ constructor() {
+ this.children = null;
+ this.nodePos = 0;
+ this.nodeColor = 0;
+ }
+}
diff --git a/src/web/data-display/src/tree/BinaryTreeDrawer.tsx b/src/web/data-display/src/tree/BinaryTreeDrawer.tsx
new file mode 100644
index 0000000..825b86b
--- /dev/null
+++ b/src/web/data-display/src/tree/BinaryTreeDrawer.tsx
@@ -0,0 +1,209 @@
+import Konva from "konva";
+import { Layer, Stage, Text } from "react-konva";
+
+import { Config } from "../utils/Config";
+import DataVisualizer from "../DataVisualizer";
+import { toText } from "../utils/utils";
+import { ArrowDrawable, BackwardArrowDrawable } from "../drawable/Drawable";
+import { AlreadyParsedTreeNode } from "./AlreadyParsedTreeNode";
+import { OriginalDrawer } from "./OriginalDrawer";
+import {
+ ArrayTreeNode,
+ DataTreeNode,
+ DrawableTreeNode,
+ FunctionTreeNode,
+ TreeNode,
+} from "./TreeNode";
+
+/**
+ * Tree drawer for binary tree view
+ */
+export class BinaryTreeDrawer extends OriginalDrawer {
+ draw(x: number, y: number, key: number): React.ReactElement {
+ // NON-BINARY TREE WARNING
+ if (!DataVisualizer.isBinTree) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ if (this.tree.rootNode instanceof DataTreeNode) {
+ const text = toText(this.tree.rootNode.data);
+ const textConfig = {
+ text: text,
+ align: "center",
+ fontStyle: "normal",
+ fontSize: 20,
+ fill: Config.Stroke,
+ };
+ const konvaText = new Konva.Text(textConfig);
+ this.width = konvaText.width();
+ this.height = konvaText.height();
+ return (
+
+
+
+
+
+ );
+ }
+
+ // RenderBinaryTree
+ this.drawNode(this.tree.rootNode, x, y, x, y, 0, 0, 0, 0);
+ this.width = this.getNodeWidth(this.tree.rootNode) - this.minX;
+ this.height = this.getNodeHeight(this.tree.rootNode) - this.minY + Config.StrokeWidth;
+
+ const EY1 = Math.max(this.leftCOUNTER, this.rightCOUNTER);
+ let EY2;
+ if (EY1 === 0) {
+ EY2 = EY1;
+ } else {
+ EY2 = 2 * (Math.pow(2, EY1 - 1) - 1) + 1; // how many nodegroups stretch left or right (not including root)
+ }
+ return (
+
+
+ {this.drawables}
+
+
+ );
+ }
+
+ protected drawNode(
+ node: TreeNode,
+ x: number,
+ y: number,
+ parentX: number,
+ parentY: number,
+ colorIndex: number,
+ parentIndex: number,
+ originIndex: number,
+ originX: number,
+ ) {
+ if (node instanceof AlreadyParsedTreeNode) {
+ // if its child is part of a cycle and it's been drawn, link back to that node instead
+ const drawnNode = node.actualNode;
+ const arrowProps = {
+ from: {
+ x: parentX + Config.BoxWidth / 2,
+ y: parentY + Config.BoxHeight / 2,
+ },
+ to: {
+ x: drawnNode.drawableX!,
+ y: drawnNode.drawableY!,
+ },
+ };
+
+ const isBackwardArrow = arrowProps.from.y >= arrowProps.to.y;
+
+ let arrow: React.ReactElement;
+
+ if (isBackwardArrow) {
+ // Update the minX and minY, in case overflow to the top or left happens
+ this.minX = Math.min(
+ this.minX,
+ drawnNode.drawableX! - Config.ArrowMarginHorizontal - Config.StrokeWidth / 2,
+ );
+ this.minY = Math.min(
+ this.minY,
+ drawnNode.drawableY! - Config.ArrowMarginTop - Config.StrokeWidth / 2,
+ );
+ arrow = (
+
+ );
+ } else {
+ arrow = ;
+ }
+ this.drawables.push(arrow);
+ }
+
+ if (!(node instanceof DrawableTreeNode)) return;
+
+ // draws the content
+ if (node instanceof FunctionTreeNode) {
+ const drawable = node.createDrawable(x, y, parentX, parentY, 0);
+ this.drawables.push(drawable);
+ } else if (node instanceof ArrayTreeNode) {
+ // RenderBinaryTree
+ const drawable = node.createDrawable(x, y, parentX, parentY, node.nodeColor);
+ this.drawables.push(drawable);
+
+ node.children?.forEach((childNode, index) => {
+ let myY;
+ let myX;
+ let scalerV = Math.round(
+ Math.pow(2, DataVisualizer.TreeDepth) /
+ Math.pow(2, Math.round(y / (6 * Config.BoxHeight))),
+ );
+ scalerV--;
+
+ if (index === 0 && y === parentY + Config.DistanceY) {
+ // NEW left branch
+ myY = y + Config.DistanceY * 2;
+ myX = x - Config.NWidth * scalerV;
+ } else if (index === 0) {
+ // NEW right branch
+ myY = y + Config.DistanceY * 2;
+ myX = x + Config.NWidth * scalerV;
+ } else if (y === parentY + Config.DistanceY) {
+ // third box
+ myY = y;
+ myX = x + Config.NWidth * 2;
+ colorIndex = parentIndex;
+ } else {
+ // second box
+ myY = y + Config.DistanceY;
+ myX = x - Config.NWidth;
+ colorIndex = parentIndex;
+ }
+
+ if (x < this.runningX && index === 0 && y === parentY + Config.DistanceY) {
+ // NEW left branches that stretch towards the left
+ this.leftCOUNTER++;
+ this.runningX = myX;
+ } else if (x > this.runningX2 && index === 0 && y === parentY + Config.DistanceY) {
+ // NEW right branches that stretch towards the right
+ this.rightCOUNTER++;
+ this.runningX2 = myX;
+ }
+
+ if (y > this.runningY && index === 0) {
+ // NEW branches (doesn't matter left or right) that stretches down
+ this.downCOUNTER++;
+ this.runningY = myY;
+ }
+
+ this.drawNode(
+ childNode,
+ myX,
+ myY,
+ x + Config.BoxWidth * index,
+ y,
+ colorIndex,
+ colorIndex,
+ 0,
+ 0,
+ );
+ });
+ }
+ }
+}
diff --git a/src/web/data-display/src/tree/DataTreeNode.tsx b/src/web/data-display/src/tree/DataTreeNode.tsx
new file mode 100644
index 0000000..2934bb0
--- /dev/null
+++ b/src/web/data-display/src/tree/DataTreeNode.tsx
@@ -0,0 +1,14 @@
+import type { Data } from "@sourceacademy/common-data-display";
+import { TreeNode } from "./BaseTreeNode";
+
+/**
+ * Represents node corresponding to a data object (neither pair nor function).
+ */
+export class DataTreeNode extends TreeNode {
+ public readonly data: Data;
+
+ constructor(data: Data) {
+ super();
+ this.data = data;
+ }
+}
diff --git a/src/web/data-display/src/tree/DrawableTreeNode.tsx b/src/web/data-display/src/tree/DrawableTreeNode.tsx
new file mode 100644
index 0000000..65b0a0e
--- /dev/null
+++ b/src/web/data-display/src/tree/DrawableTreeNode.tsx
@@ -0,0 +1,33 @@
+import { TreeNode } from "./BaseTreeNode";
+
+/**
+ * Represents a node whose drawable should be cached.
+ *
+ * Concrete implementations: ArrayTreeNode, FunctionTreeNode
+ */
+export abstract class DrawableTreeNode extends TreeNode {
+ protected _drawable?: React.ReactElement;
+ public drawableX?: number;
+ public drawableY?: number;
+
+ get drawable() {
+ return this._drawable;
+ }
+
+ /**
+ * Creates a Konva object representing the drawable. Should be called only once.
+ * Subsequent references of the drawable to be obtained using the drawable getter.
+ *
+ * @param x The x position of the drawable.
+ * @param y The y position of the drawable.
+ * @param parentX The x position of the parent.
+ * @param parentY The y position of the parent.
+ */
+ abstract createDrawable(
+ x: number,
+ y: number,
+ parentX: number,
+ parentY: number,
+ colorIndex: number,
+ ): React.ReactElement;
+}
diff --git a/src/web/data-display/src/tree/FunctionTreeNode.tsx b/src/web/data-display/src/tree/FunctionTreeNode.tsx
new file mode 100644
index 0000000..cf6e3c0
--- /dev/null
+++ b/src/web/data-display/src/tree/FunctionTreeNode.tsx
@@ -0,0 +1,43 @@
+import { Group } from "react-konva";
+
+import { Config } from "../utils/Config";
+import { ArrowDrawable, FunctionDrawable } from "../drawable/Drawable";
+import { DrawableTreeNode } from "./DrawableTreeNode";
+
+/**
+ * Represents a node corresponding to a Source (and Javascript) function.
+ */
+export class FunctionTreeNode extends DrawableTreeNode {
+ createDrawable(
+ x: number,
+ y: number,
+ parentX: number,
+ parentY: number,
+ colorIndex: number,
+ ): React.ReactElement {
+ this._drawable = (
+
+
+ {(parentX !== x || parentY !== y) && (
+
+ )}
+
+ );
+
+ this.drawableX = x;
+ this.drawableY = y;
+
+ return this._drawable;
+ }
+}
diff --git a/src/web/data-display/src/tree/GeneralTreeDrawer.tsx b/src/web/data-display/src/tree/GeneralTreeDrawer.tsx
new file mode 100644
index 0000000..eca361d
--- /dev/null
+++ b/src/web/data-display/src/tree/GeneralTreeDrawer.tsx
@@ -0,0 +1,199 @@
+import Konva from "konva";
+import { Layer, Stage, Text } from "react-konva";
+
+import { Config } from "../utils/Config";
+import DataVisualizer from "../DataVisualizer";
+import { toText } from "../utils/utils";
+import { ArrowDrawable, BackwardArrowDrawable } from "../drawable/Drawable";
+import { AlreadyParsedTreeNode } from "./AlreadyParsedTreeNode";
+import { OriginalDrawer } from "./OriginalDrawer";
+import {
+ ArrayTreeNode,
+ DataTreeNode,
+ DrawableTreeNode,
+ FunctionTreeNode,
+ TreeNode,
+} from "./TreeNode";
+
+/**
+ * Tree drawer for general tree view
+ */
+export class GeneralTreeDrawer extends OriginalDrawer {
+ draw(x: number, y: number, key: number): React.ReactElement {
+ // NON-GENERAL TREE WARNING
+ if (!DataVisualizer.isGenTree) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ if (this.tree.rootNode instanceof DataTreeNode) {
+ const text = toText(this.tree.rootNode.data);
+ const textConfig = {
+ text: text,
+ align: "center",
+ fontStyle: "normal",
+ fontSize: 20,
+ fill: Config.Stroke,
+ };
+ const konvaText = new Konva.Text(textConfig);
+ this.width = konvaText.width();
+ this.height = konvaText.height();
+ return (
+
+
+
+
+
+ );
+ }
+
+ // RenderGeneralTree
+ this.drawNode(this.tree.rootNode, x, y, x, y, 0, 0, 0, 0);
+ this.width = this.getNodeWidth(this.tree.rootNode) - this.minX;
+ this.height = this.getNodeHeight(this.tree.rootNode) - this.minY + Config.StrokeWidth;
+
+ return (
+
+
+ {this.drawables}
+
+
+ );
+ }
+
+ protected drawNode(
+ node: TreeNode,
+ x: number,
+ y: number,
+ parentX: number,
+ parentY: number,
+ colorIndex: number,
+ parentIndex: number,
+ originIndex: number,
+ originX: number,
+ ) {
+ if (node instanceof AlreadyParsedTreeNode) {
+ // if its child is part of a cycle and it's been drawn, link back to that node instead
+ const drawnNode = node.actualNode;
+ const arrowProps = {
+ from: {
+ x: parentX + Config.BoxWidth / 2,
+ y: parentY + Config.BoxHeight / 2,
+ },
+ to: {
+ x: drawnNode.drawableX!,
+ y: drawnNode.drawableY!,
+ },
+ };
+
+ const isBackwardArrow = arrowProps.from.y >= arrowProps.to.y;
+
+ let arrow: React.ReactElement;
+
+ if (isBackwardArrow) {
+ // Update the minX and minY, in case overflow to the top or left happens
+ this.minX = Math.min(
+ this.minX,
+ drawnNode.drawableX! - Config.ArrowMarginHorizontal - Config.StrokeWidth / 2,
+ );
+ this.minY = Math.min(
+ this.minY,
+ drawnNode.drawableY! - Config.ArrowMarginTop - Config.StrokeWidth / 2,
+ );
+ arrow = (
+
+ );
+ } else {
+ arrow = ;
+ }
+ this.drawables.push(arrow);
+ }
+
+ if (!(node instanceof DrawableTreeNode)) return;
+
+ // draws the content
+ if (node instanceof FunctionTreeNode) {
+ const drawable = node.createDrawable(x, y, parentX, parentY, 0);
+ this.drawables.push(drawable);
+ } else if (node instanceof ArrayTreeNode) {
+ // RenderGeneralTree
+ const drawable = node.createDrawable(x, y, parentX, parentY, node.nodeColor);
+ this.drawables.push(drawable);
+
+ const longest = DataVisualizer.nodeCount[0]; // e.g. 3
+ this.runningX2 = (Config.NWidth + Config.BoxWidth) * (longest + 1);
+ this.downCOUNTER = DataVisualizer.TreeDepth;
+
+ node.children?.forEach((childNode, index) => {
+ let myY;
+ let myX;
+
+ if (index == 0) {
+ myY = y + Config.DistanceY * 2;
+ myX = originX;
+ } else {
+ myY = y;
+ myX = x + Config.NWidth + Config.BoxWidth;
+ colorIndex = parentIndex;
+ }
+
+ if (x > this.runningX2 && index == 0 && y == parentY + Config.DistanceY * 2) {
+ // NEW right branches that stretch towards the right
+ this.rightCOUNTER++;
+ this.runningX2 = myX;
+ }
+
+ const rightChild = node.children?.[1];
+ if (rightChild instanceof ArrayTreeNode) {
+ const rightLeftGrandchild = rightChild.children?.[0];
+ if (rightLeftGrandchild instanceof ArrayTreeNode) {
+ originIndex = rightLeftGrandchild.nodePos;
+ originX = this.leftMargin + (Config.NWidth + Config.BoxWidth) * originIndex;
+ }
+ }
+
+ if (index === 0) {
+ const leftChild = node.children?.[0];
+ if (leftChild instanceof ArrayTreeNode) {
+ const leftLeftGrandchild = leftChild.children?.[0];
+ if (leftLeftGrandchild instanceof ArrayTreeNode) {
+ originIndex = leftLeftGrandchild.nodePos;
+ originX = this.leftMargin + (Config.NWidth + Config.BoxWidth) * originIndex;
+ }
+ }
+ }
+
+ this.drawNode(
+ childNode,
+ myX,
+ myY,
+ x + Config.BoxWidth * index,
+ y,
+ colorIndex,
+ colorIndex,
+ originIndex,
+ originX,
+ );
+ });
+ }
+ }
+}
diff --git a/src/web/data-display/src/tree/OriginalDrawer.tsx b/src/web/data-display/src/tree/OriginalDrawer.tsx
new file mode 100644
index 0000000..d146a5d
--- /dev/null
+++ b/src/web/data-display/src/tree/OriginalDrawer.tsx
@@ -0,0 +1,218 @@
+import Konva from "konva";
+import { Layer, Stage, Text } from "react-konva";
+
+import { Config } from "../utils/Config";
+import { toText } from "../utils/utils";
+import { ArrowDrawable, BackwardArrowDrawable } from "../drawable/Drawable";
+import { AlreadyParsedTreeNode } from "./AlreadyParsedTreeNode";
+import { Tree } from "./Tree";
+import {
+ ArrayTreeNode,
+ DataTreeNode,
+ DrawableTreeNode,
+ FunctionTreeNode,
+ TreeNode,
+} from "./TreeNode";
+
+/**
+ * Base tree drawer for original view
+ */
+export class OriginalDrawer {
+ protected tree: Tree;
+ public leftCOUNTER: number = 0;
+ public rightCOUNTER: number = 0;
+ public downCOUNTER: number = 0;
+ protected runningX: number = 0;
+ protected runningY: number = 0;
+ protected runningX2: number = 0; // for rightCOUNTER
+
+ protected drawables: React.ReactElement[];
+ protected nodeWidths: Map;
+ public width: number = 0;
+ public height: number = 0;
+
+ // Used to account for backward arrow
+ protected minX = 0;
+ protected minY = 0;
+
+ protected leftMargin: number = Config.StrokeWidth / 2;
+ protected topMargin: number = Config.StrokeWidth / 2;
+
+ constructor(tree: Tree) {
+ this.tree = tree;
+ this.drawables = [];
+ this.nodeWidths = new Map();
+ }
+
+ /**
+ * Draws a tree at x, y, by calling drawNode on the root at x, y.
+ */
+ draw(x: number, y: number, key: number): React.ReactElement {
+ if (this.tree.rootNode instanceof DataTreeNode) {
+ const text = toText(this.tree.rootNode.data);
+ const textConfig = {
+ text: text,
+ align: "center",
+ fontStyle: "normal",
+ fontSize: 20,
+ fill: Config.Stroke,
+ };
+ const konvaText = new Konva.Text(textConfig);
+ this.width = konvaText.width();
+ this.height = konvaText.height();
+ return (
+
+
+
+
+
+ );
+ }
+
+ // OriginalView
+ this.drawNode(this.tree.rootNode, x, y, x, y, 0, 0, 0, 0);
+ this.width = this.getNodeWidth(this.tree.rootNode) - this.minX;
+ this.height = this.getNodeHeight(this.tree.rootNode) - this.minY + Config.StrokeWidth;
+
+ return (
+
+
+ {this.drawables}
+
+
+ );
+ }
+
+ /**
+ * Draws the box for the pair representing the tree, then recursively draws its children.
+ * A slash is drawn for empty lists.
+ *
+ * If a child node has been drawn previously, an arrow is drawn pointing to the children,
+ * instead of drawing the child node again.
+ */
+ protected drawNode(
+ node: TreeNode,
+ x: number,
+ y: number,
+ parentX: number,
+ parentY: number,
+ colorIndex: number,
+ parentIndex: number,
+ originIndex: number,
+ originX: number,
+ ) {
+ if (node instanceof AlreadyParsedTreeNode) {
+ // if its child is part of a cycle and it's been drawn, link back to that node instead
+ const drawnNode = node.actualNode;
+ const arrowProps = {
+ from: {
+ x: parentX + Config.BoxWidth / 2,
+ y: parentY + Config.BoxHeight / 2,
+ },
+ to: {
+ x: drawnNode.drawableX!,
+ y: drawnNode.drawableY!,
+ },
+ };
+
+ const isBackwardArrow = arrowProps.from.y >= arrowProps.to.y;
+
+ let arrow: React.ReactElement;
+
+ if (isBackwardArrow) {
+ // Update the minX and minY, in case overflow to the top or left happens
+ this.minX = Math.min(
+ this.minX,
+ drawnNode.drawableX! - Config.ArrowMarginHorizontal - Config.StrokeWidth / 2,
+ );
+ this.minY = Math.min(
+ this.minY,
+ drawnNode.drawableY! - Config.ArrowMarginTop - Config.StrokeWidth / 2,
+ );
+ arrow = (
+
+ );
+ } else {
+ arrow = ;
+ }
+ this.drawables.push(arrow);
+ }
+
+ if (!(node instanceof DrawableTreeNode)) return;
+
+ // draws the content
+ if (node instanceof FunctionTreeNode) {
+ const drawable = node.createDrawable(x, y, parentX, parentY, -1);
+ this.drawables.push(drawable);
+ } else if (node instanceof ArrayTreeNode) {
+ // OriginalView
+ const drawable = node.createDrawable(x, y, parentX, parentY, -1);
+ this.drawables.push(drawable);
+ let leftX = x;
+ node.children?.forEach((childNode, index) => {
+ const childY = childNode instanceof AlreadyParsedTreeNode ? y : y + Config.DistanceY;
+ this.drawNode(childNode, leftX, childY, x + Config.BoxWidth * index, y, 0, 0, 0, 0);
+ const childNodeWidth = this.getNodeWidth(childNode);
+ leftX += childNodeWidth ? childNodeWidth + Config.DistanceX : 0;
+ });
+ }
+ }
+
+ /**
+ * Returns the width taken up by the node in pixels.
+ */
+ protected getNodeWidth(node: TreeNode): number {
+ if (node instanceof DataTreeNode || node instanceof AlreadyParsedTreeNode) {
+ return 0;
+ } else if (node instanceof FunctionTreeNode) {
+ return Config.CircleRadiusLarge * 4 + 2 * Config.StrokeWidth;
+ } else if (node instanceof ArrayTreeNode) {
+ if (this.nodeWidths.has(node)) {
+ return this.nodeWidths.get(node) ?? 0;
+ } else if (node.children != null) {
+ const childrenWidths = node.children
+ .map(node => this.getNodeWidth(node))
+ .filter(x => x > 0);
+ const childrenWidth =
+ childrenWidths.length > 0 ? childrenWidths.reduce((x, y) => x + y + Config.DistanceX) : 0;
+ const nodeWidth = Math.max(
+ node.children.length * Config.BoxWidth + Config.StrokeWidth,
+ childrenWidth,
+ Config.BoxMinWidth + Config.StrokeWidth,
+ );
+ this.nodeWidths.set(node, nodeWidth);
+ return nodeWidth;
+ } else {
+ return 0;
+ }
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Returns the height taken up by the node in pixels.
+ */
+ protected getNodeHeight(node: TreeNode): number {
+ if (node instanceof DataTreeNode) {
+ return 0;
+ } else if (node instanceof FunctionTreeNode) {
+ return Config.BoxHeight;
+ } else if (node instanceof AlreadyParsedTreeNode) {
+ return Config.ArrowMarginBottom;
+ } else if (node instanceof ArrayTreeNode) {
+ // Height of array node is BoxHeight + StrokeWidth / 2 + max(childrenHeights)
+ return (
+ (node.children ?? [])
+ .map(child => {
+ const childHeight = this.getNodeHeight(child);
+ return childHeight + (child instanceof DrawableTreeNode ? Config.DistanceY / 2 : 0);
+ })
+ .filter(height => height > 0)
+ .reduce((x, y) => Math.max(x, y), 0) + Config.BoxHeight
+ );
+ } else {
+ return 0;
+ }
+ }
+}
diff --git a/src/web/data-display/src/tree/Tree.tsx b/src/web/data-display/src/tree/Tree.tsx
new file mode 100644
index 0000000..37d33e1
--- /dev/null
+++ b/src/web/data-display/src/tree/Tree.tsx
@@ -0,0 +1,137 @@
+import DataVisualizer from "../DataVisualizer";
+import { DataVizMode } from "../DataVisualizerTypes";
+import { AlreadyParsedTreeNode } from "./AlreadyParsedTreeNode";
+import { BinaryTreeDrawer } from "./BinaryTreeDrawer";
+import { GeneralTreeDrawer } from "./GeneralTreeDrawer";
+import { OriginalDrawer } from "./OriginalDrawer";
+import {
+ ArrayTreeNode,
+ DataTreeNode,
+ DrawableTreeNode,
+ FunctionTreeNode,
+ TreeNode,
+} from "./TreeNode";
+import { type ArrayValue, type Data, type FunctionValue } from "@sourceacademy/common-data-display";
+/**
+ * A tree object built based on the given Data, Function or Array of
+ * data/functions/arrays.
+ */
+export class Tree {
+ private _rootNode: TreeNode;
+ private nodes: DrawableTreeNode[];
+
+ /**
+ * Constructs a tree given a root node and a list of nodes.
+ * @param rootNode The root node of the tree.
+ * @param nodes The memoized nodes of the tree in list form.
+ */
+ constructor(rootNode: TreeNode, nodes: DrawableTreeNode[]) {
+ this._rootNode = rootNode;
+ this.nodes = nodes;
+ }
+
+ /**
+ * The root node of the tree.
+ */
+ get rootNode(): TreeNode {
+ return this._rootNode;
+ }
+
+ /**
+ * Returns the memoized node of the given id.
+ * @param id The id of the node.
+ */
+ getNodeById(id: number): DrawableTreeNode {
+ return this.nodes[id];
+ }
+
+ static fromSourceStructure(tree: Data): Tree {
+ let nodeCount = 0;
+ const mode = DataVisualizer.getMode();
+ const genTreeChecker = mode === DataVizMode.GENERAL_TREE;
+ const binTreeChecker = mode === DataVizMode.BINARY_TREE;
+ function constructNode(structure: Data): TreeNode {
+ const alreadyDrawnNode = visitedStructures.get(structure);
+ if (alreadyDrawnNode !== undefined) {
+ return new AlreadyParsedTreeNode(alreadyDrawnNode);
+ }
+ return structure.type === "array"
+ ? constructTree(structure)
+ : structure.type === "function"
+ ? constructFunction(structure)
+ : constructData(structure);
+ }
+
+ /**
+ * Returns a node representing the given tree as a pair.
+ * Also memoizes the pair object, for the case where the
+ * pair appears multiple times in the data structure.
+ * @param tree The Source tree to construct a node for.
+ */
+ function constructTree(tree: ArrayValue): TreeNode {
+ const node = new ArrayTreeNode();
+ visitedStructures.set(tree, node);
+ treeNodes[nodeCount] = node;
+ nodeCount++;
+
+ if (genTreeChecker) {
+ node.nodeColor = DataVisualizer.colorMap.get(tree) ?? 0;
+ node.nodePos = DataVisualizer.posMap.get(tree) ?? 0;
+ }
+ if (binTreeChecker) {
+ node.nodeColor = DataVisualizer.colorMap.get(tree) ?? 0;
+ }
+
+ node.children = tree.value.map(constructNode);
+
+ return node;
+ }
+
+ /**
+ * Returns a node representing the given function.
+ * Also memoizes the function object, for the case where the
+ * function appears multiple times in the data structure.
+ * @param func The function to construct a node for.
+ */
+ function constructFunction(func: FunctionValue): TreeNode {
+ const node = new FunctionTreeNode();
+
+ // memoise current function
+ visitedStructures.set(func, node);
+ treeNodes[nodeCount] = node;
+ nodeCount++;
+
+ //set genTree and binTree to false since we are rendering a function tree
+ DataVisualizer.isGenTree = false;
+ DataVisualizer.isBinTree = false;
+
+ return node;
+ }
+
+ /**
+ * Returns a node representing the given data.
+ * Anything except functions and pairs are considered data, including empty lists.
+ * @param data The data to construct a node for.
+ */
+ function constructData(data: Data) {
+ return new DataTreeNode(data);
+ }
+
+ const visitedStructures: Map = new Map(); // detects cycles
+ const treeNodes: DrawableTreeNode[] = [];
+ const rootNode = constructNode(tree);
+
+ return new Tree(rootNode, treeNodes);
+ }
+
+ draw(): OriginalDrawer | BinaryTreeDrawer | GeneralTreeDrawer {
+ switch (DataVisualizer.getMode()) {
+ case DataVizMode.BINARY_TREE:
+ return new BinaryTreeDrawer(this);
+ case DataVizMode.GENERAL_TREE:
+ return new GeneralTreeDrawer(this);
+ case DataVizMode.NORMAL:
+ return new OriginalDrawer(this);
+ }
+ }
+}
diff --git a/src/web/data-display/src/tree/TreeNode.ts b/src/web/data-display/src/tree/TreeNode.ts
new file mode 100644
index 0000000..f00ea87
--- /dev/null
+++ b/src/web/data-display/src/tree/TreeNode.ts
@@ -0,0 +1,5 @@
+export { ArrayTreeNode } from "./ArrayTreeNode";
+export { TreeNode } from "./BaseTreeNode";
+export { DataTreeNode } from "./DataTreeNode";
+export { DrawableTreeNode } from "./DrawableTreeNode";
+export { FunctionTreeNode } from "./FunctionTreeNode";
diff --git a/src/web/data-display/src/utils/Config.ts b/src/web/data-display/src/utils/Config.ts
new file mode 100644
index 0000000..65ae772
--- /dev/null
+++ b/src/web/data-display/src/utils/Config.ts
@@ -0,0 +1,29 @@
+/**
+ * Represents the config used to draw the drawings.
+ */
+export const Config = {
+ StrokeWidth: 2,
+ Stroke: "white",
+ Fill: "white",
+
+ NWidth: 90,
+ BoxWidth: 45,
+ BoxMinWidth: 15, // Set to half of BoxHeight for empty arrays following CseMachineConfig
+ BoxHeight: 30,
+ BoxSpacingX: 50,
+ BoxSpacingY: 60,
+
+ DistanceX: 45 / 2, // Half of box width
+ DistanceY: 60,
+ CircleRadiusLarge: 15,
+ CircleRadiusSmall: 4,
+
+ ArrowPointerOffsetHorizontal: 13, // Pixels to offset the arrow head by
+ ArrowPointerOffsetVertical: -5, // Pixels to offset the arrow head by
+ ArrowPointerSize: 5,
+ ArrowMarginTop: 15,
+ ArrowMarginBottom: 15,
+ ArrowMarginHorizontal: 15,
+
+ MaxTextLength: 20,
+};
diff --git a/src/web/data-display/src/utils/list.ts b/src/web/data-display/src/utils/list.ts
new file mode 100644
index 0000000..d8a245a
--- /dev/null
+++ b/src/web/data-display/src/utils/list.ts
@@ -0,0 +1,123 @@
+/**
+ * list.js: Supporting lists in the Scheme style, using pairs made
+ * up of two-element JavaScript arrays (vectors)
+ * (Adapted to Typescript and the new Data type)
+ *
+ * @author Martin Henz
+ */
+
+import type { ArrayValue, Data, EmptyListValue } from "@sourceacademy/common-data-display";
+
+/**
+ * is_pair returns true iff arg is a two-element array
+ * @param x - the argument to check
+ * @returns true if x is a two-element array, false otherwise
+ */
+export function is_pair(x: Data): x is ArrayValue {
+ return x.type === "array" && x.value.length === 2;
+}
+
+/**
+ * head returns the first component of the given pair,
+ * throws an exception if the argument is not a pair
+ * @param xs - the pair to get the head of
+ * @throws Error if xs is not a pair
+ * @returns the first component of the pair
+ */
+export function head(xs: Data): Data {
+ if (is_pair(xs)) {
+ return xs.value[0];
+ } else {
+ throw new Error("head(xs) expects a pair as argument xs, but encountered " + xs);
+ }
+}
+
+/**
+ * tail returns the second component of the given pair
+ * throws an exception if the argument is not a pair
+ * @param xs - the pair to get the tail of
+ * @throws an exception if xs is not a pair
+ * @returns the second component of the pair
+ */
+export function tail(xs: Data): Data {
+ if (is_pair(xs)) {
+ return xs.value[1];
+ } else {
+ throw new Error("tail(xs) expects a pair as argument xs, but encountered " + xs);
+ }
+}
+
+/**
+ * is_list recurses down the list and checks that it ends with the empty list []
+ * @param xs - the list to check
+ * @returns true if xs is a list, false otherwise
+ */
+export function is_list(xs: Data): xs is ArrayValue | EmptyListValue {
+ for (; ; xs = tail(xs)) {
+ if (xs === null) {
+ return true;
+ } else if (!is_pair(xs)) {
+ return false;
+ }
+ }
+}
+
+/**
+ * map_list applies the given function to each element of the list and returns a new list with the results
+ * @param f - the function to apply to each element of the list
+ * @param xs - the list to map over
+ * @returns a new list with the results of applying f to each element of xs
+ */
+export function map_list(f: (x: Data) => Data, xs: Data): Data {
+ if (!is_list(xs)) {
+ throw new Error("map_list(f, xs) expects a list as argument xs, but encountered " + xs);
+ }
+ if (xs.type === "null") {
+ return xs;
+ } else {
+ return { type: "array", value: [f(head(xs)), map_list(f, tail(xs))] };
+ }
+}
+
+/**
+ * The length_list function computes the length of a list.
+ * @param xs The list to compute the length of.
+ * @returns The length of the list xs.
+ * @throws Error if xs is not a list.
+ */
+export function length_list(xs: Data): number {
+ if (!is_list(xs)) {
+ throw new Error("length_list(xs) expects a list as argument xs, but encountered " + xs);
+ }
+ let length = 0;
+ for (; ; xs = tail(xs)) {
+ if (xs.type === "null") {
+ return length;
+ } else {
+ length++;
+ }
+ }
+}
+
+/**
+ * reduce_list applies a function to each element of a list, accumulating a result.
+ * @param f The function to apply to each element of the list.
+ * @param initial The initial value for the accumulator.
+ * @param xs The list to reduce.
+ * @returns The accumulated result after applying f to each element of xs.
+ */
+export function reduce_list(f: (acc: T, x: Data) => T, initial: T, xs: Data): T {
+ if (!is_list(xs)) {
+ throw new Error(
+ "reduce_list(f, initial, xs) expects a list as argument xs, but encountered " + xs,
+ );
+ }
+ let acc = initial;
+ for (; ; xs = tail(xs)) {
+ if (xs.type === "null") {
+ return acc;
+ } else {
+ acc = f(acc, head(xs));
+ }
+ }
+}
diff --git a/src/web/data-display/src/utils/utils.ts b/src/web/data-display/src/utils/utils.ts
new file mode 100644
index 0000000..28bce59
--- /dev/null
+++ b/src/web/data-display/src/utils/utils.ts
@@ -0,0 +1,18 @@
+import { Config } from "./Config";
+import { type Data } from "@sourceacademy/common-data-display";
+
+/**
+ * Returns data in text form, fitted into the box.
+ * If not possible to fit data, return undefined. A number will be assigned and logged in the console.
+ */
+export function toText(data: Data, full: boolean = false): string | undefined {
+ if (data.type !== "string") {
+ return undefined;
+ }
+ if (full) {
+ return data.value;
+ }
+ const dataString = data.value;
+ const str = dataString.substring(0, Config.MaxTextLength);
+ return `${str}${dataString.length > Config.MaxTextLength ? "..." : ""}`;
+}
diff --git a/src/web/data-display/tsconfig.json b/src/web/data-display/tsconfig.json
new file mode 100644
index 0000000..7dfc94a
--- /dev/null
+++ b/src/web/data-display/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "../tsconfig.json",
+ "exclude": ["./dist"],
+ "include": ["./src"],
+ "compilerOptions": {
+ "declaration": true,
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "jsx": "react-jsx"
+ }
+}
diff --git a/src/web/tsconfig.json b/src/web/tsconfig.json
index f628782..eda3fa8 100644
--- a/src/web/tsconfig.json
+++ b/src/web/tsconfig.json
@@ -1,6 +1,7 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
- "jsx": "react-jsx"
+ "jsx": "react-jsx",
+ "types": ["@testing-library/jest-dom/matchers"]
}
}
diff --git a/vitest-setup.js b/vitest-setup.js
new file mode 100644
index 0000000..f149f27
--- /dev/null
+++ b/vitest-setup.js
@@ -0,0 +1 @@
+import "@testing-library/jest-dom/vitest";
diff --git a/vitest.config.js b/vitest.config.js
index e83a239..121eb19 100644
--- a/vitest.config.js
+++ b/vitest.config.js
@@ -7,5 +7,7 @@ export default defineConfig({
reporter: "lcov",
exclude: ["**/dist"],
},
+ environment: "jsdom",
+ setupFiles: ["./vitest-setup.js"],
},
});
diff --git a/yarn.lock b/yarn.lock
index 376d72a..2805577 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5,7 +5,54 @@ __metadata:
version: 8
cacheKey: 10c0
-"@babel/code-frame@npm:^7.29.7":
+"@adobe/css-tools@npm:^4.4.0":
+ version: 4.5.0
+ resolution: "@adobe/css-tools@npm:4.5.0"
+ checksum: 10c0/fc969e1117098eb4cccdb73beb2508daa0e52760af1183d6288bafea59204943490ab3ede28593032ffb8929c0cee270b2a53254fe61139ab00604ea8fc33cea
+ languageName: node
+ linkType: hard
+
+"@asamuzakjp/css-color@npm:^5.1.11":
+ version: 5.1.11
+ resolution: "@asamuzakjp/css-color@npm:5.1.11"
+ dependencies:
+ "@asamuzakjp/generational-cache": "npm:^1.0.1"
+ "@csstools/css-calc": "npm:^3.2.0"
+ "@csstools/css-color-parser": "npm:^4.1.0"
+ "@csstools/css-parser-algorithms": "npm:^4.0.0"
+ "@csstools/css-tokenizer": "npm:^4.0.0"
+ checksum: 10c0/32720bdff8daea6a8847aba6cdfae55baa3b4a2690b51d21db7f0382bbd183f3d9f2d5126df50afd889062635684b2819e47113629ee2e80c99389e75f48d060
+ languageName: node
+ linkType: hard
+
+"@asamuzakjp/dom-selector@npm:^7.1.1":
+ version: 7.1.1
+ resolution: "@asamuzakjp/dom-selector@npm:7.1.1"
+ dependencies:
+ "@asamuzakjp/generational-cache": "npm:^1.0.1"
+ "@asamuzakjp/nwsapi": "npm:^2.3.9"
+ bidi-js: "npm:^1.0.3"
+ css-tree: "npm:^3.2.1"
+ is-potential-custom-element-name: "npm:^1.0.1"
+ checksum: 10c0/8cec1c618781c94de5836a215bbe5aafb4d8b835b18c51faf8547f4574afa39f92def3951e40123860062467613dd825f1e1600ff32e8045cc099a91796dcfb8
+ languageName: node
+ linkType: hard
+
+"@asamuzakjp/generational-cache@npm:^1.0.1":
+ version: 1.0.1
+ resolution: "@asamuzakjp/generational-cache@npm:1.0.1"
+ checksum: 10c0/1de62de43764e13fca3b9a31b7ea9b1bf0780fe053d266e40378a19ff8c66b543e011e6a0df02d410cd59bf981126706f176cdbb938985165202c4a079fe1057
+ languageName: node
+ linkType: hard
+
+"@asamuzakjp/nwsapi@npm:^2.3.9":
+ version: 2.3.9
+ resolution: "@asamuzakjp/nwsapi@npm:2.3.9"
+ checksum: 10c0/869b81382e775499c96c45c6dbe0d0766a6da04bcf0abb79f5333535c4e19946851acaa43398f896e2ecc5a1de9cf3db7cf8c4b1afac1ee3d15e21584546d74d
+ languageName: node
+ linkType: hard
+
+"@babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.29.7":
version: 7.29.7
resolution: "@babel/code-frame@npm:7.29.7"
dependencies:
@@ -144,7 +191,7 @@ __metadata:
languageName: node
linkType: hard
-"@babel/runtime@npm:^7.5.5":
+"@babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.29.2, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7":
version: 7.29.7
resolution: "@babel/runtime@npm:7.29.7"
checksum: 10c0/ca11572f7146b21e0bde6a9ed4bb6a89eafbee5f0944c7eb54d0d8a2dac962c33638a1d611e14faa71dfbb92b4b5f9236232208568a6b7d5c6f3f39ddb91771e
@@ -187,7 +234,43 @@ __metadata:
languageName: node
linkType: hard
-"@blueprintjs/icons@npm:^6.0.0":
+"@blueprintjs/colors@npm:^5.1.16":
+ version: 5.1.16
+ resolution: "@blueprintjs/colors@npm:5.1.16"
+ dependencies:
+ tslib: "npm:~2.6.2"
+ checksum: 10c0/e6019c658d48240b69a855d0757d2097282458d60459f26be59458c7b5b3e170bc4d62e7c0b67893f7e1b7bcf46ea6fcd7e4f38e1483f2762aa865080de7b650
+ languageName: node
+ linkType: hard
+
+"@blueprintjs/core@npm:^6.16.0":
+ version: 6.16.0
+ resolution: "@blueprintjs/core@npm:6.16.0"
+ dependencies:
+ "@blueprintjs/colors": "npm:^5.1.16"
+ "@blueprintjs/icons": "npm:^6.11.0"
+ "@floating-ui/react": "npm:^0.27.13"
+ "@popperjs/core": "npm:^2.11.8"
+ classnames: "npm:^2.3.1"
+ normalize.css: "npm:^8.0.1"
+ react-popper: "npm:^2.3.0"
+ react-transition-group: "npm:^4.4.5"
+ tslib: "npm:~2.6.2"
+ peerDependencies:
+ "@types/react": 18 || 19
+ react: 18 || 19
+ react-dom: 18 || 19
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ bin:
+ upgrade-blueprint-2.0.0-rename: scripts/upgrade-blueprint-2.0.0-rename.sh
+ upgrade-blueprint-3.0.0-rename: scripts/upgrade-blueprint-3.0.0-rename.sh
+ checksum: 10c0/5a55fa5637d8def84bc7db04d00291898e6ef776b3941f7518a2a2fbc8dd7da46486addbd54ad108462d83ccc9a79983a0ba87730af7ca3e3fcd9763674b83da
+ languageName: node
+ linkType: hard
+
+"@blueprintjs/icons@npm:^6.0.0, @blueprintjs/icons@npm:^6.11.0":
version: 6.11.0
resolution: "@blueprintjs/icons@npm:6.11.0"
dependencies:
@@ -205,6 +288,17 @@ __metadata:
languageName: node
linkType: hard
+"@bramus/specificity@npm:^2.4.2":
+ version: 2.4.2
+ resolution: "@bramus/specificity@npm:2.4.2"
+ dependencies:
+ css-tree: "npm:^3.0.0"
+ bin:
+ specificity: bin/cli.js
+ checksum: 10c0/c5f4e04e0bca0d2202598207a5eb0733c8109d12a68a329caa26373bec598d99db5bb785b8865fefa00fc01b08c6068138807ceb11a948fe15e904ed6cf4ba72
+ languageName: node
+ linkType: hard
+
"@changesets/apply-release-plan@npm:^7.1.1":
version: 7.1.1
resolution: "@changesets/apply-release-plan@npm:7.1.1"
@@ -438,6 +532,64 @@ __metadata:
languageName: node
linkType: hard
+"@csstools/color-helpers@npm:^6.0.2":
+ version: 6.0.2
+ resolution: "@csstools/color-helpers@npm:6.0.2"
+ checksum: 10c0/4c66574563d7c960010c11e41c2673675baff07c427cca6e8dddffa5777de45770d13ff3efce1c0642798089ad55de52870d9d8141f78db3fa5bba012f2d3789
+ languageName: node
+ linkType: hard
+
+"@csstools/css-calc@npm:^3.2.0, @csstools/css-calc@npm:^3.2.1":
+ version: 3.2.1
+ resolution: "@csstools/css-calc@npm:3.2.1"
+ peerDependencies:
+ "@csstools/css-parser-algorithms": ^4.0.0
+ "@csstools/css-tokenizer": ^4.0.0
+ checksum: 10c0/0191c8d1cd4dffa0d3b6bfd1e78a721934b1d7a6c972966e4fdaa72208c6789e8ff443ee81764a32f1e6107825695b5524ef2b4dc1681b5b29230f2a1277e5df
+ languageName: node
+ linkType: hard
+
+"@csstools/css-color-parser@npm:^4.1.0":
+ version: 4.1.8
+ resolution: "@csstools/css-color-parser@npm:4.1.8"
+ dependencies:
+ "@csstools/color-helpers": "npm:^6.0.2"
+ "@csstools/css-calc": "npm:^3.2.1"
+ peerDependencies:
+ "@csstools/css-parser-algorithms": ^4.0.0
+ "@csstools/css-tokenizer": ^4.0.0
+ checksum: 10c0/7a5ed5cca6ee2d33e6f9710eb00616658efc09df5ed0cf1619f572986180e36c70728bde42a0cc29bd59c6dc4469c04edd4d7f3e52129c3ec9e56a56a85d2d85
+ languageName: node
+ linkType: hard
+
+"@csstools/css-parser-algorithms@npm:^4.0.0":
+ version: 4.0.0
+ resolution: "@csstools/css-parser-algorithms@npm:4.0.0"
+ peerDependencies:
+ "@csstools/css-tokenizer": ^4.0.0
+ checksum: 10c0/94558c2428d6ef0ddef542e86e0a8376aa1263a12a59770abb13ba50d7b83086822c75433f32aa2e7fef00555e1cc88292f9ca5bce79aed232bb3fed73b1528d
+ languageName: node
+ linkType: hard
+
+"@csstools/css-syntax-patches-for-csstree@npm:^1.1.3":
+ version: 1.1.5
+ resolution: "@csstools/css-syntax-patches-for-csstree@npm:1.1.5"
+ peerDependencies:
+ css-tree: ^3.2.1
+ peerDependenciesMeta:
+ css-tree:
+ optional: true
+ checksum: 10c0/a31f0cfb74e2b5ce8a283c47969a202fc3b23c3ee05c6b6beab7f5c14d89c50b82533e446df74f7df0bf88bf23810ed59431353db26e00d5b013995c1ebf07a2
+ languageName: node
+ linkType: hard
+
+"@csstools/css-tokenizer@npm:^4.0.0":
+ version: 4.0.0
+ resolution: "@csstools/css-tokenizer@npm:4.0.0"
+ checksum: 10c0/669cf3d0f9c8e1ffdf8c9955ad8beba0c8cfe03197fe29a4fcbd9ee6f7a18856cfa42c62670021a75183d9ab37f5d14a866e6a9df753a6c07f59e36797a9ea9f
+ languageName: node
+ linkType: hard
+
"@emnapi/core@npm:1.10.0":
version: 1.10.0
resolution: "@emnapi/core@npm:1.10.0"
@@ -712,6 +864,70 @@ __metadata:
languageName: node
linkType: hard
+"@exodus/bytes@npm:^1.11.0, @exodus/bytes@npm:^1.15.0, @exodus/bytes@npm:^1.6.0":
+ version: 1.15.1
+ resolution: "@exodus/bytes@npm:1.15.1"
+ peerDependencies:
+ "@noble/hashes": ^1.8.0 || ^2.0.0
+ peerDependenciesMeta:
+ "@noble/hashes":
+ optional: true
+ checksum: 10c0/333056a6953bbf875d9f3b86c32314de29458d842e5f56f6ef8034b18c2d9660184550093d1bae5de0064043d5e23f54cc03148798d9d29cf5167ac03f2e9f8c
+ languageName: node
+ linkType: hard
+
+"@floating-ui/core@npm:^1.7.5":
+ version: 1.7.5
+ resolution: "@floating-ui/core@npm:1.7.5"
+ dependencies:
+ "@floating-ui/utils": "npm:^0.2.11"
+ checksum: 10c0/f9c52205e198b231d63a387b09c659aab08c46a1899e0b0bbe147b8b4f048b546f15ba17cb5d2a471da9534f1883d979425e13e5c4ceee67be63e4b0abd4db5d
+ languageName: node
+ linkType: hard
+
+"@floating-ui/dom@npm:^1.7.6":
+ version: 1.7.6
+ resolution: "@floating-ui/dom@npm:1.7.6"
+ dependencies:
+ "@floating-ui/core": "npm:^1.7.5"
+ "@floating-ui/utils": "npm:^0.2.11"
+ checksum: 10c0/5c098e0d7b58c9bc769f276cca1766994c2c9c70c92d091a61bba8b3e9be53c011e0a79a8457fc2fb2f3d91697a26eb52e0a4962ef936dc963b45f58613c212f
+ languageName: node
+ linkType: hard
+
+"@floating-ui/react-dom@npm:^2.1.8":
+ version: 2.1.8
+ resolution: "@floating-ui/react-dom@npm:2.1.8"
+ dependencies:
+ "@floating-ui/dom": "npm:^1.7.6"
+ peerDependencies:
+ react: ">=16.8.0"
+ react-dom: ">=16.8.0"
+ checksum: 10c0/26260ca4bb23b57c73b824062505abf977a008ce6e0463bdacca74f7e49853c4cd1d2bbf1a77c6caa17fa37dfffda2c6c4cd07a8737ebd7474aaff7818401d75
+ languageName: node
+ linkType: hard
+
+"@floating-ui/react@npm:^0.27.13":
+ version: 0.27.19
+ resolution: "@floating-ui/react@npm:0.27.19"
+ dependencies:
+ "@floating-ui/react-dom": "npm:^2.1.8"
+ "@floating-ui/utils": "npm:^0.2.11"
+ tabbable: "npm:^6.0.0"
+ peerDependencies:
+ react: ">=17.0.0"
+ react-dom: ">=17.0.0"
+ checksum: 10c0/2a2cdfd3e67e0606833b63f922ad2a9037974f22b944e1cb8c0991b4c40450f8413d69745c0bbf4646e5ba283747f60d2fdc9a8d289b68b24448e59d81a3a96d
+ languageName: node
+ linkType: hard
+
+"@floating-ui/utils@npm:^0.2.11":
+ version: 0.2.11
+ resolution: "@floating-ui/utils@npm:0.2.11"
+ checksum: 10c0/f4bcea1559bdbb721ecc8e8ead423ac58d6a5b6e70b602cf0810ba6ad4ed1c77211b207faa88b278a9042f0c743133de08a203ed6741c1b6443423332884d5b3
+ languageName: node
+ linkType: hard
+
"@humanfs/core@npm:^0.19.2":
version: 0.19.2
resolution: "@humanfs/core@npm:0.19.2"
@@ -838,6 +1054,15 @@ __metadata:
languageName: node
linkType: hard
+"@mantine/hooks@npm:^9.3.1":
+ version: 9.3.2
+ resolution: "@mantine/hooks@npm:9.3.2"
+ peerDependencies:
+ react: ^19.2.0
+ checksum: 10c0/b9c3982236aabc7a85fb01b70b4012d2afe0fe67144e0929857722afdc7fa8886266bec51c075551d33a84ee2bbd1cfce057f2d4839a877217b8412eb174332b
+ languageName: node
+ linkType: hard
+
"@manypkg/find-root@npm:^1.1.0":
version: 1.1.0
resolution: "@manypkg/find-root@npm:1.1.0"
@@ -910,6 +1135,13 @@ __metadata:
languageName: node
linkType: hard
+"@popperjs/core@npm:^2.11.8":
+ version: 2.11.8
+ resolution: "@popperjs/core@npm:2.11.8"
+ checksum: 10c0/4681e682abc006d25eb380d0cf3efc7557043f53b6aea7a5057d0d1e7df849a00e281cd8ea79c902a35a414d7919621fc2ba293ecec05f413598e0b23d5a1e63
+ languageName: node
+ linkType: hard
+
"@rolldown/binding-android-arm64@npm:1.0.3":
version: 1.0.3
resolution: "@rolldown/binding-android-arm64@npm:1.0.3"
@@ -1064,6 +1296,21 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/plugin-replace@npm:^6.0.3":
+ version: 6.0.3
+ resolution: "@rollup/plugin-replace@npm:6.0.3"
+ dependencies:
+ "@rollup/pluginutils": "npm:^5.0.1"
+ magic-string: "npm:^0.30.3"
+ peerDependencies:
+ rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
+ peerDependenciesMeta:
+ rollup:
+ optional: true
+ checksum: 10c0/93217c52fe86b03363bc534b5f07963ac4fd6b91f19f6070a66809b0c65a036b3b234624a65a8bfcc01a8dc0e42838feae03c021b0d1e5e91753c503c4d70cd6
+ languageName: node
+ linkType: hard
+
"@rollup/plugin-terser@npm:^1.0.0":
version: 1.0.0
resolution: "@rollup/plugin-terser@npm:1.0.0"
@@ -1305,6 +1552,20 @@ __metadata:
languageName: unknown
linkType: soft
+"@sourceacademy/common-data-display@workspace:^, @sourceacademy/common-data-display@workspace:src/common/data-display":
+ version: 0.0.0-use.local
+ resolution: "@sourceacademy/common-data-display@workspace:src/common/data-display"
+ dependencies:
+ "@rollup/plugin-node-resolve": "npm:^16.0.3"
+ "@rollup/plugin-terser": "npm:^1.0.0"
+ "@rollup/plugin-typescript": "npm:^12.3.0"
+ rollup: "npm:^4.60.2"
+ tslib: "npm:^2.8.1"
+ typescript: "npm:^6.0.3"
+ vitest: "npm:^4.1.9"
+ languageName: unknown
+ linkType: soft
+
"@sourceacademy/common-tabs@workspace:^, @sourceacademy/common-tabs@workspace:src/common/tabs":
version: 0.0.0-use.local
resolution: "@sourceacademy/common-tabs@workspace:src/common/tabs"
@@ -1371,6 +1632,24 @@ __metadata:
languageName: unknown
linkType: soft
+"@sourceacademy/runner-data-display@workspace:src/runner/data-display":
+ version: 0.0.0-use.local
+ resolution: "@sourceacademy/runner-data-display@workspace:src/runner/data-display"
+ dependencies:
+ "@rollup/plugin-node-resolve": "npm:^16.0.3"
+ "@rollup/plugin-terser": "npm:^1.0.0"
+ "@rollup/plugin-typescript": "npm:^12.3.0"
+ "@sourceacademy/common-data-display": "workspace:^"
+ "@sourceacademy/conductor": "npm:>=0.3.0"
+ rollup: "npm:^4.60.2"
+ tslib: "npm:^2.8.1"
+ typescript: "npm:^6.0.3"
+ vitest: "npm:^4.1.9"
+ peerDependencies:
+ "@sourceacademy/conductor": ">=0.3.0"
+ languageName: unknown
+ linkType: soft
+
"@sourceacademy/runner-test@workspace:src/runner/test":
version: 0.0.0-use.local
resolution: "@sourceacademy/runner-test@workspace:src/runner/test"
@@ -1409,6 +1688,45 @@ __metadata:
languageName: unknown
linkType: soft
+"@sourceacademy/web-data-display@workspace:src/web/data-display":
+ version: 0.0.0-use.local
+ resolution: "@sourceacademy/web-data-display@workspace:src/web/data-display"
+ dependencies:
+ "@blueprintjs/core": "npm:^6.16.0"
+ "@blueprintjs/icons": "npm:^6.11.0"
+ "@mantine/hooks": "npm:^9.3.1"
+ "@rollup/plugin-commonjs": "npm:^29.0.3"
+ "@rollup/plugin-node-resolve": "npm:^16.0.3"
+ "@rollup/plugin-replace": "npm:^6.0.3"
+ "@rollup/plugin-terser": "npm:^1.0.0"
+ "@rollup/plugin-typescript": "npm:^12.3.0"
+ "@sourceacademy/common-data-display": "workspace:^"
+ "@sourceacademy/common-tabs": "workspace:^"
+ "@sourceacademy/conductor": "npm:>=0.3.0"
+ "@testing-library/dom": "npm:^10.4.1"
+ "@testing-library/jest-dom": "npm:^6.9.1"
+ "@testing-library/react": "npm:^16.3.2"
+ "@types/react": "npm:^19.2.17"
+ canvas: "npm:^3.2.3"
+ classnames: "npm:^2.5.1"
+ i18next: "npm:^26.3.1"
+ konva: "npm:^10.3.0"
+ react: "npm:^19.2.4"
+ react-dom: "npm:^19.2.7"
+ react-i18next: "npm:^17.0.8"
+ react-konva: "npm:^19.0.7"
+ rollup: "npm:^4.60.2"
+ tslib: "npm:^2.8.1"
+ typescript: "npm:^6.0.3"
+ vitest: "npm:^4.1.9"
+ peerDependencies:
+ "@sourceacademy/common-tabs": ^0.0.1
+ "@sourceacademy/conductor": ">=0.3.0"
+ react: ^19
+ react-dom: ^19
+ languageName: unknown
+ linkType: soft
+
"@sourceacademy/web-test@workspace:src/web/test":
version: 0.0.0-use.local
resolution: "@sourceacademy/web-test@workspace:src/web/test"
@@ -1440,6 +1758,56 @@ __metadata:
languageName: node
linkType: hard
+"@testing-library/dom@npm:^10.4.1":
+ version: 10.4.1
+ resolution: "@testing-library/dom@npm:10.4.1"
+ dependencies:
+ "@babel/code-frame": "npm:^7.10.4"
+ "@babel/runtime": "npm:^7.12.5"
+ "@types/aria-query": "npm:^5.0.1"
+ aria-query: "npm:5.3.0"
+ dom-accessibility-api: "npm:^0.5.9"
+ lz-string: "npm:^1.5.0"
+ picocolors: "npm:1.1.1"
+ pretty-format: "npm:^27.0.2"
+ checksum: 10c0/19ce048012d395ad0468b0dbcc4d0911f6f9e39464d7a8464a587b29707eed5482000dad728f5acc4ed314d2f4d54f34982999a114d2404f36d048278db815b1
+ languageName: node
+ linkType: hard
+
+"@testing-library/jest-dom@npm:^6.9.1":
+ version: 6.9.1
+ resolution: "@testing-library/jest-dom@npm:6.9.1"
+ dependencies:
+ "@adobe/css-tools": "npm:^4.4.0"
+ aria-query: "npm:^5.0.0"
+ css.escape: "npm:^1.5.1"
+ dom-accessibility-api: "npm:^0.6.3"
+ picocolors: "npm:^1.1.1"
+ redent: "npm:^3.0.0"
+ checksum: 10c0/4291ebd2f0f38d14cefac142c56c337941775a5807e2a3d6f1a14c2fbd6be76a18e498ed189e95bedc97d9e8cf1738049bc76c85b5bc5e23fae7c9e10f7b3a12
+ languageName: node
+ linkType: hard
+
+"@testing-library/react@npm:^16.3.2":
+ version: 16.3.2
+ resolution: "@testing-library/react@npm:16.3.2"
+ dependencies:
+ "@babel/runtime": "npm:^7.12.5"
+ peerDependencies:
+ "@testing-library/dom": ^10.0.0
+ "@types/react": ^18.0.0 || ^19.0.0
+ "@types/react-dom": ^18.0.0 || ^19.0.0
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/f9c7f0915e1b5f7b750e6c7d8b51f091b8ae7ea99bacb761d7b8505ba25de9cfcb749a0f779f1650fb268b499dd79165dc7e1ee0b8b4cb63430d3ddc81ffe044
+ languageName: node
+ linkType: hard
+
"@tybys/wasm-util@npm:^0.10.2":
version: 0.10.2
resolution: "@tybys/wasm-util@npm:0.10.2"
@@ -1449,6 +1817,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/aria-query@npm:^5.0.1":
+ version: 5.0.4
+ resolution: "@types/aria-query@npm:5.0.4"
+ checksum: 10c0/dc667bc6a3acc7bba2bccf8c23d56cb1f2f4defaa704cfef595437107efaa972d3b3db9ec1d66bc2711bfc35086821edd32c302bffab36f2e79b97f312069f08
+ languageName: node
+ linkType: hard
+
"@types/chai@npm:^5.2.2":
version: 5.2.3
resolution: "@types/chai@npm:5.2.3"
@@ -1510,6 +1885,24 @@ __metadata:
languageName: node
linkType: hard
+"@types/react-reconciler@npm:^0.28.9":
+ version: 0.28.9
+ resolution: "@types/react-reconciler@npm:0.28.9"
+ peerDependencies:
+ "@types/react": "*"
+ checksum: 10c0/9fe71fa856aab2cd4742fe0416bdd4f5c82ecc05ef6451ee7fcb65a5efdf5fa588f5820fbe2a665b15371b0da3bfc4097f28bb6d450b9a834af2d0fc00f403bd
+ languageName: node
+ linkType: hard
+
+"@types/react-reconciler@npm:^0.33.0":
+ version: 0.33.0
+ resolution: "@types/react-reconciler@npm:0.33.0"
+ peerDependencies:
+ "@types/react": "*"
+ checksum: 10c0/190c203d93c0df9a42fabd693ce059dbdf6c53e15eb14502d9e5b946c981231c5846b867de15522ff61368e9218a8508a9db5476f3e47b5d664bbb2c84b31ac7
+ languageName: node
+ linkType: hard
+
"@types/react@npm:^19.2.17":
version: 19.2.17
resolution: "@types/react@npm:19.2.17"
@@ -1814,6 +2207,13 @@ __metadata:
languageName: node
linkType: hard
+"ansi-styles@npm:^5.0.0":
+ version: 5.2.0
+ resolution: "ansi-styles@npm:5.2.0"
+ checksum: 10c0/9c4ca80eb3c2fb7b33841c210d2f20807f40865d27008d7c3f707b7f95cab7d67462a565e2388ac3285b71cb3d9bb2173de8da37c57692a362885ec34d6e27df
+ languageName: node
+ linkType: hard
+
"argparse@npm:^1.0.7":
version: 1.0.10
resolution: "argparse@npm:1.0.10"
@@ -1830,6 +2230,22 @@ __metadata:
languageName: node
linkType: hard
+"aria-query@npm:5.3.0":
+ version: 5.3.0
+ resolution: "aria-query@npm:5.3.0"
+ dependencies:
+ dequal: "npm:^2.0.3"
+ checksum: 10c0/2bff0d4eba5852a9dd578ecf47eaef0e82cc52569b48469b0aac2db5145db0b17b7a58d9e01237706d1e14b7a1b0ac9b78e9c97027ad97679dd8f91b85da1469
+ languageName: node
+ linkType: hard
+
+"aria-query@npm:^5.0.0":
+ version: 5.3.2
+ resolution: "aria-query@npm:5.3.2"
+ checksum: 10c0/003c7e3e2cff5540bf7a7893775fc614de82b0c5dde8ae823d47b7a28a9d4da1f7ed85f340bdb93d5649caa927755f0e31ecc7ab63edfdfc00c8ef07e505e03e
+ languageName: node
+ linkType: hard
+
"array-union@npm:^2.1.0":
version: 2.1.0
resolution: "array-union@npm:2.1.0"
@@ -1851,6 +2267,13 @@ __metadata:
languageName: node
linkType: hard
+"base64-js@npm:^1.3.1":
+ version: 1.5.1
+ resolution: "base64-js@npm:1.5.1"
+ checksum: 10c0/f23823513b63173a001030fae4f2dabe283b99a9d324ade3ad3d148e218134676f1ee8568c877cd79ec1c53158dcf2d2ba527a97c606618928ba99dd930102bf
+ languageName: node
+ linkType: hard
+
"baseline-browser-mapping@npm:^2.10.12":
version: 2.10.25
resolution: "baseline-browser-mapping@npm:2.10.25"
@@ -1869,6 +2292,26 @@ __metadata:
languageName: node
linkType: hard
+"bidi-js@npm:^1.0.3":
+ version: 1.0.3
+ resolution: "bidi-js@npm:1.0.3"
+ dependencies:
+ require-from-string: "npm:^2.0.2"
+ checksum: 10c0/fdddea4aa4120a34285486f2267526cd9298b6e8b773ad25e765d4f104b6d7437ab4ba542e6939e3ac834a7570bcf121ee2cf6d3ae7cd7082c4b5bedc8f271e1
+ languageName: node
+ linkType: hard
+
+"bl@npm:^4.0.3":
+ version: 4.1.0
+ resolution: "bl@npm:4.1.0"
+ dependencies:
+ buffer: "npm:^5.5.0"
+ inherits: "npm:^2.0.4"
+ readable-stream: "npm:^3.4.0"
+ checksum: 10c0/02847e1d2cb089c9dc6958add42e3cdeaf07d13f575973963335ac0fdece563a50ac770ac4c8fa06492d2dd276f6cc3b7f08c7cd9c7a7ad0f8d388b2a28def5f
+ languageName: node
+ linkType: hard
+
"brace-expansion@npm:^5.0.5":
version: 5.0.5
resolution: "brace-expansion@npm:5.0.5"
@@ -1909,6 +2352,16 @@ __metadata:
languageName: node
linkType: hard
+"buffer@npm:^5.5.0":
+ version: 5.7.1
+ resolution: "buffer@npm:5.7.1"
+ dependencies:
+ base64-js: "npm:^1.3.1"
+ ieee754: "npm:^1.1.13"
+ checksum: 10c0/27cac81cff434ed2876058d72e7c4789d11ff1120ef32c9de48f59eab58179b66710c488987d295ae89a228f835fc66d088652dffeb8e3ba8659f80eb091d55e
+ languageName: node
+ linkType: hard
+
"camel-case@npm:^4.1.2":
version: 4.1.2
resolution: "camel-case@npm:4.1.2"
@@ -1926,6 +2379,17 @@ __metadata:
languageName: node
linkType: hard
+"canvas@npm:^3.2.3":
+ version: 3.2.3
+ resolution: "canvas@npm:3.2.3"
+ dependencies:
+ node-addon-api: "npm:^7.0.0"
+ node-gyp: "npm:latest"
+ prebuild-install: "npm:^7.1.3"
+ checksum: 10c0/c0b8b4a093964270c014ac93b5af2c2c452974737c2c1dc988821fe9cc8fd9d6a85f7380c477cb881e057f144a7aa50e743b836a47a69c94e18a4a21277b25d5
+ languageName: node
+ linkType: hard
+
"capital-case@npm:^1.0.4":
version: 1.0.4
resolution: "capital-case@npm:1.0.4"
@@ -1971,6 +2435,13 @@ __metadata:
languageName: node
linkType: hard
+"chownr@npm:^1.1.1":
+ version: 1.1.4
+ resolution: "chownr@npm:1.1.4"
+ checksum: 10c0/ed57952a84cc0c802af900cf7136de643d3aba2eecb59d29344bc2f3f9bf703a301b9d84cdc71f82c3ffc9ccde831b0d92f5b45f91727d6c9da62f23aef9d9db
+ languageName: node
+ linkType: hard
+
"chownr@npm:^3.0.0":
version: 3.0.0
resolution: "chownr@npm:3.0.0"
@@ -1978,7 +2449,7 @@ __metadata:
languageName: node
linkType: hard
-"classnames@npm:^2.3.1":
+"classnames@npm:^2.3.1, classnames@npm:^2.5.1":
version: 2.5.1
resolution: "classnames@npm:2.5.1"
checksum: 10c0/afff4f77e62cea2d79c39962980bf316bacb0d7c49e13a21adaadb9221e1c6b9d3cdb829d8bb1b23c406f4e740507f37e1dcf506f7e3b7113d17c5bab787aa69
@@ -2035,13 +2506,40 @@ __metadata:
languageName: node
linkType: hard
-"csstype@npm:^3.2.2":
+"css-tree@npm:^3.0.0, css-tree@npm:^3.2.1":
+ version: 3.2.1
+ resolution: "css-tree@npm:3.2.1"
+ dependencies:
+ mdn-data: "npm:2.27.1"
+ source-map-js: "npm:^1.2.1"
+ checksum: 10c0/1f65e9ccaa56112a4706d6f003dd43d777f0dbcf848e66fd320f823192533581f8dd58daa906cb80622658332d50284d6be13b87a6ab4556cbbfe9ef535bbf7e
+ languageName: node
+ linkType: hard
+
+"css.escape@npm:^1.5.1":
+ version: 1.5.1
+ resolution: "css.escape@npm:1.5.1"
+ checksum: 10c0/5e09035e5bf6c2c422b40c6df2eb1529657a17df37fda5d0433d722609527ab98090baf25b13970ca754079a0f3161dd3dfc0e743563ded8cfa0749d861c1525
+ languageName: node
+ linkType: hard
+
+"csstype@npm:^3.0.2, csstype@npm:^3.2.2":
version: 3.2.3
resolution: "csstype@npm:3.2.3"
checksum: 10c0/cd29c51e70fa822f1cecd8641a1445bed7063697469d35633b516e60fe8c1bde04b08f6c5b6022136bb669b64c63d4173af54864510fbb4ee23281801841a3ce
languageName: node
linkType: hard
+"data-urls@npm:^7.0.0":
+ version: 7.0.0
+ resolution: "data-urls@npm:7.0.0"
+ dependencies:
+ whatwg-mimetype: "npm:^5.0.0"
+ whatwg-url: "npm:^16.0.0"
+ checksum: 10c0/08d88ef50d8966a070ffdaa703e1e4b29f01bb2da364dfbc1612b1c2a4caa8045802c9532d81347b21781100132addb36a585071c8323b12cce97973961dee9f
+ languageName: node
+ linkType: hard
+
"debug@npm:^4.1.0, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.4.3":
version: 4.4.3
resolution: "debug@npm:4.4.3"
@@ -2054,18 +2552,48 @@ __metadata:
languageName: node
linkType: hard
-"deep-is@npm:^0.1.3":
- version: 0.1.4
- resolution: "deep-is@npm:0.1.4"
- checksum: 10c0/7f0ee496e0dff14a573dc6127f14c95061b448b87b995fc96c017ce0a1e66af1675e73f1d6064407975bc4ea6ab679497a29fff7b5b9c4e99cb10797c1ad0b4c
+"decimal.js@npm:^10.6.0":
+ version: 10.6.0
+ resolution: "decimal.js@npm:10.6.0"
+ checksum: 10c0/07d69fbcc54167a340d2d97de95f546f9ff1f69d2b45a02fd7a5292412df3cd9eb7e23065e532a318f5474a2e1bccf8392fdf0443ef467f97f3bf8cb0477e5aa
languageName: node
linkType: hard
-"deepmerge@npm:^4.2.2":
- version: 4.3.1
- resolution: "deepmerge@npm:4.3.1"
- checksum: 10c0/e53481aaf1aa2c4082b5342be6b6d8ad9dfe387bc92ce197a66dea08bd4265904a087e75e464f14d1347cf2ac8afe1e4c16b266e0561cc5df29382d3c5f80044
- languageName: node
+"decompress-response@npm:^6.0.0":
+ version: 6.0.0
+ resolution: "decompress-response@npm:6.0.0"
+ dependencies:
+ mimic-response: "npm:^3.1.0"
+ checksum: 10c0/bd89d23141b96d80577e70c54fb226b2f40e74a6817652b80a116d7befb8758261ad073a8895648a29cc0a5947021ab66705cb542fa9c143c82022b27c5b175e
+ languageName: node
+ linkType: hard
+
+"deep-extend@npm:^0.6.0":
+ version: 0.6.0
+ resolution: "deep-extend@npm:0.6.0"
+ checksum: 10c0/1c6b0abcdb901e13a44c7d699116d3d4279fdb261983122a3783e7273844d5f2537dc2e1c454a23fcf645917f93fbf8d07101c1d03c015a87faa662755212566
+ languageName: node
+ linkType: hard
+
+"deep-is@npm:^0.1.3":
+ version: 0.1.4
+ resolution: "deep-is@npm:0.1.4"
+ checksum: 10c0/7f0ee496e0dff14a573dc6127f14c95061b448b87b995fc96c017ce0a1e66af1675e73f1d6064407975bc4ea6ab679497a29fff7b5b9c4e99cb10797c1ad0b4c
+ languageName: node
+ linkType: hard
+
+"deepmerge@npm:^4.2.2":
+ version: 4.3.1
+ resolution: "deepmerge@npm:4.3.1"
+ checksum: 10c0/e53481aaf1aa2c4082b5342be6b6d8ad9dfe387bc92ce197a66dea08bd4265904a087e75e464f14d1347cf2ac8afe1e4c16b266e0561cc5df29382d3c5f80044
+ languageName: node
+ linkType: hard
+
+"dequal@npm:^2.0.3":
+ version: 2.0.3
+ resolution: "dequal@npm:2.0.3"
+ checksum: 10c0/f98860cdf58b64991ae10205137c0e97d384c3a4edc7f807603887b7c4b850af1224a33d88012009f150861cbee4fa2d322c4cc04b9313bee312e47f6ecaa888
+ languageName: node
linkType: hard
"detect-indent@npm:^6.0.0":
@@ -2075,7 +2603,7 @@ __metadata:
languageName: node
linkType: hard
-"detect-libc@npm:^2.0.3":
+"detect-libc@npm:^2.0.0, detect-libc@npm:^2.0.3":
version: 2.1.2
resolution: "detect-libc@npm:2.1.2"
checksum: 10c0/acc675c29a5649fa1fb6e255f993b8ee829e510b6b56b0910666949c80c364738833417d0edb5f90e4e46be17228b0f2b66a010513984e18b15deeeac49369c4
@@ -2091,6 +2619,30 @@ __metadata:
languageName: node
linkType: hard
+"dom-accessibility-api@npm:^0.5.9":
+ version: 0.5.16
+ resolution: "dom-accessibility-api@npm:0.5.16"
+ checksum: 10c0/b2c2eda4fae568977cdac27a9f0c001edf4f95a6a6191dfa611e3721db2478d1badc01db5bb4fa8a848aeee13e442a6c2a4386d65ec65a1436f24715a2f8d053
+ languageName: node
+ linkType: hard
+
+"dom-accessibility-api@npm:^0.6.3":
+ version: 0.6.3
+ resolution: "dom-accessibility-api@npm:0.6.3"
+ checksum: 10c0/10bee5aa514b2a9a37c87cd81268db607a2e933a050074abc2f6fa3da9080ebed206a320cbc123567f2c3087d22292853bdfdceaffdd4334ffe2af9510b29360
+ languageName: node
+ linkType: hard
+
+"dom-helpers@npm:^5.0.1":
+ version: 5.2.1
+ resolution: "dom-helpers@npm:5.2.1"
+ dependencies:
+ "@babel/runtime": "npm:^7.8.7"
+ csstype: "npm:^3.0.2"
+ checksum: 10c0/f735074d66dd759b36b158fa26e9d00c9388ee0e8c9b16af941c38f014a37fc80782de83afefd621681b19ac0501034b4f1c4a3bff5caa1b8667f0212b5e124c
+ languageName: node
+ linkType: hard
+
"dot-case@npm:^3.0.4":
version: 3.0.4
resolution: "dot-case@npm:3.0.4"
@@ -2108,6 +2660,15 @@ __metadata:
languageName: node
linkType: hard
+"end-of-stream@npm:^1.1.0, end-of-stream@npm:^1.4.1":
+ version: 1.4.5
+ resolution: "end-of-stream@npm:1.4.5"
+ dependencies:
+ once: "npm:^1.4.0"
+ checksum: 10c0/b0701c92a10b89afb1cb45bf54a5292c6f008d744eb4382fa559d54775ff31617d1d7bc3ef617575f552e24fad2c7c1a1835948c66b3f3a4be0a6c1f35c883d8
+ languageName: node
+ linkType: hard
+
"enquirer@npm:^2.4.1":
version: 2.4.1
resolution: "enquirer@npm:2.4.1"
@@ -2118,6 +2679,13 @@ __metadata:
languageName: node
linkType: hard
+"entities@npm:^8.0.0":
+ version: 8.0.0
+ resolution: "entities@npm:8.0.0"
+ checksum: 10c0/938e631664c19451823344a351aeeafd74fae2d5fa51e4d5b6ff635afaefd4bacf0f609989888c04c42733f46ffdac15211608267ebb02488005891a4793e94d
+ languageName: node
+ linkType: hard
+
"env-paths@npm:^2.2.0":
version: 2.2.1
resolution: "env-paths@npm:2.2.1"
@@ -2393,6 +2961,13 @@ __metadata:
languageName: node
linkType: hard
+"expand-template@npm:^2.0.3":
+ version: 2.0.3
+ resolution: "expand-template@npm:2.0.3"
+ checksum: 10c0/1c9e7afe9acadf9d373301d27f6a47b34e89b3391b1ef38b7471d381812537ef2457e620ae7f819d2642ce9c43b189b3583813ec395e2938319abe356a9b2f51
+ languageName: node
+ linkType: hard
+
"expect-type@npm:^1.3.0":
version: 1.3.0
resolution: "expect-type@npm:1.3.0"
@@ -2524,6 +3099,13 @@ __metadata:
languageName: node
linkType: hard
+"fs-constants@npm:^1.0.0":
+ version: 1.0.0
+ resolution: "fs-constants@npm:1.0.0"
+ checksum: 10c0/a0cde99085f0872f4d244e83e03a46aa387b74f5a5af750896c6b05e9077fac00e9932fdf5aef84f2f16634cd473c63037d7a512576da7d5c2b9163d1909f3a8
+ languageName: node
+ linkType: hard
+
"fs-extra@npm:^7.0.1":
version: 7.0.1
resolution: "fs-extra@npm:7.0.1"
@@ -2579,6 +3161,13 @@ __metadata:
languageName: node
linkType: hard
+"github-from-package@npm:0.0.0":
+ version: 0.0.0
+ resolution: "github-from-package@npm:0.0.0"
+ checksum: 10c0/737ee3f52d0a27e26332cde85b533c21fcdc0b09fb716c3f8e522cfaa9c600d4a631dec9fcde179ec9d47cca89017b7848ed4d6ae6b6b78f936c06825b1fcc12
+ languageName: node
+ linkType: hard
+
"glob-parent@npm:^5.1.2":
version: 5.1.2
resolution: "glob-parent@npm:5.1.2"
@@ -2644,6 +3233,15 @@ __metadata:
languageName: node
linkType: hard
+"html-encoding-sniffer@npm:^6.0.0":
+ version: 6.0.0
+ resolution: "html-encoding-sniffer@npm:6.0.0"
+ dependencies:
+ "@exodus/bytes": "npm:^1.6.0"
+ checksum: 10c0/66dc3f6f5539cc3beb814fcbfae7eacf4ec38cf824d6e1425b72039b51a40f4456bd8541ba66f4f4fe09cdf885ab5cd5bae6ec6339d6895a930b2fdb83c53025
+ languageName: node
+ linkType: hard
+
"html-escaper@npm:^2.0.0":
version: 2.0.2
resolution: "html-escaper@npm:2.0.2"
@@ -2651,6 +3249,15 @@ __metadata:
languageName: node
linkType: hard
+"html-parse-stringify@npm:^3.0.1":
+ version: 3.0.1
+ resolution: "html-parse-stringify@npm:3.0.1"
+ dependencies:
+ void-elements: "npm:3.1.0"
+ checksum: 10c0/159292753d48b84d216d61121054ae5a33466b3db5b446e2ffc093ac077a411a99ce6cbe0d18e55b87cf25fa3c5a86c4d8b130b9719ec9b66623259000c72c15
+ languageName: node
+ linkType: hard
+
"human-id@npm:^4.1.1":
version: 4.2.0
resolution: "human-id@npm:4.2.0"
@@ -2660,6 +3267,18 @@ __metadata:
languageName: node
linkType: hard
+"i18next@npm:^26.3.1":
+ version: 26.3.1
+ resolution: "i18next@npm:26.3.1"
+ peerDependencies:
+ typescript: ^5 || ^6
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ checksum: 10c0/8f26cb9a09b648edde395a58c2fe890dd996cb81a969d0a05da178b9cb4ce3dee009f9c677a480bb58b5a9d8bbf762fd226111626b015e9bbd66b96132dbba39
+ languageName: node
+ linkType: hard
+
"iconv-lite@npm:^0.7.0":
version: 0.7.2
resolution: "iconv-lite@npm:0.7.2"
@@ -2669,6 +3288,13 @@ __metadata:
languageName: node
linkType: hard
+"ieee754@npm:^1.1.13":
+ version: 1.2.1
+ resolution: "ieee754@npm:1.2.1"
+ checksum: 10c0/b0782ef5e0935b9f12883a2e2aa37baa75da6e66ce6515c168697b42160807d9330de9a32ec1ed73149aea02e0d822e572bca6f1e22bdcbd2149e13b050b17bb
+ languageName: node
+ linkType: hard
+
"ignore@npm:^5.2.0":
version: 5.3.2
resolution: "ignore@npm:5.3.2"
@@ -2690,6 +3316,27 @@ __metadata:
languageName: node
linkType: hard
+"indent-string@npm:^4.0.0":
+ version: 4.0.0
+ resolution: "indent-string@npm:4.0.0"
+ checksum: 10c0/1e1904ddb0cb3d6cce7cd09e27a90184908b7a5d5c21b92e232c93579d314f0b83c246ffb035493d0504b1e9147ba2c9b21df0030f48673fba0496ecd698161f
+ languageName: node
+ linkType: hard
+
+"inherits@npm:^2.0.3, inherits@npm:^2.0.4":
+ version: 2.0.4
+ resolution: "inherits@npm:2.0.4"
+ checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2
+ languageName: node
+ linkType: hard
+
+"ini@npm:~1.3.0":
+ version: 1.3.8
+ resolution: "ini@npm:1.3.8"
+ checksum: 10c0/ec93838d2328b619532e4f1ff05df7909760b6f66d9c9e2ded11e5c1897d6f2f9980c54dd638f88654b00919ce31e827040631eab0a3969e4d1abefa0719516a
+ languageName: node
+ linkType: hard
+
"is-core-module@npm:^2.16.1":
version: 2.16.1
resolution: "is-core-module@npm:2.16.1"
@@ -2729,6 +3376,13 @@ __metadata:
languageName: node
linkType: hard
+"is-potential-custom-element-name@npm:^1.0.1":
+ version: 1.0.1
+ resolution: "is-potential-custom-element-name@npm:1.0.1"
+ checksum: 10c0/b73e2f22bc863b0939941d369486d308b43d7aef1f9439705e3582bfccaa4516406865e32c968a35f97a99396dac84e2624e67b0a16b0a15086a785e16ce7db9
+ languageName: node
+ linkType: hard
+
"is-reference@npm:1.2.1":
version: 1.2.1
resolution: "is-reference@npm:1.2.1"
@@ -2796,7 +3450,18 @@ __metadata:
languageName: node
linkType: hard
-"js-tokens@npm:^4.0.0":
+"its-fine@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "its-fine@npm:2.0.0"
+ dependencies:
+ "@types/react-reconciler": "npm:^0.28.9"
+ peerDependencies:
+ react: ^19.0.0
+ checksum: 10c0/1ff1ff3257c0c7eb115c9c26cf0506eb84162edc1a63d3136780d146f7c7833298b240d0fcb46888909773f1a7d16539e0c03f2482cff1a5a502d6436686fe21
+ languageName: node
+ linkType: hard
+
+"js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0":
version: 4.0.0
resolution: "js-tokens@npm:4.0.0"
checksum: 10c0/e248708d377aa058eacf2037b07ded847790e6de892bbad3dac0abba2e759cb9f121b00099a65195616badcb6eca8d14d975cb3e89eb1cfda644756402c8aeed
@@ -2826,6 +3491,40 @@ __metadata:
languageName: node
linkType: hard
+"jsdom@npm:^29.1.1":
+ version: 29.1.1
+ resolution: "jsdom@npm:29.1.1"
+ dependencies:
+ "@asamuzakjp/css-color": "npm:^5.1.11"
+ "@asamuzakjp/dom-selector": "npm:^7.1.1"
+ "@bramus/specificity": "npm:^2.4.2"
+ "@csstools/css-syntax-patches-for-csstree": "npm:^1.1.3"
+ "@exodus/bytes": "npm:^1.15.0"
+ css-tree: "npm:^3.2.1"
+ data-urls: "npm:^7.0.0"
+ decimal.js: "npm:^10.6.0"
+ html-encoding-sniffer: "npm:^6.0.0"
+ is-potential-custom-element-name: "npm:^1.0.1"
+ lru-cache: "npm:^11.3.5"
+ parse5: "npm:^8.0.1"
+ saxes: "npm:^6.0.0"
+ symbol-tree: "npm:^3.2.4"
+ tough-cookie: "npm:^6.0.1"
+ undici: "npm:^7.25.0"
+ w3c-xmlserializer: "npm:^5.0.0"
+ webidl-conversions: "npm:^8.0.1"
+ whatwg-mimetype: "npm:^5.0.0"
+ whatwg-url: "npm:^16.0.1"
+ xml-name-validator: "npm:^5.0.0"
+ peerDependencies:
+ canvas: ^3.0.0
+ peerDependenciesMeta:
+ canvas:
+ optional: true
+ checksum: 10c0/20e2174b09d9d06393cb48e1392b7a1cb7191d6656a6f7b3b8fbf9853b4ab0ef60b4a42c2c55f71b55ca5da50ffa75bcdc6986210963182e7993c6f9cd4f499b
+ languageName: node
+ linkType: hard
+
"jsesc@npm:^3.0.2":
version: 3.1.0
resolution: "jsesc@npm:3.1.0"
@@ -2886,6 +3585,13 @@ __metadata:
languageName: node
linkType: hard
+"konva@npm:^10.3.0":
+ version: 10.3.0
+ resolution: "konva@npm:10.3.0"
+ checksum: 10c0/f97bf5b370633b97e8860209f3248f0641cd88116036c0ec8c1c1227c9e988e500ef07482fc6677300e066312841fe1851119eafcfcdf93c0e747b958582bef0
+ languageName: node
+ linkType: hard
+
"levn@npm:^0.4.1":
version: 0.4.1
resolution: "levn@npm:0.4.1"
@@ -3041,6 +3747,17 @@ __metadata:
languageName: node
linkType: hard
+"loose-envify@npm:^1.0.0, loose-envify@npm:^1.4.0":
+ version: 1.4.0
+ resolution: "loose-envify@npm:1.4.0"
+ dependencies:
+ js-tokens: "npm:^3.0.0 || ^4.0.0"
+ bin:
+ loose-envify: cli.js
+ checksum: 10c0/655d110220983c1a4b9c0c679a2e8016d4b67f6e9c7b5435ff5979ecdb20d0813f4dec0a08674fcbdd4846a3f07edbb50a36811fd37930b94aaa0d9daceb017e
+ languageName: node
+ linkType: hard
+
"lower-case@npm:^2.0.2":
version: 2.0.2
resolution: "lower-case@npm:2.0.2"
@@ -3050,6 +3767,13 @@ __metadata:
languageName: node
linkType: hard
+"lru-cache@npm:^11.3.5":
+ version: 11.5.1
+ resolution: "lru-cache@npm:11.5.1"
+ checksum: 10c0/7b341cea79a8efe9c6a6f20c8757a77eca5b25d7ff983ccf4e11e547b81f6787824baa1c84705251dff84ab4ffac85717ac354b9d02e465f86a9f8b166409979
+ languageName: node
+ linkType: hard
+
"lru-cache@npm:^5.1.1":
version: 5.1.1
resolution: "lru-cache@npm:5.1.1"
@@ -3059,6 +3783,15 @@ __metadata:
languageName: node
linkType: hard
+"lz-string@npm:^1.5.0":
+ version: 1.5.0
+ resolution: "lz-string@npm:1.5.0"
+ bin:
+ lz-string: bin/bin.js
+ checksum: 10c0/36128e4de34791838abe979b19927c26e67201ca5acf00880377af7d765b38d1c60847e01c5ec61b1a260c48029084ab3893a3925fd6e48a04011364b089991b
+ languageName: node
+ linkType: hard
+
"magic-string@npm:^0.30.21, magic-string@npm:^0.30.3":
version: 0.30.21
resolution: "magic-string@npm:0.30.21"
@@ -3088,6 +3821,13 @@ __metadata:
languageName: node
linkType: hard
+"mdn-data@npm:2.27.1":
+ version: 2.27.1
+ resolution: "mdn-data@npm:2.27.1"
+ checksum: 10c0/eb8abf5d22e4d1e090346f5e81b67d23cef14c83940e445da5c44541ad874dc8fb9f6ca236e8258c3a489d9fb5884188a4d7d58773adb9089ac2c0b966796393
+ languageName: node
+ linkType: hard
+
"merge2@npm:^1.3.0, merge2@npm:^1.4.1":
version: 1.4.1
resolution: "merge2@npm:1.4.1"
@@ -3105,6 +3845,20 @@ __metadata:
languageName: node
linkType: hard
+"mimic-response@npm:^3.1.0":
+ version: 3.1.0
+ resolution: "mimic-response@npm:3.1.0"
+ checksum: 10c0/0d6f07ce6e03e9e4445bee655202153bdb8a98d67ee8dc965ac140900d7a2688343e6b4c9a72cfc9ef2f7944dfd76eef4ab2482eb7b293a68b84916bac735362
+ languageName: node
+ linkType: hard
+
+"min-indent@npm:^1.0.0":
+ version: 1.0.1
+ resolution: "min-indent@npm:1.0.1"
+ checksum: 10c0/7e207bd5c20401b292de291f02913230cb1163abca162044f7db1d951fa245b174dc00869d40dd9a9f32a885ad6a5f3e767ee104cf278f399cb4e92d3f582d5c
+ languageName: node
+ linkType: hard
+
"minimatch@npm:^10.2.2, minimatch@npm:^10.2.4":
version: 10.2.5
resolution: "minimatch@npm:10.2.5"
@@ -3114,6 +3868,13 @@ __metadata:
languageName: node
linkType: hard
+"minimist@npm:^1.2.0, minimist@npm:^1.2.3":
+ version: 1.2.8
+ resolution: "minimist@npm:1.2.8"
+ checksum: 10c0/19d3fcdca050087b84c2029841a093691a91259a47def2f18222f41e7645a0b7c44ef4b40e88a1e58a40c84d2ef0ee6047c55594d298146d0eb3f6b737c20ce6
+ languageName: node
+ linkType: hard
+
"minipass@npm:^7.0.4, minipass@npm:^7.1.2":
version: 7.1.3
resolution: "minipass@npm:7.1.3"
@@ -3130,6 +3891,13 @@ __metadata:
languageName: node
linkType: hard
+"mkdirp-classic@npm:^0.5.2, mkdirp-classic@npm:^0.5.3":
+ version: 0.5.3
+ resolution: "mkdirp-classic@npm:0.5.3"
+ checksum: 10c0/95371d831d196960ddc3833cc6907e6b8f67ac5501a6582f47dfae5eb0f092e9f8ce88e0d83afcae95d6e2b61a01741ba03714eeafb6f7a6e9dcc158ac85b168
+ languageName: node
+ linkType: hard
+
"mri@npm:^1.2.0":
version: 1.2.0
resolution: "mri@npm:1.2.0"
@@ -3153,6 +3921,13 @@ __metadata:
languageName: node
linkType: hard
+"napi-build-utils@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "napi-build-utils@npm:2.0.0"
+ checksum: 10c0/5833aaeb5cc5c173da47a102efa4680a95842c13e0d9cc70428bd3ee8d96bb2172f8860d2811799b5daa5cbeda779933601492a2028a6a5351c6d0fcf6de83db
+ languageName: node
+ linkType: hard
+
"natural-compare@npm:^1.4.0":
version: 1.4.0
resolution: "natural-compare@npm:1.4.0"
@@ -3170,6 +3945,24 @@ __metadata:
languageName: node
linkType: hard
+"node-abi@npm:^3.3.0":
+ version: 3.92.0
+ resolution: "node-abi@npm:3.92.0"
+ dependencies:
+ semver: "npm:^7.3.5"
+ checksum: 10c0/d5fe063701542e1beef9017251b64dd648db200ae8d76745ddd07d475504734066ee69966609322ed4842a3b2f20cd3fbb0e1d2e675e3c25ff925d1e20fd06be
+ languageName: node
+ linkType: hard
+
+"node-addon-api@npm:^7.0.0":
+ version: 7.1.1
+ resolution: "node-addon-api@npm:7.1.1"
+ dependencies:
+ node-gyp: "npm:latest"
+ checksum: 10c0/fb32a206276d608037fa1bcd7e9921e177fe992fc610d098aa3128baca3c0050fc1e014fa007e9b3874cf865ddb4f5bd9f43ccb7cbbbe4efaff6a83e920b17e9
+ languageName: node
+ linkType: hard
+
"node-gyp@npm:latest":
version: 12.3.0
resolution: "node-gyp@npm:12.3.0"
@@ -3208,6 +4001,20 @@ __metadata:
languageName: node
linkType: hard
+"normalize.css@npm:^8.0.1":
+ version: 8.0.1
+ resolution: "normalize.css@npm:8.0.1"
+ checksum: 10c0/4ddf56d1af5ca755fa5e692e718316d8758ecb792aa96e1ad206824b5810a043763d681d6f7697d46573515f5e9690038b4c91a95c1997567128815545fb8cd7
+ languageName: node
+ linkType: hard
+
+"object-assign@npm:^4.1.1":
+ version: 4.1.1
+ resolution: "object-assign@npm:4.1.1"
+ checksum: 10c0/1f4df9945120325d041ccf7b86f31e8bcc14e73d29171e37a7903050e96b81323784ec59f93f102ec635bcf6fa8034ba3ea0a8c7e69fa202b87ae3b6cec5a414
+ languageName: node
+ linkType: hard
+
"obug@npm:^2.1.1":
version: 2.1.3
resolution: "obug@npm:2.1.3"
@@ -3215,6 +4022,15 @@ __metadata:
languageName: node
linkType: hard
+"once@npm:^1.3.1, once@npm:^1.4.0":
+ version: 1.4.0
+ resolution: "once@npm:1.4.0"
+ dependencies:
+ wrappy: "npm:1"
+ checksum: 10c0/5d48aca287dfefabd756621c5dfce5c91a549a93e9fdb7b8246bc4c4790aa2ec17b34a260530474635147aeb631a2dcc8b32c613df0675f96041cbb8244517d0
+ languageName: node
+ linkType: hard
+
"optionator@npm:^0.9.3":
version: 0.9.4
resolution: "optionator@npm:0.9.4"
@@ -3314,6 +4130,15 @@ __metadata:
languageName: node
linkType: hard
+"parse5@npm:^8.0.1":
+ version: 8.0.1
+ resolution: "parse5@npm:8.0.1"
+ dependencies:
+ entities: "npm:^8.0.0"
+ checksum: 10c0/c3c1c5aab55f6e4be5245599790e56e64be7764a4a0edd7f98db4fe3bb380f63add752fa047dff0496446c25f4104f0c7c1967723de640bde92306a7bb67ed2f
+ languageName: node
+ linkType: hard
+
"pascal-case@npm:^3.1.2":
version: 3.1.2
resolution: "pascal-case@npm:3.1.2"
@@ -3369,7 +4194,7 @@ __metadata:
languageName: node
linkType: hard
-"picocolors@npm:^1.1.0, picocolors@npm:^1.1.1":
+"picocolors@npm:1.1.1, picocolors@npm:^1.1.0, picocolors@npm:^1.1.1":
version: 1.1.1
resolution: "picocolors@npm:1.1.1"
checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58
@@ -3404,11 +4229,14 @@ __metadata:
"@changesets/cli": "npm:^2.31.0"
"@sourceacademy/conductor": "npm:^0.3.0"
"@sourceacademy/plugin-directory": "source-academy/plugin-directory#main"
+ "@testing-library/jest-dom": "npm:^6.9.1"
"@types/node": "npm:^25.9.1"
"@vitest/coverage-istanbul": "npm:^4.1.9"
+ canvas: "npm:^3.2.3"
commander: "npm:^15.0.0"
eslint: "npm:^10.0.3"
eslint-config-prettier: "npm:^10.1.8"
+ jsdom: "npm:^29.1.1"
prettier: "npm:^3.8.1"
tsx: "npm:^4.22.3"
typescript: "npm:^6.0.3"
@@ -3428,6 +4256,28 @@ __metadata:
languageName: node
linkType: hard
+"prebuild-install@npm:^7.1.3":
+ version: 7.1.3
+ resolution: "prebuild-install@npm:7.1.3"
+ dependencies:
+ detect-libc: "npm:^2.0.0"
+ expand-template: "npm:^2.0.3"
+ github-from-package: "npm:0.0.0"
+ minimist: "npm:^1.2.3"
+ mkdirp-classic: "npm:^0.5.3"
+ napi-build-utils: "npm:^2.0.0"
+ node-abi: "npm:^3.3.0"
+ pump: "npm:^3.0.0"
+ rc: "npm:^1.2.7"
+ simple-get: "npm:^4.0.0"
+ tar-fs: "npm:^2.0.0"
+ tunnel-agent: "npm:^0.6.0"
+ bin:
+ prebuild-install: bin.js
+ checksum: 10c0/25919a42b52734606a4036ab492d37cfe8b601273d8dfb1fa3c84e141a0a475e7bad3ab848c741d2f810cef892fcf6059b8c7fe5b29f98d30e0c29ad009bedff
+ languageName: node
+ linkType: hard
+
"prelude-ls@npm:^1.2.1":
version: 1.2.1
resolution: "prelude-ls@npm:1.2.1"
@@ -3453,6 +4303,17 @@ __metadata:
languageName: node
linkType: hard
+"pretty-format@npm:^27.0.2":
+ version: 27.5.1
+ resolution: "pretty-format@npm:27.5.1"
+ dependencies:
+ ansi-regex: "npm:^5.0.1"
+ ansi-styles: "npm:^5.0.0"
+ react-is: "npm:^17.0.1"
+ checksum: 10c0/0cbda1031aa30c659e10921fa94e0dd3f903ecbbbe7184a729ad66f2b6e7f17891e8c7d7654c458fa4ccb1a411ffb695b4f17bbcd3fe075fabe181027c4040ed
+ languageName: node
+ linkType: hard
+
"proc-log@npm:^6.0.0":
version: 6.1.0
resolution: "proc-log@npm:6.1.0"
@@ -3460,7 +4321,28 @@ __metadata:
languageName: node
linkType: hard
-"punycode@npm:^2.1.0":
+"prop-types@npm:^15.6.2":
+ version: 15.8.1
+ resolution: "prop-types@npm:15.8.1"
+ dependencies:
+ loose-envify: "npm:^1.4.0"
+ object-assign: "npm:^4.1.1"
+ react-is: "npm:^16.13.1"
+ checksum: 10c0/59ece7ca2fb9838031d73a48d4becb9a7cc1ed10e610517c7d8f19a1e02fa47f7c27d557d8a5702bec3cfeccddc853579832b43f449e54635803f277b1c78077
+ languageName: node
+ linkType: hard
+
+"pump@npm:^3.0.0":
+ version: 3.0.4
+ resolution: "pump@npm:3.0.4"
+ dependencies:
+ end-of-stream: "npm:^1.1.0"
+ once: "npm:^1.3.1"
+ checksum: 10c0/2780e66b5471c19e3e3e1063b84f3f6a3a08367f24c5ed552f98cd5901e6ada27c7ad6495d4244f553fd03b01884a4561933064f053f47c8994d84fd352768ea
+ languageName: node
+ linkType: hard
+
+"punycode@npm:^2.1.0, punycode@npm:^2.3.1":
version: 2.3.1
resolution: "punycode@npm:2.3.1"
checksum: 10c0/14f76a8206bc3464f794fb2e3d3cc665ae416c01893ad7a02b23766eb07159144ee612ad67af5e84fa4479ccfe67678c4feb126b0485651b302babf66f04f9e9
@@ -3481,6 +4363,137 @@ __metadata:
languageName: node
linkType: hard
+"rc@npm:^1.2.7":
+ version: 1.2.8
+ resolution: "rc@npm:1.2.8"
+ dependencies:
+ deep-extend: "npm:^0.6.0"
+ ini: "npm:~1.3.0"
+ minimist: "npm:^1.2.0"
+ strip-json-comments: "npm:~2.0.1"
+ bin:
+ rc: ./cli.js
+ checksum: 10c0/24a07653150f0d9ac7168e52943cc3cb4b7a22c0e43c7dff3219977c2fdca5a2760a304a029c20811a0e79d351f57d46c9bde216193a0f73978496afc2b85b15
+ languageName: node
+ linkType: hard
+
+"react-dom@npm:*, react-dom@npm:^19.2.7":
+ version: 19.2.7
+ resolution: "react-dom@npm:19.2.7"
+ dependencies:
+ scheduler: "npm:^0.27.0"
+ peerDependencies:
+ react: ^19.2.7
+ checksum: 10c0/970ff600f6e80d47d39e2f226f12f226173b3cba3382efc97c5f0cd663de9af38c7a4c11c213fb936094faeac83060d660247accaa96b752180d5b951b9cfecb
+ languageName: node
+ linkType: hard
+
+"react-fast-compare@npm:^3.0.1":
+ version: 3.2.2
+ resolution: "react-fast-compare@npm:3.2.2"
+ checksum: 10c0/0bbd2f3eb41ab2ff7380daaa55105db698d965c396df73e6874831dbafec8c4b5b08ba36ff09df01526caa3c61595247e3269558c284e37646241cba2b90a367
+ languageName: node
+ linkType: hard
+
+"react-i18next@npm:^17.0.8":
+ version: 17.0.8
+ resolution: "react-i18next@npm:17.0.8"
+ dependencies:
+ "@babel/runtime": "npm:^7.29.2"
+ html-parse-stringify: "npm:^3.0.1"
+ use-sync-external-store: "npm:^1.6.0"
+ peerDependencies:
+ i18next: ">= 26.2.0"
+ react: ">= 16.8.0"
+ typescript: ^5 || ^6
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+ react-native:
+ optional: true
+ typescript:
+ optional: true
+ checksum: 10c0/4d13d396208927ed3c80eb62db011a98dae16d93fc951bb4cb220fc5a3a5dd2f0046244fdbd197f627f595230d6fa05cc5d1c6f52b6241acd63ff818dff63acb
+ languageName: node
+ linkType: hard
+
+"react-is@npm:^16.13.1":
+ version: 16.13.1
+ resolution: "react-is@npm:16.13.1"
+ checksum: 10c0/33977da7a5f1a287936a0c85639fec6ca74f4f15ef1e59a6bc20338fc73dc69555381e211f7a3529b8150a1f71e4225525b41b60b52965bda53ce7d47377ada1
+ languageName: node
+ linkType: hard
+
+"react-is@npm:^17.0.1":
+ version: 17.0.2
+ resolution: "react-is@npm:17.0.2"
+ checksum: 10c0/2bdb6b93fbb1820b024b496042cce405c57e2f85e777c9aabd55f9b26d145408f9f74f5934676ffdc46f3dcff656d78413a6e43968e7b3f92eea35b3052e9053
+ languageName: node
+ linkType: hard
+
+"react-konva@npm:^19.0.7":
+ version: 19.2.5
+ resolution: "react-konva@npm:19.2.5"
+ dependencies:
+ "@types/react-reconciler": "npm:^0.33.0"
+ its-fine: "npm:^2.0.0"
+ react-reconciler: "npm:0.33.0"
+ scheduler: "npm:0.27.0"
+ peerDependencies:
+ konva: ^8.0.1 || ^7.2.5 || ^9.0.0 || ^10.0.0
+ react: ^19.2.0
+ react-dom: ^19.2.0
+ checksum: 10c0/6f222c47cc4c105a3f4b662a9a86a31983c77bf254aebac9ca21c4e0bf7d3f78a6c2b0c6c2cf23d44fbded1260c69e2a90766ef4eeec85d19457f8aebd978ad1
+ languageName: node
+ linkType: hard
+
+"react-popper@npm:^2.3.0":
+ version: 2.3.0
+ resolution: "react-popper@npm:2.3.0"
+ dependencies:
+ react-fast-compare: "npm:^3.0.1"
+ warning: "npm:^4.0.2"
+ peerDependencies:
+ "@popperjs/core": ^2.0.0
+ react: ^16.8.0 || ^17 || ^18
+ react-dom: ^16.8.0 || ^17 || ^18
+ checksum: 10c0/23f93540537ca4c035425bb8d5e51b11131fbc921d7ac1d041d0ae557feac8c877f3a012d36b94df8787803f52ed81e6df9257ac9e58719875f7805518d6db3f
+ languageName: node
+ linkType: hard
+
+"react-reconciler@npm:0.33.0":
+ version: 0.33.0
+ resolution: "react-reconciler@npm:0.33.0"
+ dependencies:
+ scheduler: "npm:^0.27.0"
+ peerDependencies:
+ react: ^19.2.0
+ checksum: 10c0/3f7b27ea8d0ff4c8bf0e402a285e1af9b7d0e6f4c1a70a28f4384938bc1130bc82a90a31df0b79ef5e380e2e55e2598bd90b4dbf802b1203d735ba0355817d3a
+ languageName: node
+ linkType: hard
+
+"react-transition-group@npm:^4.4.5":
+ version: 4.4.5
+ resolution: "react-transition-group@npm:4.4.5"
+ dependencies:
+ "@babel/runtime": "npm:^7.5.5"
+ dom-helpers: "npm:^5.0.1"
+ loose-envify: "npm:^1.4.0"
+ prop-types: "npm:^15.6.2"
+ peerDependencies:
+ react: ">=16.6.0"
+ react-dom: ">=16.6.0"
+ checksum: 10c0/2ba754ba748faefa15f87c96dfa700d5525054a0141de8c75763aae6734af0740e77e11261a1e8f4ffc08fd9ab78510122e05c21c2d79066c38bb6861a886c82
+ languageName: node
+ linkType: hard
+
+"react@npm:^19.2.4":
+ version: 19.2.7
+ resolution: "react@npm:19.2.7"
+ checksum: 10c0/0bd0e2f1bbd4ba97561c6597bf8a5fec05e6476fe61e165c1065598d16668efc6715205599c94d3ddd49d36cb0f21cbf1b9bcc18ee840b805ce222c3e8d558ac
+ languageName: node
+ linkType: hard
+
"read-yaml-file@npm:^1.1.0":
version: 1.1.0
resolution: "read-yaml-file@npm:1.1.0"
@@ -3493,6 +4506,34 @@ __metadata:
languageName: node
linkType: hard
+"readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0":
+ version: 3.6.2
+ resolution: "readable-stream@npm:3.6.2"
+ dependencies:
+ inherits: "npm:^2.0.3"
+ string_decoder: "npm:^1.1.1"
+ util-deprecate: "npm:^1.0.1"
+ checksum: 10c0/e37be5c79c376fdd088a45fa31ea2e423e5d48854be7a22a58869b4e84d25047b193f6acb54f1012331e1bcd667ffb569c01b99d36b0bd59658fb33f513511b7
+ languageName: node
+ linkType: hard
+
+"redent@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "redent@npm:3.0.0"
+ dependencies:
+ indent-string: "npm:^4.0.0"
+ strip-indent: "npm:^3.0.0"
+ checksum: 10c0/d64a6b5c0b50eb3ddce3ab770f866658a2b9998c678f797919ceb1b586bab9259b311407280bd80b804e2a7c7539b19238ae6a2a20c843f1a7fcff21d48c2eae
+ languageName: node
+ linkType: hard
+
+"require-from-string@npm:^2.0.2":
+ version: 2.0.2
+ resolution: "require-from-string@npm:2.0.2"
+ checksum: 10c0/aaa267e0c5b022fc5fd4eef49d8285086b15f2a1c54b28240fdf03599cbd9c26049fee3eab894f2e1f6ca65e513b030a7c264201e3f005601e80c49fb2937ce2
+ languageName: node
+ linkType: hard
+
"resolve-from@npm:^5.0.0":
version: 5.0.0
resolution: "resolve-from@npm:5.0.0"
@@ -3692,6 +4733,13 @@ __metadata:
languageName: node
linkType: hard
+"safe-buffer@npm:^5.0.1, safe-buffer@npm:~5.2.0":
+ version: 5.2.1
+ resolution: "safe-buffer@npm:5.2.1"
+ checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3
+ languageName: node
+ linkType: hard
+
"safer-buffer@npm:>= 2.1.2 < 3.0.0":
version: 2.1.2
resolution: "safer-buffer@npm:2.1.2"
@@ -3699,6 +4747,22 @@ __metadata:
languageName: node
linkType: hard
+"saxes@npm:^6.0.0":
+ version: 6.0.0
+ resolution: "saxes@npm:6.0.0"
+ dependencies:
+ xmlchars: "npm:^2.2.0"
+ checksum: 10c0/3847b839f060ef3476eb8623d099aa502ad658f5c40fd60c105ebce86d244389b0d76fcae30f4d0c728d7705ceb2f7e9b34bb54717b6a7dbedaf5dad2d9a4b74
+ languageName: node
+ linkType: hard
+
+"scheduler@npm:0.27.0, scheduler@npm:^0.27.0":
+ version: 0.27.0
+ resolution: "scheduler@npm:0.27.0"
+ checksum: 10c0/4f03048cb05a3c8fddc45813052251eca00688f413a3cee236d984a161da28db28ba71bd11e7a3dd02f7af84ab28d39fb311431d3b3772fed557945beb00c452
+ languageName: node
+ linkType: hard
+
"semver@npm:^6.3.1":
version: 6.3.1
resolution: "semver@npm:6.3.1"
@@ -3765,6 +4829,24 @@ __metadata:
languageName: node
linkType: hard
+"simple-concat@npm:^1.0.0":
+ version: 1.0.1
+ resolution: "simple-concat@npm:1.0.1"
+ checksum: 10c0/62f7508e674414008910b5397c1811941d457dfa0db4fd5aa7fa0409eb02c3609608dfcd7508cace75b3a0bf67a2a77990711e32cd213d2c76f4fd12ee86d776
+ languageName: node
+ linkType: hard
+
+"simple-get@npm:^4.0.0":
+ version: 4.0.1
+ resolution: "simple-get@npm:4.0.1"
+ dependencies:
+ decompress-response: "npm:^6.0.0"
+ once: "npm:^1.3.1"
+ simple-concat: "npm:^1.0.0"
+ checksum: 10c0/b0649a581dbca741babb960423248899203165769747142033479a7dc5e77d7b0fced0253c731cd57cf21e31e4d77c9157c3069f4448d558ebc96cf9e1eebcf0
+ languageName: node
+ linkType: hard
+
"slash@npm:^3.0.0":
version: 3.0.0
resolution: "slash@npm:3.0.0"
@@ -3844,6 +4926,15 @@ __metadata:
languageName: node
linkType: hard
+"string_decoder@npm:^1.1.1":
+ version: 1.3.0
+ resolution: "string_decoder@npm:1.3.0"
+ dependencies:
+ safe-buffer: "npm:~5.2.0"
+ checksum: 10c0/810614ddb030e271cd591935dcd5956b2410dd079d64ff92a1844d6b7588bf992b3e1b69b0f4d34a3e06e0bd73046ac646b5264c1987b20d0601f81ef35d731d
+ languageName: node
+ linkType: hard
+
"strip-ansi@npm:^6.0.1":
version: 6.0.1
resolution: "strip-ansi@npm:6.0.1"
@@ -3860,6 +4951,22 @@ __metadata:
languageName: node
linkType: hard
+"strip-indent@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "strip-indent@npm:3.0.0"
+ dependencies:
+ min-indent: "npm:^1.0.0"
+ checksum: 10c0/ae0deaf41c8d1001c5d4fbe16cb553865c1863da4fae036683b474fa926af9fc121e155cb3fc57a68262b2ae7d5b8420aa752c97a6428c315d00efe2a3875679
+ languageName: node
+ linkType: hard
+
+"strip-json-comments@npm:~2.0.1":
+ version: 2.0.1
+ resolution: "strip-json-comments@npm:2.0.1"
+ checksum: 10c0/b509231cbdee45064ff4f9fd73609e2bcc4e84a4d508e9dd0f31f70356473fde18abfb5838c17d56fb236f5a06b102ef115438de0600b749e818a35fbbc48c43
+ languageName: node
+ linkType: hard
+
"supports-color@npm:^7.1.0":
version: 7.2.0
resolution: "supports-color@npm:7.2.0"
@@ -3876,6 +4983,45 @@ __metadata:
languageName: node
linkType: hard
+"symbol-tree@npm:^3.2.4":
+ version: 3.2.4
+ resolution: "symbol-tree@npm:3.2.4"
+ checksum: 10c0/dfbe201ae09ac6053d163578778c53aa860a784147ecf95705de0cd23f42c851e1be7889241495e95c37cabb058edb1052f141387bef68f705afc8f9dd358509
+ languageName: node
+ linkType: hard
+
+"tabbable@npm:^6.0.0":
+ version: 6.4.0
+ resolution: "tabbable@npm:6.4.0"
+ checksum: 10c0/d931427f4a96b801fd8801ba296a702119e06f70ad262fed8abc5271225c9f1ca51b89fdec4fb2f22e1d35acb3d2881db0a17cedc758272e9ecb540d00299d76
+ languageName: node
+ linkType: hard
+
+"tar-fs@npm:^2.0.0":
+ version: 2.1.4
+ resolution: "tar-fs@npm:2.1.4"
+ dependencies:
+ chownr: "npm:^1.1.1"
+ mkdirp-classic: "npm:^0.5.2"
+ pump: "npm:^3.0.0"
+ tar-stream: "npm:^2.1.4"
+ checksum: 10c0/decb25acdc6839182c06ec83cba6136205bda1db984e120c8ffd0d80182bc5baa1d916f9b6c5c663ea3f9975b4dd49e3c6bb7b1707cbcdaba4e76042f43ec84c
+ languageName: node
+ linkType: hard
+
+"tar-stream@npm:^2.1.4":
+ version: 2.2.0
+ resolution: "tar-stream@npm:2.2.0"
+ dependencies:
+ bl: "npm:^4.0.3"
+ end-of-stream: "npm:^1.4.1"
+ fs-constants: "npm:^1.0.0"
+ inherits: "npm:^2.0.3"
+ readable-stream: "npm:^3.1.1"
+ checksum: 10c0/2f4c910b3ee7196502e1ff015a7ba321ec6ea837667220d7bcb8d0852d51cb04b87f7ae471008a6fb8f5b1a1b5078f62f3a82d30c706f20ada1238ac797e7692
+ languageName: node
+ linkType: hard
+
"tar@npm:^7.5.4":
version: 7.5.13
resolution: "tar@npm:7.5.13"
@@ -3941,6 +5087,24 @@ __metadata:
languageName: node
linkType: hard
+"tldts-core@npm:^7.4.3":
+ version: 7.4.3
+ resolution: "tldts-core@npm:7.4.3"
+ checksum: 10c0/866f9d46ef7ba80a560edaa0a659c32e0aa3b4e281694c96bcf7773f6530e107c5681c714f47d58ee1720dc5578bb168a1e8535c514de90b5907850dc1202cd8
+ languageName: node
+ linkType: hard
+
+"tldts@npm:^7.0.5":
+ version: 7.4.3
+ resolution: "tldts@npm:7.4.3"
+ dependencies:
+ tldts-core: "npm:^7.4.3"
+ bin:
+ tldts: bin/cli.js
+ checksum: 10c0/334c8d0d50fb0ac69453947460a6e51396f5c35bef6c70300b201832d86801ce54e6a26d03c1745cf801aa409780086e350a098c0a0afdf005c06de14e5e94c1
+ languageName: node
+ linkType: hard
+
"to-regex-range@npm:^5.0.1":
version: 5.0.1
resolution: "to-regex-range@npm:5.0.1"
@@ -3950,6 +5114,24 @@ __metadata:
languageName: node
linkType: hard
+"tough-cookie@npm:^6.0.1":
+ version: 6.0.1
+ resolution: "tough-cookie@npm:6.0.1"
+ dependencies:
+ tldts: "npm:^7.0.5"
+ checksum: 10c0/ec70bd6b1215efe4ed31a158f0be3e4c9088fcbd8620edc23a5860d4f3d85c757b77e274baaa700f7b25e409f4181552ed189603c2b2e1a9f88104da3a61a37d
+ languageName: node
+ linkType: hard
+
+"tr46@npm:^6.0.0":
+ version: 6.0.0
+ resolution: "tr46@npm:6.0.0"
+ dependencies:
+ punycode: "npm:^2.3.1"
+ checksum: 10c0/83130df2f649228aa91c17754b66248030a3af34911d713b5ea417066fa338aa4bc8668d06bd98aa21a2210f43fc0a3db8b9099e7747fb5830e40e39a6a1058e
+ languageName: node
+ linkType: hard
+
"ts-api-utils@npm:^2.5.0":
version: 2.5.0
resolution: "ts-api-utils@npm:2.5.0"
@@ -3988,6 +5170,15 @@ __metadata:
languageName: node
linkType: hard
+"tunnel-agent@npm:^0.6.0":
+ version: 0.6.0
+ resolution: "tunnel-agent@npm:0.6.0"
+ dependencies:
+ safe-buffer: "npm:^5.0.1"
+ checksum: 10c0/4c7a1b813e7beae66fdbf567a65ec6d46313643753d0beefb3c7973d66fcec3a1e7f39759f0a0b4465883499c6dc8b0750ab8b287399af2e583823e40410a17a
+ languageName: node
+ linkType: hard
+
"type-check@npm:^0.4.0, type-check@npm:~0.4.0":
version: 0.4.0
resolution: "type-check@npm:0.4.0"
@@ -4046,6 +5237,13 @@ __metadata:
languageName: node
linkType: hard
+"undici@npm:^7.25.0":
+ version: 7.28.0
+ resolution: "undici@npm:7.28.0"
+ checksum: 10c0/fe781983a26098795e99bb1f64906cbb7d0bcaa029a26baade007b53ea67f2631d189b8f9671a31f4c8d0cb3773b7559608628ba54452fef51fec90e7c78bb0d
+ languageName: node
+ linkType: hard
+
"universalify@npm:^0.1.0":
version: 0.1.2
resolution: "universalify@npm:0.1.2"
@@ -4094,6 +5292,22 @@ __metadata:
languageName: node
linkType: hard
+"use-sync-external-store@npm:^1.6.0":
+ version: 1.6.0
+ resolution: "use-sync-external-store@npm:1.6.0"
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ checksum: 10c0/35e1179f872a53227bdf8a827f7911da4c37c0f4091c29b76b1e32473d1670ebe7bcd880b808b7549ba9a5605c233350f800ffab963ee4a4ee346ee983b6019b
+ languageName: node
+ linkType: hard
+
+"util-deprecate@npm:^1.0.1":
+ version: 1.0.2
+ resolution: "util-deprecate@npm:1.0.2"
+ checksum: 10c0/41a5bdd214df2f6c3ecf8622745e4a366c4adced864bc3c833739791aeeeb1838119af7daed4ba36428114b5c67dcda034a79c882e97e43c03e66a4dd7389942
+ languageName: node
+ linkType: hard
+
"vite@npm:^6.0.0 || ^7.0.0 || ^8.0.0":
version: 8.0.16
resolution: "vite@npm:8.0.16"
@@ -4219,6 +5433,56 @@ __metadata:
languageName: node
linkType: hard
+"void-elements@npm:3.1.0":
+ version: 3.1.0
+ resolution: "void-elements@npm:3.1.0"
+ checksum: 10c0/0b8686f9f9aa44012e9bd5eabf287ae0cde409b9a2854c5a2335cb83920c957668ac5876e3f0d158dd424744ac411a7270e64128556b451ed3bec875ef18534d
+ languageName: node
+ linkType: hard
+
+"w3c-xmlserializer@npm:^5.0.0":
+ version: 5.0.0
+ resolution: "w3c-xmlserializer@npm:5.0.0"
+ dependencies:
+ xml-name-validator: "npm:^5.0.0"
+ checksum: 10c0/8712774c1aeb62dec22928bf1cdfd11426c2c9383a1a63f2bcae18db87ca574165a0fbe96b312b73652149167ac6c7f4cf5409f2eb101d9c805efe0e4bae798b
+ languageName: node
+ linkType: hard
+
+"warning@npm:^4.0.2":
+ version: 4.0.3
+ resolution: "warning@npm:4.0.3"
+ dependencies:
+ loose-envify: "npm:^1.0.0"
+ checksum: 10c0/aebab445129f3e104c271f1637fa38e55eb25f968593e3825bd2f7a12bd58dc3738bb70dc8ec85826621d80b4acfed5a29ebc9da17397c6125864d72301b937e
+ languageName: node
+ linkType: hard
+
+"webidl-conversions@npm:^8.0.1":
+ version: 8.0.1
+ resolution: "webidl-conversions@npm:8.0.1"
+ checksum: 10c0/3f6f327ca5fa0c065ed8ed0ef3b72f33623376e68f958e9b7bd0df49fdb0b908139ac2338d19fb45bd0e05595bda96cb6d1622222a8b413daa38a17aacc4dd46
+ languageName: node
+ linkType: hard
+
+"whatwg-mimetype@npm:^5.0.0":
+ version: 5.0.0
+ resolution: "whatwg-mimetype@npm:5.0.0"
+ checksum: 10c0/eead164fe73a00dd82f817af6fc0bd22e9c273e1d55bf4bc6bdf2da7ad8127fca82ef00ea6a37892f5f5641f8e34128e09508f92126086baba126b9e0d57feb4
+ languageName: node
+ linkType: hard
+
+"whatwg-url@npm:^16.0.0, whatwg-url@npm:^16.0.1":
+ version: 16.0.1
+ resolution: "whatwg-url@npm:16.0.1"
+ dependencies:
+ "@exodus/bytes": "npm:^1.11.0"
+ tr46: "npm:^6.0.0"
+ webidl-conversions: "npm:^8.0.1"
+ checksum: 10c0/e75565566abf3a2cdbd9f06c965dbcccee6ec4e9f0d3728ad5e08ceb9944279848bcaa211d35a29cb6d2df1e467dd05cfb59fbddf8a0adcd7d0bce9ffb703fd2
+ languageName: node
+ linkType: hard
+
"which@npm:^2.0.1":
version: 2.0.2
resolution: "which@npm:2.0.2"
@@ -4260,6 +5524,27 @@ __metadata:
languageName: node
linkType: hard
+"wrappy@npm:1":
+ version: 1.0.2
+ resolution: "wrappy@npm:1.0.2"
+ checksum: 10c0/56fece1a4018c6a6c8e28fbc88c87e0fbf4ea8fd64fc6c63b18f4acc4bd13e0ad2515189786dd2c30d3eec9663d70f4ecf699330002f8ccb547e4a18231fc9f0
+ languageName: node
+ linkType: hard
+
+"xml-name-validator@npm:^5.0.0":
+ version: 5.0.0
+ resolution: "xml-name-validator@npm:5.0.0"
+ checksum: 10c0/3fcf44e7b73fb18be917fdd4ccffff3639373c7cb83f8fc35df6001fecba7942f1dbead29d91ebb8315e2f2ff786b508f0c9dc0215b6353f9983c6b7d62cb1f5
+ languageName: node
+ linkType: hard
+
+"xmlchars@npm:^2.2.0":
+ version: 2.2.0
+ resolution: "xmlchars@npm:2.2.0"
+ checksum: 10c0/b64b535861a6f310c5d9bfa10834cf49127c71922c297da9d4d1b45eeaae40bf9b4363275876088fbe2667e5db028d2cd4f8ee72eed9bede840a67d57dab7593
+ languageName: node
+ linkType: hard
+
"yallist@npm:^3.0.2":
version: 3.1.1
resolution: "yallist@npm:3.1.1"