diff --git a/.dockerignore b/.dockerignore index 5a3910f8..83d09871 100644 --- a/.dockerignore +++ b/.dockerignore @@ -13,5 +13,7 @@ dependencies/emsdk/.emscripten.old dependencies/cpython/.git dependencies/cpython/builddir dependencies/libwallaby/.git +dependencies/kipr-scratch/libwallaby-build +dependencies/kipr-scratch/kipr-scratch **/.github \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f639091..b76a349e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,12 @@ jobs: uses: actions/checkout@v2 with: path: simulator + submodules: recursive + - name: Install Simulator system dependencies + run: sudo apt-get update && sudo apt-get install -y wget git cmake build-essential swig zlib1g-dev doxygen default-jre python2.7 + - name: Install Simulator dependencies + run: yarn run build-deps + working-directory: simulator - name: Install Simulator modules # Specifying an alternative cache folder to work around a race condition issue in yarn, # which seems to occur b/c of how ivygate is referenced from GitHub. diff --git a/.gitignore b/.gitignore index 8e5beb10..97caaed6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules yarn-error.log dist +.yarncache # Prerequisites *.d diff --git a/.gitmodules b/.gitmodules index 95fbccbe..c9d2f286 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,4 +7,6 @@ [submodule "dependencies/emsdk"] path = dependencies/emsdk url = https://github.com/emscripten-core/emsdk - +[submodule "dependencies/kipr-scratch"] + path = dependencies/kipr-scratch + url = https://github.com/kipr/kipr-scratch diff --git a/Dockerfile b/Dockerfile index d0e72016..310f32b8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ RUN rm /bin/sh && ln -s /bin/bash /bin/sh ENV TZ=America/Los_Angeles RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone -RUN apt-get update && apt-get install -y wget git cmake build-essential python3.8 swig zlib1g-dev doxygen +RUN apt-get update && apt-get install -y wget git cmake build-essential python3.8 swig zlib1g-dev doxygen default-jre python2.7 RUN wget https://deb.nodesource.com/setup_18.x && chmod +x ./setup_18.x && ./setup_18.x RUN apt-get install -y nodejs @@ -13,11 +13,12 @@ RUN npm install -g yarn ADD . /app +EXPOSE 3000 WORKDIR /app/ -RUN python3.8 dependencies/build.py +RUN yarn run build-deps +# RUN python3.8 dependencies/build.py -WORKDIR /app -EXPOSE 3000 +# WORKDIR /app # WORKDIR /app/simulator # RUN yarn install --cache-folder ./.yarncache && yarn build; true RUN yarn install --cache-folder ./.yarncache diff --git a/README.md b/README.md index c26b3c5d..b5337a28 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Simulates a botball/JBC style demobot with a built in IDE. - [CMake](https://cmake.org/) - [SWIG 4+](https://swig.org/) - [Python 3.7 or newer](https://www.python.org/) +- [Also python 2.7] ### Debian/Ubuntu ```bash @@ -59,6 +60,7 @@ Or, if you've already cloned the repository without `--recurse-submodules`, you ```bash git submodule update --init +cd dependencies/kipr-scratch/libwallaby && git submodule update --init ``` ### Optional: Pull large files @@ -71,11 +73,52 @@ git lfs pull ```bash # Python 3.7+ is required for the build process -python3 dependencies/build.py -``` +yarn run build-deps +# The build-deps command executes: +"python3 dependencies/build.py && yarn add file:dependencies/kipr-scratch/kipr-scratch --cache-folder ./.yarncache", +``` Tip: if you are experiencing issues with this step, you may try deleting the repository and follow the steps listed above again. +### Notes on building dependencies +```python3 dependencies/build.py``` + +1. Run some setup checks +2. Build libwallaby (probably only need to do this once) +3. Delete ''unnecessary blocks' from scratch-blocks/blocks-vertical (renames them with a .old extension) + - 'event.js', + - 'extensions.js', + - 'default_toolbox.js', + - 'looks.js', + - 'motion.js', + - 'sensing.js', + - 'sound.js' + + +4. blockify libwallaby + - Input is libwallaby-build, output is in scratch-blocks/blocks_vertical + - Start by parsing the xml result of the libwallaby build + - Essentially getting all the functions from the included files and cdecl nodes + - Ends up with a list of modules and their functions + - For all functions, needs to generate a blockly block of the module_function with + with the correct parameters and return types. + - Only does it for analog, digital, wait_for, time, motor, and servo modules + - Patches the colors in core/colours.js for the new modules from info in module_hsl.json + - Writes a new default_toolbox.js - this is what we have access to pick from + - Modifies the category names in the vertical extensions file, removing 'sounds', 'motion', + 'looks', 'event', 'sensing', 'pen', but adding our modules + - Modifies the messages to include those from our modules and the program start + - Modifies the css flyout style + - Modifies the field variables to remove non initialization case + + +5. Patch the scratch-blocks package.json to use python2 + +6. Install the scratch blocks + + + + ## Install JavaScript Dependencies Navigate to the root directory of this repository, then run: diff --git a/configs/webpack/common.js b/configs/webpack/common.js index 09450745..87577c8b 100644 --- a/configs/webpack/common.js +++ b/configs/webpack/common.js @@ -36,7 +36,8 @@ try { module.exports = { entry: { app: './index.tsx', - login: './components/Login/index.tsx', + login: './components/Login/index.tsx', + plugin: './lms/plugin/index.tsx', 'editor.worker': 'monaco-editor/esm/vs/editor/editor.worker.js', 'ts.worker': 'monaco-editor/esm/vs/language/typescript/ts.worker.js', }, @@ -63,10 +64,11 @@ module.exports = { path: false, }, alias: { + state: resolve(__dirname, '../../src/state'), '@i18n': resolve(__dirname, '../../src/util/i18n'), }, symlinks: false, - modules + modules //: [resolve(__dirname, '../../src'), 'node_modules'] }, context: resolve(__dirname, '../../src'), module: { @@ -135,14 +137,15 @@ module.exports = { ], }, plugins: [ - new HtmlWebpackPlugin({ template: 'index.html.ejs', excludeChunks: ['login'] }), + new HtmlWebpackPlugin({ template: 'index.html.ejs', excludeChunks: ['login', 'plugin'] }), new HtmlWebpackPlugin({ template: 'components/Login/login.html.ejs', filename: 'login.html', chunks: ['login'] }), + new HtmlWebpackPlugin({ template: 'lms/plugin/plugin.html.ejs', filename: 'plugin.html', chunks: ['plugin'] }), new DefinePlugin({ SIMULATOR_VERSION: JSON.stringify(require('../../package.json').version), SIMULATOR_GIT_HASH: JSON.stringify(commitHash), SIMULATOR_HAS_CPYTHON: JSON.stringify(dependencies.cpython !== undefined), SIMULATOR_LIBKIPR_C_DOCUMENTATION: JSON.stringify(libkiprCDocumentation), - SIMULATOR_I18N: JSON.stringify(i18n), + SIMULATOR_I18N: JSON.stringify(i18n) }), new NpmDtsPlugin({ root: resolve(__dirname, '../../'), diff --git a/dependencies/.gitignore b/dependencies/.gitignore index a68ae342..716f0534 100644 --- a/dependencies/.gitignore +++ b/dependencies/.gitignore @@ -1,5 +1,5 @@ libkipr_build_c libkipr_build_python libkipr_install_c -ammo_build +scratch-rt.js dependencies.json \ No newline at end of file diff --git a/dependencies/build.py b/dependencies/build.py index e57edce7..d300e4b3 100755 --- a/dependencies/build.py +++ b/dependencies/build.py @@ -5,6 +5,26 @@ import pathlib import json import multiprocessing +import hashlib + +def sha1OfFile(filepath): + sha = hashlib.sha1() + with open(filepath, 'rb') as f: + while True: + block = f.read(2**10) # Magic number: one-megabyte blocks. + if not block: break + sha.update(block) + return sha.hexdigest() + +def hash_dir(dir_path): + hashes = [] + for path, dirs, files in os.walk(dir_path): + for file in sorted(files): + hashes.append(sha1OfFile(os.path.join(path, file))) + for dir in sorted(dirs): + hashes.append(hash_dir(os.path.join(path, dir))) + break + return str(hash(''.join(hashes))) def is_tool(name): """Check whether `name` is on PATH and marked as executable.""" @@ -68,6 +88,7 @@ def is_tool(name): } libkipr_dir = working_dir / 'libwallaby' +libkipr_hash = hash_dir(libkipr_dir) # libkipr (C) @@ -86,6 +107,7 @@ def is_tool(name): '-Dwith_python_binding=OFF', '-Dwith_documentation=ON', '-Dwith_tests=OFF', + '-Dwith_graphics=OFF', f'-DCMAKE_INSTALL_PREFIX={libkipr_install_c_dir}', libkipr_dir ], @@ -200,12 +222,46 @@ def is_tool(name): print('Generating JSON documentation...') libkipr_c_documentation_json = f'{libkipr_build_c_dir}/documentation/json.json' subprocess.run( - [ 'python3', 'generate_doxygen_json.py', f'{libkipr_build_c_dir}/documentation/xml', libkipr_c_documentation_json ], + [ python, 'generate_doxygen_json.py', f'{libkipr_build_c_dir}/documentation/xml', libkipr_c_documentation_json ], + # [ 'python3', 'generate_doxygen_json.py', f'{libkipr_build_c_dir}/documentation/xml', libkipr_c_documentation_json ], cwd = working_dir, check = True ) +print('Building kipr-scratch...') +kipr_scratch_path = working_dir / 'kipr-scratch' +subprocess.run( + [ python, kipr_scratch_path / 'build.py' ], + cwd = kipr_scratch_path, + check = True +) +print('Packaging kipr-scratch...') +subprocess.run( + [ python, kipr_scratch_path / 'package.py' ], + cwd = kipr_scratch_path, + check = True +) + +print('Generating scratch runtime...') +scratch_runtime_path = working_dir / 'scratch-rt' +# emcc -s WASM=0 -s INVOKE_RUN=0 -s ASYNCIFY -s EXIT_RUNTIME=1 -s "EXPORTED_FUNCTIONS=['_main', '_simMainWrapper']" -I${config.server.dependencies.libkipr_c}/include -Wl,--whole-archive -L${config.server.dependencies.libkipr_c}/lib -lkipr -o ${path}.js ${path} +subprocess.run([ + 'emcc', + '-sWASM=0', + '-sINVOKE_RUN=0', + '-sEXIT_RUNTIME=0', + '-sERROR_ON_UNDEFINED_SYMBOLS=0', + '-sLINKABLE=1', + '-sEXPORT_ALL=1', + f'-L{libkipr_install_c_dir}/lib', + '-Wl,--whole-archive', '-lkipr', '-Wl,--no-whole-archive', + f'-o', f'{scratch_runtime_path}.js', + f'{scratch_runtime_path}.c' + ], + env = env, + check = True +) print('Outputting results...') output = json.dumps({ @@ -216,10 +272,13 @@ def is_tool(name): 'EMSDK': f'{emsdk_dir}', 'EM_CONFIG': f'{emsdk_dot_emscripten}' }, + 'libkipr_hash': libkipr_hash, 'libkipr_c': f'{libkipr_install_c_dir}', 'libkipr_python': f'{libkipr_build_python_dir}', 'cpython': f'{cpython_emscripten_build_dir}', + 'cpython_hash': hash_dir(cpython_dir), "libkipr_c_documentation": libkipr_c_documentation_json, + 'scratch_rt': f'{scratch_runtime_path}.js', }) with open(working_dir / 'dependencies.json', 'w') as f: diff --git a/dependencies/kipr-scratch b/dependencies/kipr-scratch new file mode 160000 index 00000000..54df1658 --- /dev/null +++ b/dependencies/kipr-scratch @@ -0,0 +1 @@ +Subproject commit 54df1658a129054e68e5349481bb3b734b629b90 diff --git a/dependencies/libwallaby b/dependencies/libwallaby index 06b2bbfd..e63f9bd5 160000 --- a/dependencies/libwallaby +++ b/dependencies/libwallaby @@ -1 +1 @@ -Subproject commit 06b2bbfd7cf965d3310904555983f96caa45dcbe +Subproject commit e63f9bd5d171a6d8d095d32b3fdb3ee966bb0844 diff --git a/dependencies/scratch-rt.c b/dependencies/scratch-rt.c new file mode 100644 index 00000000..c3aeba4b --- /dev/null +++ b/dependencies/scratch-rt.c @@ -0,0 +1 @@ +int main() { return 0; } \ No newline at end of file diff --git a/express.js b/express.js index 05621562..e0b36d56 100644 --- a/express.js +++ b/express.js @@ -11,6 +11,7 @@ const sourceDir = 'dist'; const { get: getConfig } = require('./config'); const { WebhookClient } = require('discord.js'); const proxy = require('express-http-proxy'); +const path = require('path'); let config; @@ -21,6 +22,16 @@ try { throw e; } +// set up rate limiter: maximum of 100 requests per 15 minute +var RateLimit = require('express-rate-limit'); +var limiter = RateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // max 100 requests per windowMs +}); + +// apply rate limiter to all requests +app.use(limiter); + app.use((req, res, next) => { res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); @@ -204,6 +215,22 @@ app.use('/static', express.static(`${__dirname}/static`, { maxAge: config.caching.staticMaxAge, })); + +if (config.server.dependencies.scratch_rt) { + console.log('Scratch Runtime is enabled.'); + app.use('/scratch/rt.js', express.static(`${config.server.dependencies.scratch_rt}`, { + maxAge: config.caching.staticMaxAge, + })); +} + +app.use('/scratch', express.static(path.resolve(__dirname, 'node_modules', 'kipr-scratch'), { + maxAge: config.caching.staticMaxAge, +})); + +app.use('/media', express.static(path.resolve(__dirname, 'node_modules', 'kipr-scratch', 'media'), { + maxAge: config.caching.staticMaxAge, +})); + // Expose cpython artifacts if (config.server.dependencies.cpython) { console.log('CPython artifacts are enabled.'); @@ -233,6 +260,11 @@ app.get('/login', (req, res) => { res.sendFile(`${__dirname}/${sourceDir}/login.html`); }); + +app.get('/lms/plugin', (req, res) => { + res.sendFile(`${__dirname}/${sourceDir}/plugin.html`); +}); + app.use('*', (req, res) => { setCrossOriginIsolationHeaders(res); res.sendFile(`${__dirname}/${sourceDir}/index.html`); diff --git a/package.json b/package.json index 38e326c5..ca5f84bb 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "license": "GPL-3.0-only", "scripts": { "build": "yarn run clean-dist && webpack --config=configs/webpack/prod.js", + "build-deps": "python3 dependencies/build.py && yarn add file:dependencies/kipr-scratch/kipr-scratch --cache-folder ./.yarncache", "watch": "node --max-old-space-size=8192 node_modules/webpack/bin/webpack.js --watch --config=configs/webpack/dev.js", "clean-dist": "rm -f -r -d dist", "lint": "eslint . --ext .js,.jsx,.ts,.tsx", @@ -20,11 +21,13 @@ "devDependencies": { "@babel/core": "^7.9.0", "@babel/plugin-syntax-import-meta": "^7.10.4", + "@types/chai": "^4.3.3", + "@types/gapi": "^0.0.44", "@types/gettext-parser": "^4.0.2", "@types/history": "^4.7.2", "@types/jest": "^29.4.0", "@types/qs": "^6.9.7", - "@types/react-dom": "^16.9.5", + "@types/react-dom": "17.0.1", "@types/react-redux": "^7.1.18", "@types/react-router": "^5.0.0", "@types/redux": "^3.6.0", @@ -32,6 +35,7 @@ "@types/styletron-engine-atomic": "^1.1.1", "@types/styletron-react": "^5.0.2", "@types/uuid": "^8.3.1", + "@types/xmldom": "^0.1.31", "@typescript-eslint/eslint-plugin": "^4.18.0", "@typescript-eslint/parser": "^4.18.0", "babel-loader": "^8.1.0", @@ -54,13 +58,12 @@ }, "dependencies": { "@babylonjs/core": "6.18.0", - "@babylonjs/loaders": "6.18.0", "@babylonjs/havok": "^1.0.0", + "@babylonjs/loaders": "6.18.0", "@fortawesome/fontawesome-svg-core": "^6.2.0", "@fortawesome/free-brands-svg-icons": "^6.2.0", "@fortawesome/free-solid-svg-icons": "^6.2.0", "@fortawesome/react-fontawesome": "^0.2.0", - "@types/react": "^16.9.25", "babylonjs-gltf2interface": "6.18.0", "body-parser": "^1.19.0", "colorjs.io": "^0.4.2", @@ -70,19 +73,23 @@ "dotenv": "^10.0.0", "dotenv-expand": "^5.1.0", "express-http-proxy": "^1.6.3", + "express-rate-limit": "^7.4.0", "firebase": "^9.0.1", "form-data": "^4.0.0", "history": "^4.7.2", "image-loader": "^0.0.1", "image-webpack-loader": "^6.0.0", "immer": "^9.0.15", - "ivygate": "https://github.com/kipr/ivygate#v0.1.5", + "itch": "https://github.com/chrismbirmingham/itch#36", + "ivygate": "https://github.com/kipr/ivygate#v0.1.8", + "kipr-scratch": "file:dependencies/kipr-scratch/kipr-scratch", "morgan": "^1.10.0", "prop-types": "^15.8.1", "qs": "^6.11.0", "react": "^17.0.1", "react-dom": "^17.0.1", "react-loading": "^2.0.3", + "react-markdown": "^8.0.1", "react-redux": "^7.2.4", "react-reverse-portal": "^2.0.1", "react-router": "^5.0.0", @@ -95,6 +102,7 @@ "styletron-react": "^6.0.1", "symmetry": "^0.6.1", "url-loader": "^4.1.1", - "uuid": "^8.3.2" + "uuid": "^8.3.2", + "xmldom": "^0.6.0" } } diff --git a/src/App.tsx b/src/App.tsx index 193f1964..4b84e40c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,6 +14,8 @@ import Root from './pages/Root'; import ChallengeRoot from './pages/ChallengeRoot'; import DocumentationWindow from './components/documentation/DocumentationWindow'; import { DARK } from './components/constants/theme'; +import CurriculumPage from './lms/CurriculumPage'; +import { UsersAction } from './state/reducer'; export interface AppPublicProps { @@ -21,6 +23,8 @@ export interface AppPublicProps { interface AppPrivateProps { login: () => void; + setMe: (me: string) => void; + loadUser: (uid: string) => void; } interface AppState { @@ -68,10 +72,16 @@ class App extends React.Component { componentDidMount() { this.onAuthStateChangedSubscription_ = auth.onAuthStateChanged(user => { if (user) { - this.setState({ loading: false }); + console.log('User detected.'); + this.props.loadUser(user.uid); + this.props.setMe(user.uid); } else { this.props.login(); } + + this.setState({ loading: false }, () => { + if (!user) this.props.login(); + }); }); } @@ -94,6 +104,7 @@ class App extends React.Component { + @@ -134,5 +145,7 @@ export default connect((state: ReduxState) => { login: () => { console.log('Redirecting to login page', window.location.pathname); window.location.href = `/login${window.location.pathname === '/login' ? '' : `?from=${window.location.pathname}`}`; - } + }, + setMe: (me: string) => dispatch(UsersAction.setMe({ me })), + loadUser: (uid: string) => dispatch(UsersAction.loadOrEmptyUser({ userId: uid })) }))(App) as React.ComponentType; \ No newline at end of file diff --git a/src/asset-viewer/AssetViewerPage.tsx b/src/asset-viewer/AssetViewerPage.tsx new file mode 100644 index 00000000..b377b5de --- /dev/null +++ b/src/asset-viewer/AssetViewerPage.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; + +export default () => { + return ( +
+

Asset Viewer

+
+ ); +}; \ No newline at end of file diff --git a/src/asset-viewer/index.tsx b/src/asset-viewer/index.tsx new file mode 100644 index 00000000..4507b1ea --- /dev/null +++ b/src/asset-viewer/index.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import * as ReactDom from 'react-dom'; + + +import { Provider as StyletronProvider } from "styletron-react"; +import { Client as Styletron } from "styletron-engine-atomic"; + +import { Provider as ReduxProvider } from 'react-redux'; +import store from '../state'; +import AssetViewerPage from './AssetViewerPage'; + +const reactRoot = document.getElementById('reactRoot'); + +const engine = new Styletron({ prefix: 'style' }); + +ReactDom.render( + + + + + , + reactRoot +); \ No newline at end of file diff --git a/src/components/Challenge/ChallengeMenu.tsx b/src/components/Challenge/ChallengeMenu.tsx index a00cd2cf..6f3b8713 100644 --- a/src/components/Challenge/ChallengeMenu.tsx +++ b/src/components/Challenge/ChallengeMenu.tsx @@ -56,7 +56,7 @@ export interface MenuProps extends StyleProps, ThemeProps { onShowAll: () => void; onHideAll: () => void; - onRunClick: () => void; + onRunClick?: () => void; onStopClick: () => void; onResetChallengeClick: () => void; @@ -247,7 +247,7 @@ class ChallengeMenu extends React.PureComponent { diff --git a/src/components/Challenge/SimMenu.tsx b/src/components/Challenge/SimMenu.tsx index aaec3cc9..e7911867 100644 --- a/src/components/Challenge/SimMenu.tsx +++ b/src/components/Challenge/SimMenu.tsx @@ -75,7 +75,7 @@ export interface MenuPublicProps extends StyleProps, ThemeProps { onShowAll: () => void; onHideAll: () => void; - onRunClick: () => void; + onRunClick?: () => void; onStopClick: () => void; onResetWorldClick: () => void; @@ -315,7 +315,7 @@ class SimMenu extends React.PureComponent { diff --git a/src/components/DoubleSlider.tsx b/src/components/DoubleSlider.tsx new file mode 100644 index 00000000..59087810 --- /dev/null +++ b/src/components/DoubleSlider.tsx @@ -0,0 +1,214 @@ +import * as React from 'react'; +import { styled } from 'styletron-react'; + +import { RawVector2 } from '../util/math/math'; +import { StyleProps } from '../util/style'; +import Svg from './interface/Svg'; +import { Theme, ThemeProps } from './constants/theme'; + +export interface DoubleSliderOption { + label: string; +} + +export interface DoubleSliderProps extends StyleProps, ThemeProps { + options: DoubleSliderOption[]; + + startIndex: number; + endIndex: number; + + onChange: (startIndex: number, endIndex: number) => void; +} + +const Line = styled('line', ({ $theme }: { $theme: Theme }) => ({ + stroke: $theme.borderColor, +})); + +const ConnectorLine = styled('line', ({ $theme }: { $theme: Theme }) => ({ + stroke: $theme.switch.off.primary, + strokeWidth: '4px', +})); + +const Circle = styled('circle', ({ $theme, $dragging }: { $theme: Theme; $dragging: boolean; }) => ({ + fill: $dragging ? $theme.switch.on.primary : $theme.switch.off.primary, + cursor: !$dragging ? 'pointer' : 'ew-resize', +})); + +const Text = styled('text', ({ $theme }: { $theme: Theme }) => ({ + fill: $theme.color, + fontSize: '8px', +})); + + +namespace DragState { + export interface Idle { + type: 'idle'; + } + + export interface DraggingLeft { + type: 'dragging-left'; + prevIndex: number; + nextIndex: number; + } + + export interface DraggingRight { + type: 'dragging-right'; + prevIndex: number; + nextIndex: number; + } +} + +type DragState = ( + DragState.Idle | + DragState.DraggingLeft | + DragState.DraggingRight +); + +const HANDLE_RADIUS = 10; + +export default ({ + style, + className, + theme, + options, + startIndex, + endIndex, + onChange +}: DoubleSliderProps) => { + const [dragState, setDragState] = React.useState({ type: 'idle' }); + + const draw = (size: RawVector2) => { + const texts: JSX.Element[] = []; + for (const [index, option] of options.entries()) { + const x = index / (options.length - 1) * (size.x - HANDLE_RADIUS * 2) + HANDLE_RADIUS; + const y = size.y / 2 + HANDLE_RADIUS * 2; + texts.push( + + {option.label} + + ); + } + + // Lines that drop down to the text + const textTicks: JSX.Element[] = []; + for (const [index, option] of options.entries()) { + const x = index / (options.length - 1) * (size.x - HANDLE_RADIUS * 2) + HANDLE_RADIUS; + const y = size.y / 2 + HANDLE_RADIUS; + textTicks.push( + + ); + } + + // Connector + + let draggedStartIndex = startIndex; + if (dragState.type === 'dragging-left') draggedStartIndex = dragState.nextIndex; + + const x1 = draggedStartIndex / (options.length - 1) * (size.x - HANDLE_RADIUS * 2) + HANDLE_RADIUS; + + let draggedEndIndex = endIndex; + if (dragState.type === 'dragging-right') draggedEndIndex = dragState.nextIndex; + + const x2 = draggedEndIndex / (options.length - 1) * (size.x - HANDLE_RADIUS * 2) + HANDLE_RADIUS; + + return ( + <> + {textTicks} + + + { + e.stopPropagation(); + setDragState({ type: 'dragging-left', prevIndex: startIndex, nextIndex: startIndex }); + }} + /> + { + e.stopPropagation(); + setDragState({ type: 'dragging-right', prevIndex: endIndex, nextIndex: endIndex }); + }} + /> + {texts} + + ); + }; + + const ref = React.createRef(); + + const svgStyle: React.CSSProperties = { + ...style, + cursor: dragState.type === 'dragging-left' || dragState.type === 'dragging-right' ? 'ew-resize' : 'default', + }; + + return ( + { + // Check if element is SVG + const rect = ref.current.getBoundingClientRect(); + if (rect.width === 0) return; + + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + const nextIndex = Math.round((x - HANDLE_RADIUS) / (rect.width - HANDLE_RADIUS * 2) * (options.length - 1)); + if (dragState.type === 'dragging-left' && nextIndex !== dragState.nextIndex) { + setDragState({ ...dragState, nextIndex }); + } else if (dragState.type === 'dragging-right' && nextIndex !== dragState.nextIndex) { + setDragState({ ...dragState, nextIndex }); + } + }} + onMouseUp={_ => { + switch (dragState.type) { + case 'dragging-left': { + if (dragState.nextIndex !== dragState.prevIndex) onChange(dragState.nextIndex, endIndex); + break; + } + // eslint-disable-next-line no-fallthrough + case 'dragging-right': { + if (dragState.nextIndex !== dragState.prevIndex) onChange(startIndex, dragState.nextIndex); + break; + } + } + setDragState({ type: 'idle' }); + }} + /> + ); +}; \ No newline at end of file diff --git a/src/components/Editor/Editor.tsx b/src/components/Editor/Editor.tsx index b1dc77ed..9a169e5d 100644 --- a/src/components/Editor/Editor.tsx +++ b/src/components/Editor/Editor.tsx @@ -13,13 +13,15 @@ import { Ivygate, Message } from 'ivygate'; import LanguageSelectCharm from './LanguageSelectCharm'; import ProgrammingLanguage from '../../programming/compiler/ProgrammingLanguage'; -import { faArrowsRotate, faFileDownload, faIndent } from '@fortawesome/free-solid-svg-icons'; +import { faArrowsRotate, faCompress, faExpand, faFileDownload, faIndent } from '@fortawesome/free-solid-svg-icons'; +// import { faArrowsRotate, faFileDownload, faIndent } from '@fortawesome/free-solid-svg-icons'; import Script from '../../state/State/Scene/Script'; import Dict from '../../util/objectOps/Dict'; import * as monaco from 'monaco-editor'; import tr from '@i18n'; import LocalizedString from '../../util/LocalizedString'; +import ScratchEditor from './ScratchEditor'; export enum EditorActionState { None, @@ -35,7 +37,9 @@ export interface EditorPublicProps extends StyleProps, ThemeProps { messages?: Message[]; autocomplete: boolean; - onDocumentationGoToFuzzy?: (query: string, language: 'c' | 'python') => void; + onDocumentationGoToFuzzy?: (query: string, language: 'c' | 'python' | 'scratch') => void; + + mini?: boolean; } interface EditorPrivateProps { @@ -75,6 +79,8 @@ export namespace EditorBarTarget { onDownloadCode: () => void; onResetCode: () => void; onErrorClick: (event: React.MouseEvent) => void; + mini?: boolean; + onMiniClick?: () => void; } } @@ -104,15 +110,27 @@ export const createEditorBarComponents = ({ onLanguageChange: target.onLanguageChange, })); - editorBar.push(BarComponent.create(Button, { - theme, - onClick: target.onIndentCode, - children: - <> - - {' '} {LocalizedString.lookup(tr('Indent'), locale)} - - })); + if (target.language !== 'scratch') { + editorBar.push(BarComponent.create(Button, { + theme, + onClick: target.onIndentCode, + children: + <> + + {' '} {LocalizedString.lookup(tr('Indent'), locale)} + + })); + } else { + /* editorBar.push(BarComponent.create(Button, { + theme, + onClick: target.onMiniClick, + children: + <> + + {' '} {LocalizedString.lookup(target.mini ? tr('Show Toolbox') : tr('Hide Toolbox'), locale)} + + })); */ + } editorBar.push(BarComponent.create(Button, { theme, @@ -171,8 +189,7 @@ export const IVYGATE_LANGUAGE_MAPPING: Dict = { 'ecmascript': 'javascript', }; -const DOCUMENTATION_LANGUAGE_MAPPING: { [key in ProgrammingLanguage | Script.Language]: 'c' | 'python' | undefined } = { - 'ecmascript': undefined, +const DOCUMENTATION_LANGUAGE_MAPPING: { [key in ProgrammingLanguage | Script.Language]?: 'c' | 'python' | 'scratch' | undefined } = { 'python': 'python', 'c': 'c', 'cpp': 'c', @@ -232,11 +249,22 @@ class Editor extends React.PureComponent { onCodeChange, messages, autocomplete, - language + language, + mini } = this.props; - return ( - + let component: JSX.Element; + if (language === 'scratch') { + component = ( + + ); + } else { + component = ( { onCodeChange={onCodeChange} autocomplete={autocomplete} /> + ); + } + + return ( + + {component} + ); } } diff --git a/src/components/Editor/LanguageSelectCharm.tsx b/src/components/Editor/LanguageSelectCharm.tsx index 8b2b5c30..5eb5e866 100644 --- a/src/components/Editor/LanguageSelectCharm.tsx +++ b/src/components/Editor/LanguageSelectCharm.tsx @@ -29,6 +29,9 @@ const OPTIONS: ComboBox.Option[] = [{ }, { text: 'Python', data: 'python' +}, { + text: 'Scratch', + data: 'scratch' }]; const Label = styled('div', { diff --git a/src/components/Editor/ScratchEditor.tsx b/src/components/Editor/ScratchEditor.tsx new file mode 100644 index 00000000..b3aa1530 --- /dev/null +++ b/src/components/Editor/ScratchEditor.tsx @@ -0,0 +1,172 @@ +import { ThemeProps } from 'components/constants/theme'; +import { RawVector2 } from '../../util/math/math'; +import * as React from 'react'; +import { styled } from 'styletron-react'; + +import resizeListener, { ResizeListener } from '../interface/ResizeListener'; + +export interface ScratchEditorProps extends ThemeProps { + code: string; + onCodeChange: (code: string) => void; + + toolboxHidden?: boolean; +} + +interface ScratchEditorState { + size: RawVector2; +} + +type Props = ScratchEditorProps; +type State = ScratchEditorState; + +const OuterContainer = styled('div', (props: ThemeProps) => ({ + position: 'relative', + width: '100%', + height: '100%', + zIndex: 0, +})); + +const Container = styled('div', (props: ThemeProps) => ({ + position: 'absolute', + top: 0, + left: 0, + backgroundColor: '#212121' +})); + +class ScratchEditor extends React.Component { + private resizeListener_ = resizeListener(size => this.setState({ size })); + + constructor(props: Props) { + super(props); + + this.state = { + size: RawVector2.ZERO, + }; + } + + private debounce_: boolean; + componentDidUpdate(prevProps: Readonly, prevState: Readonly) { + const { props: nextProps, state: nextState } = this; + + if (this.workspace_) { + if (prevProps.code !== nextProps.code && !this.debounce_) { + if (this.props.code === '') { + this.workspace_.clear(); + } else { + Blockly.Xml.domToWorkspace(Blockly.Xml.textToDom(this.props.code), this.workspace_); + } + } + + if (prevState.size !== nextState.size) { + Blockly.svgResize(this.workspace_); + } + } + + } + + componentWillUnmount() { + this.resizeListener_.disconnect(); + } + + private outerContainerRef_: HTMLDivElement | null = null; + private bindOuterContainerRef_ = (ref: HTMLDivElement) => { + if (this.outerContainerRef_) this.resizeListener_.unobserve(this.outerContainerRef_); + + + this.outerContainerRef_ = ref; + + if (this.outerContainerRef_) this.resizeListener_.observe(this.outerContainerRef_); + + }; + + private containerRef_: HTMLDivElement | null = null; + private bindContainerRef_ = (ref: HTMLDivElement) => { + if (this.containerRef_) { + // cleanup blockly + } + + this.containerRef_ = ref; + + if (this.containerRef_) { + this.injectBlockly_(); + } + }; + + private workspace_: Blockly.Workspace; + private injectBlockly_ = () => { + this.workspace_ = Blockly.inject(this.containerRef_, { + comments: true, + disable: false, + collapse: false, + media: '../media/', + readOnly: false, + rtl: false, + scrollbars: true, + toolbox: undefined, + toolboxPosition: 'start', + verticalLayout: 'right', + trashcan: false, + sounds: false, + zoom: { + controls: false, + wheel: true, + startScale: 0.75, + maxScale: 4, + minScale: 0.25, + scaleSpeed: 1.1 + }, + colours: { + fieldShadow: 'rgba(255, 255, 255, 0.3)', + dragShadowOpacity: 0.6 + } + }); + + console.log(this.props.code); + if (this.props.code.length > 0) { + try { + Blockly.Xml.domToWorkspace( + Blockly.Xml.textToDom(this.props.code), + this.workspace_ + ); + } catch (e) { + console.error(e); + this.workspace_.clear(); + this.props.onCodeChange(''); + + } + } + + this.workspace_.addChangeListener(this.onChange_); + }; + + private onChange_ = () => { + this.debounce_ = true; + try { + const code = Blockly.Xml.domToPrettyText(Blockly.Xml.workspaceToDom(this.workspace_)); + console.log(code); + this.props.onCodeChange(code); + } catch (e) { + // console.error(e); + } + this.debounce_ = false; + }; + + render() { + const { props, state } = this; + const { theme } = props; + const { size } = state; + + const containerStyle: React.CSSProperties = { + width: `${size.x}px`, + height: `${size.y}px`, + }; + + return ( + + + + ); + } +} + +export default ScratchEditor; \ No newline at end of file diff --git a/src/components/Editor/ScratchVariablesRoot.tsx b/src/components/Editor/ScratchVariablesRoot.tsx new file mode 100644 index 00000000..bf860a17 --- /dev/null +++ b/src/components/Editor/ScratchVariablesRoot.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import * as ReactDom from 'react-dom'; + +export interface ModalProps { + children: React.ReactNode; +} + +type Props = ModalProps; + +const SCRATCH_VARIABLES_ROOT = document.getElementById('scratch-variables-root'); + +class ScratchVariablesRoot extends React.PureComponent { + static get active() { + return SCRATCH_VARIABLES_ROOT.children.length !== 0; + } + + constructor(props?: Props) { + super(props); + } + + componentDidMount() { + SCRATCH_VARIABLES_ROOT.style.opacity = '1'; + } + + componentWillUnmount() { + SCRATCH_VARIABLES_ROOT.style.opacity = '0'; + } + + static isVisible = () => { + return SCRATCH_VARIABLES_ROOT.childElementCount > 0; + }; + + render() { + return ReactDom.createPortal(this.props.children, SCRATCH_VARIABLES_ROOT); + } +} + +export default ScratchVariablesRoot; diff --git a/src/components/Editor/ScratchVariablesWindow.tsx b/src/components/Editor/ScratchVariablesWindow.tsx new file mode 100644 index 00000000..406c3b50 --- /dev/null +++ b/src/components/Editor/ScratchVariablesWindow.tsx @@ -0,0 +1,179 @@ +import * as React from 'react'; +import { RawVector2 } from '../../util/math/math'; +import { ThemeProps } from '../constants/theme'; +import Widget, { Mode, Size } from '../interface/Widget'; +import ScratchVariablesRoot from './ScratchVariablesRoot'; + +import { GLOBAL_EVENTS } from '../../util/GlobalEvents'; +import construct from '../../util/redux/construct'; +import { styled } from 'styletron-react'; +import { State as ReduxState } from '../../state'; +import { connect } from 'react-redux'; +import ScrollArea from '../interface/ScrollArea'; + +import tr from '@i18n'; +import LocalizedString from '../../util/LocalizedString'; + +namespace DragState { + export interface None { + type: 'none'; + position: RawVector2; + } + + export const none = construct('none'); + + export interface Dragging { + type: 'dragging'; + position: RawVector2; + offset: RawVector2; + } + + export const dragging = construct('dragging'); +} + +type DragState = DragState.None | DragState.Dragging; + +export interface ScratchVariablesWindowPublicProps extends ThemeProps { + +} + +interface ScratchVariablesWindowPrivateProps { + locale: LocalizedString.Language; +} + +interface ScratchVariablesWindowState { + dragState: DragState; +} + +type Props = ScratchVariablesWindowPublicProps & ScratchVariablesWindowPrivateProps; +type State = ScratchVariablesWindowState; + +const Container = styled('div', ({ theme }: ThemeProps) => ({ + display: 'flex', + flexDirection: 'column', + minWidth: '600px', + minHeight: '400px', + width: '100%', + height: '100%', + color: theme.color, + backgroundColor: theme.backgroundColor, + transition: 'opacity 0.2s', +})); + +const StyledScrollArea = styled(ScrollArea, ({ theme }: ThemeProps) => ({ + flex: 1, +})); + +const SIZES: Size[] = [ + Size.PARTIAL, +]; + +class ScratchVariablesWindow extends React.PureComponent { + constructor(props: Props) { + super(props); + + this.state = { + dragState: DragState.none({ position: RawVector2.create(0, 0) }), + }; + } + + private onWindowMouseMove_ = (e: MouseEvent) => { + const { state } = this; + const { dragState } = state; + if (dragState.type !== 'dragging') return false; + + const client = RawVector2.fromClient(e); + const position = RawVector2.subtract(client, dragState.offset); + this.setState({ + dragState: DragState.dragging({ + position, + offset: dragState.offset + }) + }); + + return true; + }; + + private onWindowMouseUp_ = (e: MouseEvent) => { + const { state } = this; + const { dragState } = state; + if (dragState.type !== 'dragging') return false; + + this.setState({ + dragState: DragState.none({ position: dragState.position }) + }); + + GLOBAL_EVENTS.remove(this.onMouseMoveHandle_); + GLOBAL_EVENTS.remove(this.onMouseUpHandle_); + + return true; + }; + + private onMouseMoveHandle_: number; + private onMouseUpHandle_: number; + private onChromeMouseDown_ = (e: React.MouseEvent) => { + const { state } = this; + const { dragState } = state; + const topLeft = RawVector2.fromTopLeft(e.currentTarget.getBoundingClientRect()); + const client = RawVector2.fromClient(e); + + this.setState({ + dragState: DragState.dragging({ + position: dragState.position, + offset: RawVector2.subtract(client, topLeft) + }) + }); + + this.onMouseMoveHandle_ = GLOBAL_EVENTS.add('onMouseMove', this.onWindowMouseMove_); + this.onMouseUpHandle_ = GLOBAL_EVENTS.add('onMouseUp', this.onWindowMouseUp_); + }; + + componentWillUnmount() { + GLOBAL_EVENTS.remove(this.onMouseMoveHandle_); + GLOBAL_EVENTS.remove(this.onMouseUpHandle_); + } + + render() { + const { props, state } = this; + const { + locale, + theme, + } = props; + const { dragState } = state; + + const mode = Mode.Floating; + const style: React.CSSProperties = { + position: 'absolute', + opacity: dragState.type === 'dragging' ? 0.8 : 1, + }; + + style.left = `${dragState.position.x}px`; + style.top = `${dragState.position.y}px`; + + return ( + + + + + <> + {/* Looking for children but not was not included in original */} + + + + + ); + } +} + +export default connect((state: ReduxState) => ({ + locale: state.i18n.locale, +}), dispatch => ({ +}))(ScratchVariablesWindow) as React.ComponentType; diff --git a/src/components/Layout/Layout.ts b/src/components/Layout/Layout.ts index 1d0881b5..f739e9a9 100644 --- a/src/components/Layout/Layout.ts +++ b/src/components/Layout/Layout.ts @@ -25,6 +25,8 @@ export namespace LayoutEditorTarget { onLanguageChange: (language: ProgrammingLanguage) => void; code: string; onCodeChange: (code: string) => void; + mini?: boolean; + onMiniClick?: () => void; } } diff --git a/src/components/Layout/OverlayLayout.tsx b/src/components/Layout/OverlayLayout.tsx index a0c3317a..eef6d9dc 100644 --- a/src/components/Layout/OverlayLayout.tsx +++ b/src/components/Layout/OverlayLayout.tsx @@ -363,7 +363,7 @@ export class OverlayLayout extends React.PureComponent {content} diff --git a/src/components/Layout/TabBar.tsx b/src/components/Layout/TabBar.tsx index b4dcf4ed..863d1a1b 100644 --- a/src/components/Layout/TabBar.tsx +++ b/src/components/Layout/TabBar.tsx @@ -82,6 +82,8 @@ const TabBarContainer = styled('div', (props: ThemeProps & { $vertical: boolean export class TabBar extends React.PureComponent { private onClick_ = (index: number) => (event: React.MouseEvent) => { + event.stopPropagation(); + event.preventDefault(); this.props.onIndexChange(index, event); }; diff --git a/src/components/PillArea.tsx b/src/components/PillArea.tsx new file mode 100644 index 00000000..e03c671f --- /dev/null +++ b/src/components/PillArea.tsx @@ -0,0 +1,88 @@ +/* eslint-disable no-extra-boolean-cast */ +import { faCross, faTimes } from '@fortawesome/free-solid-svg-icons'; +import Color from 'colorjs.io'; +import * as React from 'react'; + +import { styled } from 'styletron-react'; +import { StyleProps } from '../util/style'; +import { FontAwesome } from './FontAwesome'; +import { Theme, ThemeProps } from './constants/theme'; + +export interface PillAreaItem { + id: T; + label: string; +} + +export interface PillAreaProps extends ThemeProps, StyleProps { + items: PillAreaItem[]; + selectedItems?: Set; + onSelectionChange?: (s: Set) => void; + onItemRemove?: (item: T) => void; +} + + +const Pill = styled('span', ({ $theme, $selected, onClick }: { + $theme: Theme, + $selected: boolean, + onClick: unknown +}) => ({ + display: 'inline-flex', + flexDirection: 'row', + alignItems: 'center', + lineHeight: `${$theme.itemPadding * 2}px`, + padding: `${$theme.itemPadding * 2}px`, + marginBottom: `${$theme.itemPadding * 2}px`, + marginRight: `${$theme.itemPadding * 2}px`, + ':last-child': { + marginRight: 0, + }, + borderRadius: `${$theme.itemPadding * 2}px`, + border: `1px solid ${$theme.borderColor}`, + cursor: !!onClick ? 'pointer' : 'default', + backgroundColor: !!$selected ? `rgba(0, 0, 0, 0.1)` : 'white', + ':hover': !!onClick ? { + backgroundColor: `rgba(0, 0, 0, 0.05)`, + } : undefined, +})); + +const StyledFa = styled(FontAwesome, ({ $theme }: { $theme: Theme }) => ({ + marginLeft: `${$theme.itemPadding}px`, + cursor: 'pointer', +})); + +// eslint-disable-next-line @typescript-eslint/ban-types +export default ({ + items, + selectedItems, + onSelectionChange, + onItemRemove, + theme, + style, + className +}: PillAreaProps) => { + return ( +
+ {items.map((item, i) => ( + { + const newSelectedItems = new Set(selectedItems); + if (newSelectedItems.has(item.id)) { + newSelectedItems.delete(item.id); + } else { + newSelectedItems.add(item.id); + } + onSelectionChange(newSelectedItems); + } : undefined} + > + {item.label} + {!!onItemRemove && ( + onItemRemove(item.id)} /> + )} + + ))} +
+ ); +}; \ No newline at end of file diff --git a/src/components/SearchableSwitchList.tsx b/src/components/SearchableSwitchList.tsx new file mode 100644 index 00000000..844321a4 --- /dev/null +++ b/src/components/SearchableSwitchList.tsx @@ -0,0 +1,98 @@ +// A component that shows a list of items that can be selected, deselected, and filtered. + +import * as React from 'react'; +import { styled } from 'styletron-react'; +import LocalizedString from '../util/LocalizedString'; +import { StyleProps } from '../util/style'; +import Input from './interface/Input'; +import ScrollArea from './interface/ScrollArea'; +import { Switch } from './Switch'; +import { Theme, ThemeProps } from './constants/theme'; + +import tr from '@i18n'; + +export interface SearchableSwitchListItem { + id: string; + name: string; +} + +export interface SearchableSwitchListProps extends ThemeProps, StyleProps { + items: SearchableSwitchListItem[]; + + selectedItems: Set; + onSelectionChange: (selectedItems: Set) => void; + + filter: string; + onFilterChange: (filter: string) => void; + + locale: LocalizedString.Language; +} + +const Container = styled('div', { + width: '100%' +}); + +const List = styled(ScrollArea, ({ theme }: { theme: Theme }) => ({ + width: '100%', + height: '300px', + borderRadius: `${theme.itemPadding * 2}px`, + marginTop: `${theme.itemPadding * 2}px`, + border: `1px solid ${theme.borderColor}` +})); + +const Row = styled('div', ({ $theme }: { $theme: Theme }) => ({ + width: '100%', + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + paddingLeft: `${$theme.itemPadding * 2}px`, +})); + +const RowLabel = styled('div', { + flex: 1 +}); + +export default ({ + theme, + style, + className, + items, + selectedItems, + onSelectionChange, + filter, + onFilterChange, + locale +}: SearchableSwitchListProps) => { + + const filteredItems = items.filter(item => item.name.toLowerCase().includes(filter.toLowerCase())); + return ( + + onFilterChange(event.currentTarget.value)} + placeholder={LocalizedString.lookup(tr('Search'), locale)} + /> + + {filteredItems.map(item => ( + + {item.name} + { + const newSelectedItems = new Set(selectedItems); + if (value) { + newSelectedItems.add(item.id); + } else { + newSelectedItems.delete(item.id); + } + onSelectionChange(newSelectedItems); + }} + /> + + ))} + + + ); +}; \ No newline at end of file diff --git a/src/components/constants/theme.ts b/src/components/constants/theme.ts index 3d8741c9..a98d2d47 100644 --- a/src/components/constants/theme.ts +++ b/src/components/constants/theme.ts @@ -82,12 +82,13 @@ export const LIGHT: Theme = { ...COMMON, foreground: 'white', color: '#000', - backgroundColor: '#fff', + backgroundColor: '#efefef', + borderColor: '#c0c0c0', transparentBackgroundColor: a => `rgba(255, 255, 255, ${a})`, switch: { on: { - primary: 'rgb(0, 0, 0)', - secondary: 'rgb(72, 139, 73)' + primary: 'rgb(63, 63, 63)', + secondary: 'rgb(72, 200, 73)' }, off: { primary: 'rgb(127, 127, 127)', diff --git a/src/components/interface/Svg.tsx b/src/components/interface/Svg.tsx index ae98aef5..3ac6ce7f 100644 --- a/src/components/interface/Svg.tsx +++ b/src/components/interface/Svg.tsx @@ -14,6 +14,8 @@ export interface SvgProps extends StyleProps { onMouseEnter?: (event: React.MouseEvent) => void; onMouseLeave?: (event: React.MouseEvent) => void; onMouseMove?: (event: React.MouseEvent) => void; + + svgRef?: React.Ref; } interface SvgState { @@ -76,12 +78,12 @@ class Svg extends React.Component { render() { const { props, state } = this; - const { className, style, draw, ...rest } = props; + const { className, style, draw, svgRef, ...rest } = props; const { size } = state; return ( - + {draw(size)} diff --git a/src/db/Record.ts b/src/db/Record.ts index 451fe59b..a5bcfb76 100644 --- a/src/db/Record.ts +++ b/src/db/Record.ts @@ -1,15 +1,20 @@ +import { AsyncAssignment } from 'state/State/Assignment'; +import { AsyncUser } from 'state/State/User'; import Async from '../state/State/Async'; import { AsyncChallenge } from '../state/State/Challenge'; import { AsyncChallengeCompletion } from '../state/State/ChallengeCompletion'; import { AsyncScene } from '../state/State/Scene'; import { CHALLENGE_COLLECTION, CHALLENGE_COMPLETION_COLLECTION, SCENE_COLLECTION } from './constants'; import Selector from './Selector'; +import LocalizedString from '../util/LocalizedString'; namespace Record { export enum Type { Scene = 'scene', Challenge = 'challenge', ChallengeCompletion = 'challenge-completion', + User = 'user', + Assignment = 'assignment', } interface Base { @@ -29,11 +34,21 @@ namespace Record { type: Type.ChallengeCompletion; } + export interface User extends Base { + type: Type.User; + } + + export interface Assignment extends Base { + type: Type.Assignment; + } + export const selector = (record: Record): Selector => { switch (record.type) { case Type.Scene: return { collection: SCENE_COLLECTION, id: record.id }; case Type.Challenge: return { collection: CHALLENGE_COLLECTION, id: record.id }; case Type.ChallengeCompletion: return { collection: CHALLENGE_COMPLETION_COLLECTION, id: record.id }; + case Type.User: return { collection: 'users', id: record.id }; + case Type.Assignment: return { collection: 'assignments', id: record.id }; } }; @@ -42,6 +57,8 @@ namespace Record { case Type.Scene: return Async.latestValue(record.value).name; case Type.Challenge: return Async.latestValue(record.value).name; case Type.ChallengeCompletion: return undefined; + case Type.User: return { [LocalizedString.EN_US]: Async.latestValue(record.value).name }; + case Type.Assignment: return Async.latestValue(record.value).name; } }; @@ -50,6 +67,8 @@ namespace Record { case Type.Scene: return Async.latestValue(record.value).description; case Type.Challenge: return Async.latestValue(record.value).description; case Type.ChallengeCompletion: return undefined; + case Type.User: return undefined; + case Type.Assignment: return undefined; } }; } @@ -57,7 +76,9 @@ namespace Record { type Record = ( Record.Scene | Record.Challenge | - Record.ChallengeCompletion + Record.ChallengeCompletion | + Record.User | + Record.Assignment ); export default Record; \ No newline at end of file diff --git a/src/db/Selector.ts b/src/db/Selector.ts index 3db36fb1..64de7451 100644 --- a/src/db/Selector.ts +++ b/src/db/Selector.ts @@ -2,6 +2,8 @@ import { SCENE_COLLECTION, CHALLENGE_COLLECTION, CHALLENGE_COMPLETION_COLLECTION, + USER_COLLECTION, + ASSIGNMENT_COLLECTION, } from './constants'; interface Selector { @@ -24,6 +26,16 @@ namespace Selector { collection: CHALLENGE_COMPLETION_COLLECTION, id, }); + + export const user = (id: string): Selector => ({ + collection: USER_COLLECTION, + id, + }); + + export const assignment = (id: string): Selector => ({ + collection: ASSIGNMENT_COLLECTION, + id, + }); } export default Selector; \ No newline at end of file diff --git a/src/db/constants.ts b/src/db/constants.ts index 5c457840..f7a9e46c 100644 --- a/src/db/constants.ts +++ b/src/db/constants.ts @@ -1,3 +1,5 @@ export const SCENE_COLLECTION = 'scene'; export const CHALLENGE_COLLECTION = 'challenge'; -export const CHALLENGE_COMPLETION_COLLECTION = 'challenge_completion'; \ No newline at end of file +export const CHALLENGE_COMPLETION_COLLECTION = 'challenge_completion'; +export const USER_COLLECTION = 'user'; +export const ASSIGNMENT_COLLECTION = 'assignment'; \ No newline at end of file diff --git a/src/index.html.ejs b/src/index.html.ejs index f662c494..f9f17d8f 100644 --- a/src/index.html.ejs +++ b/src/index.html.ejs @@ -154,9 +154,16 @@
+
+
-
+ + + + + + \ No newline at end of file diff --git a/src/lms/AssignmentView.tsx b/src/lms/AssignmentView.tsx new file mode 100644 index 00000000..082136e2 --- /dev/null +++ b/src/lms/AssignmentView.tsx @@ -0,0 +1,144 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { State } from 'state'; +import Assignment, { AsyncAssignment } from '../state/State/Assignment'; +import { styled } from 'styletron-react'; +import LocalizedString from '../util/LocalizedString'; +import { Theme, ThemeProps } from '../components/constants/theme'; +import { StyleProps } from '../util/style'; +import Async from '../state/State/Async'; +import { AddButton, AddContainer, GradesContainer, NameContainer, SubjectsContainer, UsStdContainer } from './common'; +import { faChalkboardTeacher, faCheck, faGraduationCap, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesome } from '../components/FontAwesome'; +import Markdown from 'react-markdown'; +import { Tab, TabBar } from '../components/Layout/TabBar'; +import Subject from '../state/State/Assignment/Subject'; + + +export interface AssignmentViewProps extends StyleProps { + theme: Theme; + assignment: AsyncAssignment; + locale: LocalizedString.Language; + + added: boolean; + onAddedChange: (added: boolean) => void; + + expanded: boolean; + onExpandedChange: (expanded: boolean) => void; +} + +const Container = styled('div', ({ $theme, onClick }: { $theme: Theme; onClick: unknown; }) => ({ + width: '100%', + borderRadius: `${$theme.itemPadding * 2}px`, + border: `1px solid ${$theme.borderColor}`, + marginBottom: `${$theme.itemPadding * 2}px`, + ':last-child': { + marginBottom: 0, + }, + cursor: onClick ? 'pointer' : 'default', + backgroundColor: 'white', + overflow: 'hidden', +})); + +const Header = styled('div', ({ $theme, $expanded }: { $theme: Theme; $expanded: boolean; }) => ({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + width: '100%', + padding: `${$theme.itemPadding * 2}px`, + borderBottom: $expanded ? `1px solid ${$theme.borderColor}` : 'none', +})); + +const Body = styled('div', ({ $theme }: { $theme: Theme; }) => ({ + width: '100%', + overflow: 'hidden', +})); + +const StyledMarkdown = styled(Markdown, ({ $theme }: { $theme: Theme; }) => ({ + paddingLeft: `${$theme.itemPadding * 2}px`, + paddingRight: `${$theme.itemPadding * 2}px`, +})); + +const StyledTabBar = styled(TabBar, ({ theme }: ThemeProps) => ({ + width: '100%', + borderTop: `1px solid ${theme.borderColor}`, + backgroundColor: 'white' +})); + +export default ({ + style, + className, + theme, + assignment, + locale, + added, + onAddedChange, + expanded, + onExpandedChange +}: AssignmentViewProps) => { + const common = Async.latestCommon(assignment); + + const gradeLevelRanges = Assignment.gradeLevelRanges(new Set(common.gradeLevels)); + const gradeLevelAbbreviations = gradeLevelRanges.map(([min, max]) => { + if (min === max) LocalizedString.lookup(Assignment.gradeLevelAbbreviatedString(min), locale); + return `${LocalizedString.lookup(Assignment.gradeLevelAbbreviatedString(min), locale)}-${LocalizedString.lookup(Assignment.gradeLevelAbbreviatedString(max), locale)}`; + }); + + const gradeLevelString = gradeLevelAbbreviations.join(', '); + + const latestAssignment = Async.latestValue(assignment); + + const bodyTabs: TabBar.TabDescription[] = [{ + name: 'Educator Notes', + icon: faChalkboardTeacher, + }, { + name: 'Student Notes', + icon: faGraduationCap, + }]; + + const [bodyTabIndex, setBodyTabIndex] = React.useState(0); + + return ( + onExpandedChange(!expanded)} + > +
+ {LocalizedString.lookup(common.name, locale)} + + {common.standardsAligned ? : null} + + + {gradeLevelString} + + {common.subjects + .map(sub => LocalizedString.lookup(Subject.toString(sub), locale)).join(', ') + } + + { + e.stopPropagation(); + e.preventDefault(); + onAddedChange(!added); + }}> + {added ? 'Remove' : 'Add'} + + +
+ {expanded && latestAssignment && ( + + + {LocalizedString.lookup(bodyTabIndex === 0 ? latestAssignment.educatorNotes : latestAssignment.studentNotes, locale)} + + + + )} +
+ ); +}; diff --git a/src/lms/AssignmentsView.tsx b/src/lms/AssignmentsView.tsx new file mode 100644 index 00000000..e9b610e3 --- /dev/null +++ b/src/lms/AssignmentsView.tsx @@ -0,0 +1,318 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import Input from '../components/interface/Input'; +import * as React from 'react'; +import { StyleProps } from 'util/style'; +import { styled, StyletronComponent, withStyleDeep } from 'styletron-react'; +import { Theme, ThemeProps } from '../components/constants/theme'; +import LocalizedString from '../util/LocalizedString'; + +import tr from '@i18n'; +import Assignment, { AsyncAssignment } from 'state/State/Assignment'; +import Dict from '../util/objectOps/Dict'; +import AssignmentView from './AssignmentView'; +import Async from '../state/State/Async'; +import { AddContainer, GradesContainer, NameContainer, SubjectsContainer, UsStdContainer } from './common'; +import { FontAwesome } from '../components/FontAwesome'; +import { faCaretDown, faCaretUp } from '@fortawesome/free-solid-svg-icons'; +import StandardsLocation from '../state/State/Assignment/StandardsLocation'; + +import Color from 'colorjs.io'; +import Subject_ from '../state/State/Assignment/Subject'; +import PillArea, { PillAreaItem, PillAreaProps } from '../components/PillArea'; +import construct from '../util/redux/construct'; +import { sprintf } from 'sprintf-js'; + +export interface AssignmentsViewProps extends ThemeProps, StyleProps { + locale: LocalizedString.Language; + assignments: Dict; + + standardsAligned: boolean; + onStandardsAlignedChange: (aligned: boolean) => void; + + subjectsSelected?: Set; + standardsSelected?: Set; + + gradeLevels: [number, number]; + onGradeLevelsChange: (gradeLevels: [number, number]) => void; + + onRemoveSubject?: (subject: Subject_) => void; + onRemoveStandard?: (standard: StandardsLocation) => void; + + added: Set; + onAddedChange: (added: Set) => void; + + expanded: Set; + onExpandedChange: (expanded: Set) => void; +} + +const Container = styled('div', ({ $theme }: { $theme: Theme }) => ({ + backgroundColor: $theme.backgroundColor, + width: '100%', + padding: `${$theme.itemPadding * 2}px`, +})); + +const Search = styled(Input, ({ theme }: ThemeProps) => ({ + width: '100%', + marginBottom: `${theme.itemPadding * 2}px`, +})); + +const Header = styled('div', ({ $theme }: { $theme: Theme }) => ({ + display: 'flex', + flexDirection: 'row', + width: '100%', + borderRadius: `${$theme.itemPadding * 2}px`, + padding: `${$theme.itemPadding * 2}px`, + border: `1px solid ${$theme.borderColor}`, + marginBottom: `${$theme.itemPadding * 2}px`, + backgroundColor: 'white' +})); + +namespace Sort { + export interface Name { + direction: 'asc' | 'desc'; + } + + export interface UsStd { + direction: 'asc' | 'desc'; + } +} + +const clickable = element => withStyleDeep(element, { + cursor: 'pointer', + userSelect: 'none', +}); + +const StyledNameContainer = clickable(NameContainer); +const StyledUsStdContainer = clickable(UsStdContainer); + +type SortDirection = 'asc' | 'desc'; + +const sortIcon = (direction: SortDirection) => (direction === 'asc' ? faCaretUp : faCaretDown); + +namespace PillId { + export interface Subject { + type: 'subject'; + subject: Subject_; + } + + export const subject = construct('subject'); + + export interface Standard { + type: 'standard'; + standard: StandardsLocation; + } + + export const standard = construct('standard'); + + export interface GradeLevels { + type: 'gradeLevels'; + gradeLevels: [number, number]; + } + + export const gradeLevels = construct('gradeLevels'); +} + +type PillId = ( + PillId.Subject | + PillId.Standard | + PillId.GradeLevels +); + +const StyledPillArea = styled(PillArea, ({ theme }: { theme: Theme }) => ({ + marginBottom: `${theme.itemPadding * 2}px`, +})) as StyletronComponent, keyof PillAreaProps> & { + theme: Theme; +}>; + +const AssignmentsView = ({ + style, + className, + theme, + locale, + assignments, + subjectsSelected, + standardsSelected, + onStandardsAlignedChange, + standardsAligned, + onRemoveStandard, + onRemoveSubject, + added, + onAddedChange, + expanded, + onExpandedChange, + gradeLevels, + onGradeLevelsChange +}: AssignmentsViewProps) => { + const [filter, setFilter] = React.useState(''); + const [nameSort, setNameSort] = React.useState('asc'); + const [usStdSort, setUsStdSort] = React.useState('desc'); + + const assignmentsList = Dict.toList(assignments).filter(([_, assignment]) => !!Async.latestCommon(assignment)); + + // filter standards aligned + let filteredAssignments = standardsAligned ? assignmentsList.filter(([_, assignment]) => { + const latestValue = Async.latestValue(assignment); + if (!latestValue) return false; + + return latestValue.standardsAligned; + }) : assignmentsList; + + // filter subjects + if (subjectsSelected && subjectsSelected.size > 0) { + filteredAssignments = filteredAssignments.filter(([_, assignment]) => { + const latestValue = Async.latestValue(assignment); + if (!latestValue) return false; + return latestValue.subjects.some(subject => subjectsSelected.has(subject)); + }); + } + + // filter standards + if (standardsSelected && standardsSelected.size > 0) { + filteredAssignments = filteredAssignments.filter(([_, assignment]) => { + const latestValue = Async.latestValue(assignment); + if (!latestValue) return false; + return latestValue.standardsConformance.some(standard => standardsSelected.has(standard)); + }); + } + + // filter grade levels + if (gradeLevels[0] !== 0 || gradeLevels[1] !== 12) { + filteredAssignments = filteredAssignments.filter(([_, assignment]) => { + const latestValue = Async.latestValue(assignment); + if (!latestValue) return false; + return latestValue.gradeLevels.some(gradeLevel => gradeLevel >= gradeLevels[0] && gradeLevel <= gradeLevels[1]); + }); + } + + // filter search + filteredAssignments = filteredAssignments.filter(([_, assignment]) => { + const common = Async.latestCommon(assignment)!; + const name = LocalizedString.lookup(common.name, locale); + if (name.toLowerCase().includes(filter.toLowerCase())) return true; + + const latestValue = Async.latestValue(assignment); + if (!latestValue) return false; + + const educatorNotes = LocalizedString.lookup(latestValue.educatorNotes, locale); + if (educatorNotes.toLowerCase().includes(filter.toLowerCase())) return true; + + const studentNotes = LocalizedString.lookup(latestValue.studentNotes, locale); + if (studentNotes.toLowerCase().includes(filter.toLowerCase())) return true; + + return false; + }); + + const sortedAssignments = filteredAssignments.sort(([aId, a], [bId, b]) => { + const aCommon = Async.latestCommon(a)!; + const bCommon = Async.latestCommon(b)!; + + const aName = LocalizedString.lookup(aCommon.name, locale); + const bName = LocalizedString.lookup(bCommon.name, locale); + return nameSort === 'asc' ? aName.localeCompare(bName) : bName.localeCompare(aName); + }); + + sortedAssignments.sort(([aId, a], [bId, b]) => { + const aCommon = Async.latestCommon(a)!; + const bCommon = Async.latestCommon(b)!; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + const aUsStd = aCommon.standardsConformance.findIndex(sc => sc === StandardsLocation.UnitedStates); + const bUsStd = bCommon.standardsConformance.findIndex(sc => sc === StandardsLocation.UnitedStates); + + return usStdSort === 'asc' ? aUsStd - bUsStd : bUsStd - aUsStd; + }); + + const pillItems: PillAreaItem[] = []; + + if (gradeLevels[0] !== 0 || gradeLevels[1] !== 12) { + pillItems.push({ + id: PillId.gradeLevels({ gradeLevels }), + label: sprintf( + LocalizedString.lookup(tr('Grade Levels: %s'), locale), + `${LocalizedString.lookup(Assignment.gradeLevelString(gradeLevels[0]), locale)} - ${LocalizedString.lookup(Assignment.gradeLevelString(gradeLevels[1]), locale)}` + ), + }); + } + + for (const standard of standardsSelected || new Set()) { + pillItems.push({ + id: PillId.standard({ standard }), + label: sprintf( + LocalizedString.lookup(tr('Standard: %s'), locale), + standard + ), + }); + } + + for (const subject of subjectsSelected || new Set()) { + pillItems.push({ + id: PillId.subject({ subject }), + label: sprintf( + LocalizedString.lookup(tr('Subject: %s'), locale), + LocalizedString.lookup(Subject_.toString(subject), locale) + ), + }); + } + + return ( + + setFilter(e.currentTarget.value)} + placeholder={LocalizedString.lookup(tr('Search'), locale)} + /> + {pillItems.length > 0 && ( + { + switch (id.type) { + case 'subject': return onRemoveSubject(id.subject); + case 'standard': return onRemoveStandard(id.standard); + case 'gradeLevels': return onGradeLevelsChange([0, 12]); + } + }} + /> + )} +
+ setNameSort(nameSort === 'asc' ? 'desc' : 'asc')}> + {LocalizedString.lookup(tr('Name'), locale)} + + setUsStdSort(usStdSort === 'asc' ? 'desc' : 'asc')}> + {LocalizedString.lookup(tr('U.S. Std.'), locale)} + + Grades + Subjects + +
+ {sortedAssignments.map(([id, assignment]) => ( + { + const nextAdded = new Set(added); + if (addedSingle) nextAdded.add(id); + else nextAdded.delete(id); + console.log(nextAdded); + onAddedChange(nextAdded); + }} + expanded={expanded.has(id)} + onExpandedChange={expandedSingle => { + const nextExpanded = new Set(expanded); + if (expandedSingle) nextExpanded.add(id); + else nextExpanded.delete(id); + onExpandedChange(nextExpanded); + }} + /> + ))} +
+ ); +}; + +export default AssignmentsView; \ No newline at end of file diff --git a/src/lms/CurriculumPage.tsx b/src/lms/CurriculumPage.tsx new file mode 100644 index 00000000..fc318af0 --- /dev/null +++ b/src/lms/CurriculumPage.tsx @@ -0,0 +1,233 @@ +import { TabBar } from '../components/Layout/TabBar'; +import * as React from 'react'; +import { RouteComponentProps } from 'react-router'; +import { styled } from 'styletron-react'; +import LocalizedString from '../util/LocalizedString'; +import { DARK, LIGHT, Theme, ThemeProps } from '../components/constants/theme'; + +import tr from '@i18n'; +import { faHome, faSchool, faSchoolCircleCheck } from '@fortawesome/free-solid-svg-icons'; +import { connect } from 'react-redux'; +import { State } from '../state'; +import SearchFilters from './SearchFilters'; +import AssignmentsView from './AssignmentsView'; +import { AsyncAssignment } from '../state/State/Assignment'; +import Dict from '../util/objectOps/Dict'; +import { FontAwesome } from '../components/FontAwesome'; +import Async from '../state/State/Async'; +import Subject from '../state/State/Assignment/Subject'; +import { PillAreaItem } from '../components/PillArea'; +import StandardsLocation from '../state/State/Assignment/StandardsLocation'; +import { UsersAction } from '../state/reducer'; + +export interface CurriculumPagePublicProps extends RouteComponentProps { + +} + +export interface CurriculumPagePrivateProps extends ThemeProps { + locale: LocalizedString.Language; + assignments: Dict; + + userId: string; + myAssignments: Set; + onMyAssignmentsChange: (myAssignments: Set) => void; +} + +const Container = styled('div', ({ $theme }: { $theme: Theme }) => ({ + width: '100%', + display: 'flex', + flexDirection: 'column', + color: $theme.color, + backgroundColor: $theme.backgroundColor, + minHeight: '100vh', +})); + +const Body = styled('div', { + flex: 1, + display: 'flex', + flexDirection: 'row', + '@screen and (max-width: 800px)': { + flexDirection: 'column', + }, +}); + +const TopBar = styled('div', ({ $theme }: { $theme: Theme }) => ({ + width: '100%', + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + borderBottom: `1px solid ${$theme.borderColor}`, + height: '48px', +})); + +const StyledTabBar = styled(TabBar, ({ theme }: ThemeProps) => ({ + flex: 1, + borderTopLeftRadius: `${theme.itemPadding * 2}px`, + borderTopRightRadius: `${theme.itemPadding * 2}px`, + alignSelf: 'end', + borderTop: `1px solid ${theme.borderColor}`, + borderLeft: `1px solid ${theme.borderColor}`, + borderRight: `1px solid ${theme.borderColor}`, + backgroundColor: 'white', + ':last-child': { + marginRight: `${theme.itemPadding * 2}px`, + } + +})); + +const TopFa = styled(FontAwesome, ({ $theme }: { $theme: Theme }) => ({ + paddingLeft: `${$theme.itemPadding * 2}px`, + paddingRight: `${$theme.itemPadding * 2}px`, + fontSize: '32px', +})); + + +const AddBotballPluginButton = styled('button', ({ $theme }: { $theme: Theme }) => ({ + // Remove all button styles + border: 'none', + background: 'none', + color: 'inherit', + padding: `${$theme.itemPadding * 2}px`, + + margin: `0 ${$theme.itemPadding * 2}px`, + // Add custom styles +})); + + +const CurriculumPage = ({ + theme, + locale, + assignments, + myAssignments, + onMyAssignmentsChange, + userId +}: CurriculumPagePublicProps & CurriculumPagePrivateProps) => { + const [tabIndex, setTabIndex] = React.useState(0); + const [isStandardsAligned, setIsStandardsAligned] = React.useState(false); + const [subjectsSelected, setSubjectsSelected] = React.useState>(new Set()); + const [standardsSelected, setStandardsSelected] = React.useState>(new Set()); + const [expandedAssignments, setExpandedAssignments] = React.useState>(new Set()); + const [gradeLevels, setGradeLevels] = React.useState<[number, number]>([0, 12]); + + const tabs: TabBar.TabDescription[] = [{ + name: LocalizedString.lookup(tr('Curriculum'), locale), + icon: faSchool + }, { + name: LocalizedString.lookup(tr('My Assignments'), locale), + icon: faSchoolCircleCheck + }]; + + + return ( + + + { + window.location.pathname = '/'; + }} + /> + + + {/* + Add Botball Plugin + */} + + {tabIndex === 0 ? ( + + + { + const newStandardsSelected = new Set(standardsSelected); + newStandardsSelected.delete(standard); + setStandardsSelected(newStandardsSelected); + }} + onRemoveSubject={subject => { + const newSubjectsSelected = new Set(subjectsSelected); + newSubjectsSelected.delete(subject); + setSubjectsSelected(newSubjectsSelected); + }} + added={myAssignments} + onAddedChange={added => onMyAssignmentsChange(added)} + expanded={expandedAssignments} + onExpandedChange={setExpandedAssignments} + standardsAligned={isStandardsAligned} + onStandardsAlignedChange={setIsStandardsAligned} + /> + + ) : ( + + + myAssignments.has(id))} + added={myAssignments} + onAddedChange={added => onMyAssignmentsChange(added)} + expanded={expandedAssignments} + onExpandedChange={setExpandedAssignments} + gradeLevels={gradeLevels} + onGradeLevelsChange={setGradeLevels} + onStandardsAlignedChange={setIsStandardsAligned} + standardsAligned={isStandardsAligned} + /> + + )} + + ); +}; + +export default connect((state: State) => { + let myAssignments = new Set(); + if (state.users.me) { + const latestMe = Async.latestValue(state.users.users[state.users.me]); + if (latestMe && latestMe.myAssignments) myAssignments = new Set(latestMe.myAssignments); + } + return { + theme: LIGHT, + locale: state.i18n.locale, + assignments: state.assignments, + myAssignments + }; +}, (dispatch, ownProps) => ({ + onMyAssignmentsChange: (myAssignments: Set) => dispatch(UsersAction.setMyAssignments({ + assignmentIds: Array.from(myAssignments) + })) +}))(CurriculumPage) as React.ComponentType; \ No newline at end of file diff --git a/src/lms/SearchFilters.tsx b/src/lms/SearchFilters.tsx new file mode 100644 index 00000000..9295d69b --- /dev/null +++ b/src/lms/SearchFilters.tsx @@ -0,0 +1,183 @@ +import Section from '../components/interface/Section'; +import { Switch } from '../components/Switch'; +import { Theme, ThemeProps } from '../components/constants/theme'; +import * as React from 'react'; + +import { StyleProps } from 'util/style'; +import { styled } from 'styletron-react'; +import LocalizedString from '../util/LocalizedString'; + +import tr from '@i18n'; +import ValueEdit from '../components/ValueEdit'; +import SearchableSwitchList, { SearchableSwitchListItem } from '../components/SearchableSwitchList'; +import StandardsLocation from '../state/State/Assignment/StandardsLocation'; +import Assignment, { AsyncAssignment } from '../state/State/Assignment'; +import Dict from '../util/objectOps/Dict'; +import Async from '../state/State/Async'; +import Widget, { Mode } from '../components/interface/Widget'; +import Subject from '../state/State/Assignment/Subject'; +import PillArea, { PillAreaItem } from '../components/PillArea'; +import DoubleSlider, { DoubleSliderOption } from '../components/DoubleSlider'; + +export interface SearchFiltersProps extends ThemeProps, StyleProps { + locale: LocalizedString.Language; + + isStandardsAligned: boolean; + onIsStandardsAlignedChange: (isStandardsAligned: boolean) => void; + + subjectsSelected: Set; + onSubjectsSelectedChange: (subjectsSelected: Set) => void; + + standardsSelected: Set; + onStandardsSelectedChange: (standardsSelected: Set) => void; + + assignments: Dict; + + gradeLevels: [number, number]; + onGradeLevelsChange: (gradeLevels: [number, number]) => void; +} + +const StyledWidget = styled(Widget, ({ theme }: { theme: Theme }) => ({ + minWidth: '300px', + width: '300px', + maxWidth: '300px', + backgroundColor: 'white' +})); + +const Row = styled('div', { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', +}); + +const RowLabel = styled('div', ({ $theme }: { $theme: Theme }) => ({ + flex: 1, + paddingLeft: `${$theme.itemPadding * 2}px`, +})); + +const StyledDoubleSlider = styled(DoubleSlider, ({ theme }: { theme: Theme }) => ({ + height: '50px', +})); + +export default ({ + style, + className, + theme, + locale, + isStandardsAligned, + onIsStandardsAlignedChange, + subjectsSelected, + onStandardsSelectedChange, + onSubjectsSelectedChange, + standardsSelected, + assignments, + gradeLevels, + onGradeLevelsChange +}: SearchFiltersProps) => { + const [standardsCollapsed, setStandardsCollapsed] = React.useState(false); + const [subjectCollapsed, setSubjectCollapsed] = React.useState(false); + const [gradeLevelCollapsed, setGradeLevelCollapsed] = React.useState(false); + + const [standardsFilter, setStandardsFilter] = React.useState(''); + + + const standardsLocations: SearchableSwitchListItem[] = []; + for (const unitedStatesTerritory of StandardsLocation.UNITED_STATES_TERRITORIES) { + const conformingAssignments = Dict.filter(assignments, a => + Async.latestCommon(a).standardsConformance.findIndex(sc => + sc === unitedStatesTerritory + ) !== -1 + ); + standardsLocations.push({ + id: unitedStatesTerritory, + name: `${unitedStatesTerritory} (${Dict.keySet(conformingAssignments).size})` + }); + } + + const subjects = new Set(); + for (const assignment of Object.values(assignments)) { + const common = Async.latestCommon(assignment); + if (!common) continue; + for (const subject of common.subjects) subjects.add(subject); + } + + const subjectItems: PillAreaItem[] = []; + + for (const subject of subjects) { + subjectItems.push({ + id: subject, + label: LocalizedString.lookup(Subject.toString(subject), locale) + }); + } + + const sliderOptions: DoubleSliderOption[] = []; + for (let i = 0; i <= 12; ++i) { + sliderOptions.push({ + label: LocalizedString.lookup(Assignment.gradeLevelAbbreviatedString(i), locale) + }); + } + + return ( + + + {LocalizedString.lookup(tr('Is standards aligned'), locale)} + + + +
+ ) => void} + filter={standardsFilter} + onFilterChange={setStandardsFilter} + locale={locale} + /> +
+ {subjectItems.length > 0 &&
+ +
} +
+ onGradeLevelsChange([s, e])} + theme={theme} + /> +
+
+ + ); +}; \ No newline at end of file diff --git a/src/lms/common.tsx b/src/lms/common.tsx new file mode 100644 index 00000000..3ee0cd6e --- /dev/null +++ b/src/lms/common.tsx @@ -0,0 +1,35 @@ +import { styled } from 'styletron-react'; +import { RED, Theme } from '../components/constants/theme'; + +const Base = styled('div', { + flex: '1 1 0', + overflow: 'hidden', + userSelect: 'none', +}); + +export const NameContainer = Base; + +export const UsStdContainer = Base; + +export const GradesContainer = Base; + +export const SubjectsContainer = Base; + +export const AddContainer = styled('div', { + width: '100px', +}); + +export const AddButton = styled('button', ({ $on, $theme }: { $on: boolean; $theme: Theme }) => ({ + backgroundColor: $on ? `hsl(0, 100%, 40%)` : 'white', + ':hover': { + backgroundColor: $on ? `hsl(0, 100%, 50%)` : 'rgba(0, 0, 0, 0.05)', + }, + color: $on ? 'white' : $theme.color, + border: `1px solid ${$on ? `hsl(0, 100%, 30%)` : $theme.borderColor}`, + borderRadius: `${$theme.itemPadding * 2}px`, + padding: `${$theme.itemPadding * 2}px`, + transition: 'background-color 0.2s, color 0.2s', + cursor: 'pointer', + width: '100%', + fontWeight: 'bold', +})); \ No newline at end of file diff --git a/src/lms/index.tsx b/src/lms/index.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/lms/plugin/BriefAssignmentView.tsx b/src/lms/plugin/BriefAssignmentView.tsx new file mode 100644 index 00000000..bbb160a8 --- /dev/null +++ b/src/lms/plugin/BriefAssignmentView.tsx @@ -0,0 +1,104 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { State } from 'state'; +import Assignment, { AsyncAssignment } from '../../state/State/Assignment'; +import { styled } from 'styletron-react'; +import LocalizedString from '../../util/LocalizedString'; +import { Theme, ThemeProps } from '../../components/constants/theme'; +import { StyleProps } from '../../util/style'; +import Async from '../../state/State/Async'; +import { GradesContainer, NameContainer, SubjectsContainer, UsStdContainer } from '../common'; +import { FontAwesome } from '../../components/FontAwesome'; +import Markdown from 'react-markdown'; +import { TabBar } from '../../components/Layout/TabBar'; +import Subject from '../../state/State/Assignment/Subject'; + +export interface AssignmentViewProps extends StyleProps { + theme: Theme; + assignment: AsyncAssignment; + locale: LocalizedString.Language; + + onClick?: (e: React.MouseEvent) => void; +} + +const Container = styled('div', ({ $theme, onClick }: { $theme: Theme; onClick: unknown; }) => ({ + width: '100%', + borderRadius: `${$theme.itemPadding * 2}px`, + border: `1px solid ${$theme.borderColor}`, + marginBottom: `${$theme.itemPadding * 2}px`, + ':last-child': { + marginBottom: 0, + }, + // eslint-disable-next-line no-extra-boolean-cast + cursor: !!onClick ? 'pointer' : 'default', + backgroundColor: 'white', + overflow: 'hidden', +})); + +const Header = styled('div', ({ $theme }: { $theme: Theme; }) => ({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + width: '100%', + padding: `${$theme.itemPadding * 2}px`, + borderBottom: 'none', +})); + +const Body = styled('div', ({ $theme }: { $theme: Theme; }) => ({ + width: '100%', + overflow: 'hidden', +})); + +const StyledMarkdown = styled(Markdown, ({ $theme }: { $theme: Theme; }) => ({ + paddingLeft: `${$theme.itemPadding * 2}px`, + paddingRight: `${$theme.itemPadding * 2}px`, +})); + +const StyledTabBar = styled(TabBar, ({ theme }: ThemeProps) => ({ + width: '100%', + borderTop: `1px solid ${theme.borderColor}`, + backgroundColor: 'white' +})); + +export default ({ + style, + className, + theme, + assignment, + locale, + onClick +}: AssignmentViewProps) => { + const common = Async.latestCommon(assignment); + + const gradeLevelRanges = Assignment.gradeLevelRanges(new Set(common.gradeLevels)); + const gradeLevelAbbreviations = gradeLevelRanges.map(([min, max]) => { + if (min === max) LocalizedString.lookup(Assignment.gradeLevelAbbreviatedString(min), locale); + return `${LocalizedString.lookup(Assignment.gradeLevelAbbreviatedString(min), locale)}-${LocalizedString.lookup(Assignment.gradeLevelAbbreviatedString(max), locale)}`; + }); + + const gradeLevelString = gradeLevelAbbreviations.join(', '); + + const latestAssignment = Async.latestValue(assignment); + + return ( + +
+ {LocalizedString.lookup(common.name, locale)} + +
+
+ + {gradeLevelString} + + {common.subjects + .map(sub => LocalizedString.lookup(Subject.toString(sub), locale)).join(', ') + } +
+
+ ); +}; diff --git a/src/lms/plugin/BriefAssignmentsView.tsx b/src/lms/plugin/BriefAssignmentsView.tsx new file mode 100644 index 00000000..96874a80 --- /dev/null +++ b/src/lms/plugin/BriefAssignmentsView.tsx @@ -0,0 +1,91 @@ +import Input from '../../components/interface/Input'; +import * as React from 'react'; +import { StyleProps } from 'util/style'; +import { styled, StyletronComponent, withStyleDeep } from 'styletron-react'; +import { Theme, ThemeProps } from '../../components/constants/theme'; +import LocalizedString from '../../util/LocalizedString'; + +import tr from '@i18n'; +import { AsyncAssignment } from 'state/State/Assignment'; +import Dict from '../../util/objectOps/Dict'; +import Async from '../../state/State/Async'; +import { FontAwesome } from '../../components/FontAwesome'; +import { faCaretDown, faCaretUp } from '@fortawesome/free-solid-svg-icons'; +import StandardsLocation from '../../state/State/Assignment/StandardsLocation'; + +import Color from 'colorjs.io'; +import Subject_ from '../../state/State/Assignment/Subject'; +import construct from '../../util/redux/construct'; +import { sprintf } from 'sprintf-js'; +import BriefAssignmentView from './BriefAssignmentView'; + +export interface AssignmentsViewProps extends ThemeProps, StyleProps { + locale: LocalizedString.Language; + assignments: Dict; + + onAssignmentClick?: (id: string) => void; +} + +const Container = styled('div', ({ $theme }: { $theme: Theme }) => ({ + backgroundColor: $theme.backgroundColor, + width: '100%', + padding: `${$theme.itemPadding * 2}px`, +})); + +const Search = styled(Input, ({ theme }: ThemeProps) => ({ + width: '100%', + marginBottom: `${theme.itemPadding * 2}px`, +})); + +const BriefAssignmentsView = ({ + style, + className, + theme, + locale, + assignments, + onAssignmentClick +}: AssignmentsViewProps) => { + const [filter, setFilter] = React.useState(''); + + const assignmentsList = Dict.toList(assignments).filter(([_, assignment]) => !!Async.latestCommon(assignment)); + + const filteredAssignments = assignmentsList.filter(([_, assignment]) => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const common = Async.latestCommon(assignment)!; + const name = LocalizedString.lookup(common.name, locale); + if (name.toLowerCase().includes(filter.toLowerCase())) return true; + + const latestValue = Async.latestValue(assignment); + if (!latestValue) return false; + + const educatorNotes = LocalizedString.lookup(latestValue.educatorNotes, locale); + if (educatorNotes.toLowerCase().includes(filter.toLowerCase())) return true; + + const studentNotes = LocalizedString.lookup(latestValue.studentNotes, locale); + if (studentNotes.toLowerCase().includes(filter.toLowerCase())) return true; + + return false; + }); + + return ( + + setFilter(e.currentTarget.value)} + placeholder={LocalizedString.lookup(tr('Search'), locale)} + /> + {filteredAssignments.map(([id, assignment]) => ( + onAssignmentClick && onAssignmentClick(id)} + /> + ))} + + ); +}; + +export default BriefAssignmentsView; \ No newline at end of file diff --git a/src/lms/plugin/PluginPage.tsx b/src/lms/plugin/PluginPage.tsx new file mode 100644 index 00000000..1d024a2a --- /dev/null +++ b/src/lms/plugin/PluginPage.tsx @@ -0,0 +1,141 @@ +import { TabBar } from '../../components/Layout/TabBar'; +import * as React from 'react'; +import { RouteComponentProps } from 'react-router'; +import { styled } from 'styletron-react'; +import LocalizedString from '../../util/LocalizedString'; +import { DARK, LIGHT, Theme, ThemeProps } from '../../components/constants/theme'; + +import tr from '@i18n'; +import { faHome, faSchool, faSchoolCircleCheck } from '@fortawesome/free-solid-svg-icons'; +import { connect } from 'react-redux'; +import { State } from '../../state'; +import SearchFilters from '../SearchFilters'; +import AssignmentsView from '../AssignmentsView'; +import { AsyncAssignment } from '../../state/State/Assignment'; +import Dict from '../../util/objectOps/Dict'; +import { FontAwesome } from '../../components/FontAwesome'; +import Async from '../../state/State/Async'; +import Subject from '../../state/State/Assignment/Subject'; +import { PillAreaItem } from '../../components/PillArea'; +import StandardsLocation from '../../state/State/Assignment/StandardsLocation'; +import { UsersAction } from '../../state/reducer'; +import Input from 'components/interface/Input'; +import BriefAssignmentsView from './BriefAssignmentsView'; +import { courseId } from './globals'; + +export interface PluginPagePublicProps { + +} + +export interface PluginPagePrivateProps extends ThemeProps { + locale: LocalizedString.Language; + assignments: Dict; + + userId: string; + myAssignments: Set; + onMyAssignmentsChange: (myAssignments: Set) => void; +} + +const Container = styled('div', ({ $theme }: { $theme: Theme }) => ({ + width: '100%', + display: 'flex', + flexDirection: 'column', + color: $theme.color, + backgroundColor: $theme.backgroundColor, + minHeight: '100vh', +})); + +const Body = styled('div', { + flex: 1, + display: 'flex', + flexDirection: 'row', + '@screen and (max-width: 800px)': { + flexDirection: 'column', + }, +}); + +const TopBar = styled('div', ({ $theme }: { $theme: Theme }) => ({ + width: '100%', + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + borderBottom: `1px solid ${$theme.borderColor}`, + height: '48px', +})); + +const StyledTabBar = styled(TabBar, ({ theme }: ThemeProps) => ({ + flex: 1, + borderTopLeftRadius: `${theme.itemPadding * 2}px`, + borderTopRightRadius: `${theme.itemPadding * 2}px`, + alignSelf: 'end', + borderTop: `1px solid ${theme.borderColor}`, + borderLeft: `1px solid ${theme.borderColor}`, + borderRight: `1px solid ${theme.borderColor}`, + backgroundColor: 'white' +})); + +const TopFa = styled(FontAwesome, ({ $theme }: { $theme: Theme }) => ({ + paddingLeft: `${$theme.itemPadding * 2}px`, + paddingRight: `${$theme.itemPadding * 2}px`, + fontSize: '32px', +})); + +const AddBotballPluginButton = styled('button', ({ $theme }: { $theme: Theme }) => ({ + // Remove all button styles + border: 'none', + background: 'none', + color: 'inherit', + padding: `${$theme.itemPadding * 2}px`, + + margin: `0 ${$theme.itemPadding * 2}px`, + // Add custom styles +})); + + +const PluginPage = ({ + theme, + locale, + assignments, + myAssignments, + onMyAssignmentsChange, + userId +}: PluginPagePublicProps & PluginPagePrivateProps) => { + const [expandedAssignments, setExpandedAssignments] = React.useState>(new Set()); + + return ( + + + { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any + await (gapi.client as any).classroom.courses.courseWork.create({ + courseId, + }); + window.top.postMessage({ type: 'open-assignment', id }, '*'); + }} + /> + + + ); +}; + +export default connect((state: State) => { + let myAssignments = new Set(); + if (state.users.me) { + const latestMe = Async.latestValue(state.users.users[state.users.me]); + if (latestMe && latestMe.myAssignments) myAssignments = new Set(latestMe.myAssignments); + } + return { + theme: LIGHT, + locale: state.i18n.locale, + assignments: state.assignments, + myAssignments + }; +}, (dispatch, ownProps) => ({ + onMyAssignmentsChange: (myAssignments: Set) => dispatch(UsersAction.setMyAssignments({ + assignmentIds: Array.from(myAssignments) + })) +}))(PluginPage) as React.ComponentType; \ No newline at end of file diff --git a/src/lms/plugin/globals.ts b/src/lms/plugin/globals.ts new file mode 100644 index 00000000..623f968f --- /dev/null +++ b/src/lms/plugin/globals.ts @@ -0,0 +1,11 @@ +export let courseId: string; + +export const setCourseId = (id: string) => { + courseId = id; +}; + +export let gapiInitialized = false; + +export const setGapiInitialized = (initialized: boolean) => { + gapiInitialized = initialized; +}; \ No newline at end of file diff --git a/src/lms/plugin/index.tsx b/src/lms/plugin/index.tsx new file mode 100644 index 00000000..a805f1f7 --- /dev/null +++ b/src/lms/plugin/index.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import * as ReactDom from 'react-dom'; + + +import { Provider as StyletronProvider } from "styletron-react"; +import { Client as Styletron } from "styletron-engine-atomic"; + +import { Provider as ReduxProvider } from 'react-redux'; +import store from '../../state'; +import PluginPage from './PluginPage'; +import { courseId, setCourseId, setGapiInitialized } from './globals'; + +const reactRoot = document.getElementById('reactRoot'); + +const engine = new Styletron({ prefix: 'style' }); + + +const initializeGapiClient = async () => { + await gapi.client.init({ + apiKey: 'AIzaSyCuSBzNumuFyeOzAhnSEslWIgdgVqKR7z0', + scope: 'https://www.googleapis.com/auth/classroom.coursework.me', + discoveryDocs: ['https://classroom.googleapis.com/$discovery/rest?version=v1'], + clientId: '705985154569-oeqkm4u0hdhjsa1s3qi05l39eci594sp.apps.googleusercontent.com', + }); + + setGapiInitialized(true); +}; + +gapi.load('client', void initializeGapiClient); + +ReactDom.render( + + + + + , + reactRoot +); + +window.onmessage = (e: MessageEvent<{ type: string; id: string }>) => { + try { + if (e.data.type === 'set-course-id') { + setCourseId(e.data.id); + console.log('set course id', e.data.id); + } + } catch (err) { + console.log(err); + } +}; \ No newline at end of file diff --git a/src/lms/plugin/plugin.html.ejs b/src/lms/plugin/plugin.html.ejs new file mode 100644 index 00000000..e8b25637 --- /dev/null +++ b/src/lms/plugin/plugin.html.ejs @@ -0,0 +1,37 @@ + + + + + + + + + Plugin + + + + + + + +
+ + \ No newline at end of file diff --git a/src/pages/ChallengeRoot.tsx b/src/pages/ChallengeRoot.tsx index 670923d1..35b39454 100644 --- a/src/pages/ChallengeRoot.tsx +++ b/src/pages/ChallengeRoot.tsx @@ -483,7 +483,7 @@ class Root extends React.Component { : latestChallenge.defaultLanguage; } - private get code(): { [language in ProgrammingLanguage]: string } { + private get code(): { [language in ProgrammingLanguage]?: string } { const { challenge, challengeCompletion } = this.props; const latestChallengeCompletion = Async.latestValue(challengeCompletion); const latestChallenge = Async.latestValue(challenge); diff --git a/src/pages/Root.tsx b/src/pages/Root.tsx index cac6059e..f564e8ed 100644 --- a/src/pages/Root.tsx +++ b/src/pages/Root.tsx @@ -138,6 +138,7 @@ interface RootState { windowInnerHeight: number; + miniEditor: boolean; } @@ -181,6 +182,7 @@ class Root extends React.Component { 'c': window.localStorage.getItem('code-c') || ProgrammingLanguage.DEFAULT_CODE['c'], 'cpp': window.localStorage.getItem('code-cpp') || ProgrammingLanguage.DEFAULT_CODE['cpp'], 'python': window.localStorage.getItem('code-python') || ProgrammingLanguage.DEFAULT_CODE['python'], + 'scratch': window.localStorage.getItem('code-scratch') || ProgrammingLanguage.DEFAULT_CODE['scratch'], }, modal: Modal.NONE, simulatorState: SimulatorState.STOPPED, @@ -190,6 +192,7 @@ class Root extends React.Component { settings: DEFAULT_SETTINGS, feedback: DEFAULT_FEEDBACK, windowInnerHeight: window.innerHeight, + miniEditor: true }; @@ -409,6 +412,17 @@ class Root extends React.Component { }); break; } + case 'scratch': { + this.setState({ + simulatorState: SimulatorState.RUNNING, + }, () => { + WorkerInstance.start({ + language: 'scratch', + code: activeCode + }); + }); + break; + } } @@ -569,6 +583,12 @@ class Root extends React.Component { this.props.onSaveScene(this.props.match.params.sceneId); }; + private onMiniEditorToggle_ = () => { + this.setState({ + miniEditor: !this.state.miniEditor + }); + }; + render() { const { props, state } = this; @@ -601,6 +621,7 @@ class Root extends React.Component { settings, feedback, windowInnerHeight, + miniEditor } = state; @@ -612,6 +633,8 @@ class Root extends React.Component { language: activeLanguage, onCodeChange: this.onCodeChange_, onLanguageChange: this.onActiveLanguageChange_, + mini: miniEditor, + onMiniClick: this.onMiniEditorToggle_ }; const commonLayoutProps: LayoutProps = { @@ -679,7 +702,7 @@ class Root extends React.Component { onAboutClick={this.onModalClick_(Modal.ABOUT)} onResetWorldClick={this.onResetWorldClick_} onStartChallengeClick={this.onStartChallengeClick_} - onRunClick={this.onRunClick_} + onRunClick={code[activeLanguage].length > 0 ? this.onRunClick_ : undefined} onStopClick={this.onStopClick_} onDocumentationClick={onDocumentationClick} onDashboardClick={this.onDashboardClick} diff --git a/src/programming/WorkerInstance.ts b/src/programming/WorkerInstance.ts index a45a23db..0f56c69a 100644 --- a/src/programming/WorkerInstance.ts +++ b/src/programming/WorkerInstance.ts @@ -6,9 +6,38 @@ import SharedRingBufferUtf32 from './buffers/SharedRingBufferUtf32'; import SharedRegistersRobot from './SharedRegistersRobot'; import AbstractRobot from './AbstractRobot'; import WriteCommand from './AbstractRobot/WriteCommand'; +import Dict from 'util/objectOps/Dict'; const SHARED_CONSOLE_LENGTH = 1024; +class SharedVariablesStateMachine { + constructor(private sharedVariables_: SharedRingBufferUtf32) {} + + private length_: number = undefined; + private buffer_ = ''; + + private watchedVariables_: Dict = {}; + + update() { + this.buffer_ = this.sharedVariables_.popString(); + + if (this.length_ === undefined && this.buffer_.indexOf(';') >= 0) { + const [lengthStr, ...rest] = this.buffer_.split(';'); + this.length_ = parseInt(lengthStr); + this.buffer_ = rest.join(';'); + } + + if (this.length_ !== undefined && this.buffer_.length >= this.length_) { + const json = this.buffer_.slice(0, this.length_); + this.buffer_ = this.buffer_.slice(this.length_); + this.length_ = undefined; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + this.watchedVariables_ = JSON.parse(json); + } + } + + get watchedVariables() { return this.watchedVariables_; } +} /** * Represents an instance of a worker that interfaces with the sharedRegistersRobot. @@ -22,6 +51,8 @@ class WorkerInstance implements AbstractRobot { // Shared registers and console buffer. private sharedRegisters_ = new SharedRegisters(); private sharedConsole_ = SharedRingBufferUtf32.create(SHARED_CONSOLE_LENGTH); + private sharedVariables_ = SharedRingBufferUtf32.create(2048); + private sharedVariablesStateMachine_ = new SharedVariablesStateMachine(this.sharedVariables_); private createSerial_ = SerialU32.create(1024); private sharedRegistersRobot_: SharedRegistersRobot; @@ -102,6 +133,9 @@ class WorkerInstance implements AbstractRobot { */ get sharedConsole() { return this.sharedConsole_; } + private watchedVariables_: Dict = {}; + get watchedVariables() { return this.watchedVariables_; } + /** * Internal method to handle the 'stopped' event. * Resets specific registers and triggers the onStopped event if defined. @@ -144,6 +178,10 @@ class WorkerInstance implements AbstractRobot { SerialU32.flip(this.createSerial_) ) ); + this.worker_.postMessage({ + type: 'set-shared-variables', + sharedArrayBuffer: this.sharedVariables_.sharedArrayBuffer, + } as Protocol.Worker.SetSharedVariablesRequest); break; } case 'stopped': { diff --git a/src/programming/compiler/ProgrammingLanguage.ts b/src/programming/compiler/ProgrammingLanguage.ts index 590ac439..5b6e37b1 100644 --- a/src/programming/compiler/ProgrammingLanguage.ts +++ b/src/programming/compiler/ProgrammingLanguage.ts @@ -1,17 +1,19 @@ -type ProgrammingLanguage = 'c' | 'cpp' | 'python'; +type ProgrammingLanguage = 'c' | 'cpp' | 'python' | 'scratch'; namespace ProgrammingLanguage { export const FILE_EXTENSION: { [key in ProgrammingLanguage]: string } = { c: 'c', cpp: 'cpp', - python: 'py' + python: 'py', + scratch: 'scratch' }; export const DEFAULT_CODE: { [key in ProgrammingLanguage]: string } = { c: '#include \n#include \n\nint main()\n{\n printf("Hello, World!\\n");\n return 0;\n}\n', cpp: '#include \n#include \n\nint main()\n{\n std::cout << "Hello, World!" << std::endl;\n return 0;\n}\n', - python: 'from kipr import *\n\nprint(\'Hello, World!\')' + python: 'from kipr import *\n\nprint(\'Hello, World!\')', + scratch: '' }; } diff --git a/src/programming/worker/Protocol.ts b/src/programming/worker/Protocol.ts index 20782487..48c0e192 100644 --- a/src/programming/worker/Protocol.ts +++ b/src/programming/worker/Protocol.ts @@ -22,6 +22,7 @@ export namespace Protocol { SetSharedRegistersRequest | SetCreateSerialRequest | SetSharedConsoleRequest | + SetSharedVariablesRequest | ProgramOutputRequest | ProgramErrorRequest | WorkerReadyRequest | @@ -49,6 +50,11 @@ export namespace Protocol { rx: SharedArrayBuffer; } + export interface SetSharedVariablesRequest { + type: 'set-shared-variables'; + sharedArrayBuffer: SharedArrayBuffer; + } + export namespace SetCreateSerialRequest { export const fromSerialU32 = (serial: SerialU32): SetCreateSerialRequest => ({ type: 'set-create-serial', diff --git a/src/programming/worker/worker.ts b/src/programming/worker/worker.ts index c0a9b186..ae45811b 100644 --- a/src/programming/worker/worker.ts +++ b/src/programming/worker/worker.ts @@ -1,3 +1,12 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ + + + import Protocol from './Protocol'; import dynRequire from '../compiler/require'; import SharedRegisters from '../registers/SharedRegisters'; @@ -5,7 +14,170 @@ import python from '../python'; import SharedRingBufferUtf32 from '../buffers/SharedRingBufferUtf32'; import SerialU32 from '../buffers/SerialU32'; import SharedRingBufferU32 from '../buffers/SharedRingBufferU32'; +import Instance, { DispatchContext, InstanceError } from 'itch/src/Instance'; +import control from 'itch/src/module/control'; +import data from 'itch/src/module/data'; +import operator from 'itch/src/module/operator'; + +import { DOMParser } from 'xmldom'; +import { toNumber } from 'itch/src/util'; + +const resolveNumber = (ctx: DispatchContext, name: string) => toNumber(ctx.resolveValue(name)); + +const motor = (rt: any) => ({ + motor: (ctx: DispatchContext) => { + rt._motor( + toNumber(ctx.resolveValue('MOTOR')), + toNumber(ctx.resolveValue('PERCENT')) + ); + }, + fd: (ctx: DispatchContext) => rt._fd(resolveNumber(ctx, 'MOTOR')), + bk: (ctx: DispatchContext) => rt._bk(resolveNumber(ctx, 'MOTOR')), + off: (ctx: DispatchContext) => rt._off(resolveNumber(ctx, 'MOTOR')), + ao: (ctx: DispatchContext) => rt._ao(), + alloff: (ctx: DispatchContext) => rt._alloff(), + gmpc: (ctx: DispatchContext) => rt._gmpc(resolveNumber(ctx, 'MOTOR')), + get_motor_position_counter: (ctx: DispatchContext) => rt._get_motor_position_counter( + resolveNumber(ctx, 'MOTOR') + ), + freeze: (ctx: DispatchContext) => rt._freeze(resolveNumber(ctx, 'MOTOR')), + cmpc: (ctx: DispatchContext) => rt._cmpc( + resolveNumber(ctx, 'MOTOR') + ), + clear_motor_position_counter: (ctx: DispatchContext) => rt._clear_motor_position_counter( + resolveNumber(ctx, 'MOTOR') + ), + mav: (ctx: DispatchContext) => rt._mav( + resolveNumber(ctx, 'MOTOR'), + resolveNumber(ctx, 'VELOCITY') + ), + move_at_velocity: (ctx: DispatchContext) => rt._move_at_velocity( + resolveNumber(ctx, 'MOTOR'), + resolveNumber(ctx, 'VELOCITY') + ), + mtp: (ctx: DispatchContext) => rt._mtp( + resolveNumber(ctx, 'MOTOR'), + resolveNumber(ctx, 'SPEED'), + resolveNumber(ctx, 'GOAL_POS') + ), + move_to_position: (ctx: DispatchContext) => rt._move_to_position( + resolveNumber(ctx, 'MOTOR'), + resolveNumber(ctx, 'SPEED'), + resolveNumber(ctx, 'GOAL_POS') + ), + mrp: (ctx: DispatchContext) => rt._mrp( + resolveNumber(ctx, 'MOTOR'), + resolveNumber(ctx, 'SPEED'), + resolveNumber(ctx, 'DELTA_POS') + ), + move_relative_position: (ctx: DispatchContext) => rt._move_relative_position( + resolveNumber(ctx, 'MOTOR'), + resolveNumber(ctx, 'SPEED'), + resolveNumber(ctx, 'DELTA_POS') + ), + get_motor_done: (ctx: DispatchContext) => rt._get_motor_done( + resolveNumber(ctx, 'MOTOR') + ), + block_motor_done: (ctx: DispatchContext) => rt._block_motor_done( + resolveNumber(ctx, 'MOTOR') + ), + bmd: (ctx: DispatchContext) => rt._bmd( + resolveNumber(ctx, 'MOTOR') + ), + setpwm: (ctx: DispatchContext) => rt._setpwm( + resolveNumber(ctx, 'MOTOR'), + resolveNumber(ctx, 'PERCENT') + ), + getpwm: (ctx: DispatchContext) => rt._getpwm( + resolveNumber(ctx, 'MOTOR') + ), + baasbennaguui: (ctx: DispatchContext) => rt._baasbennaguui( + resolveNumber(ctx, 'MOTOR'), + resolveNumber(ctx, 'PERCENT') + ), + motor_power: (ctx: DispatchContext) => rt._motor_power( + resolveNumber(ctx, 'MOTOR'), + resolveNumber(ctx, 'PERCENT') + ), +}); + +const time = (rt: any) => ({ + msleep: (ctx: DispatchContext) => rt._msleep( + resolveNumber(ctx, 'MSECS') + ), + systime: (ctx: DispatchContext) => rt._systime(), + seconds: (ctx: DispatchContext) => rt._seconds(), +}); + +const wait_for = (rt: any) => ({ + wait_for_milliseconds: (ctx: DispatchContext) => rt._wait_for_milliseconds( + resolveNumber(ctx, 'MSECS') + ), + wait_for_touch: (ctx: DispatchContext) => rt._wait_for_touch( + resolveNumber(ctx, 'PORT') + ), +}); +const servo = (rt: any) => ({ + enable_servos: (ctx: DispatchContext) => rt._enable_servos(), + disable_servos: (ctx: DispatchContext) => rt._disable_servos(), + enable_servo: (ctx: DispatchContext) => rt._enable_servo(resolveNumber(ctx, 'PORT')), + disable_servo: (ctx: DispatchContext) => rt._disable_servo(resolveNumber(ctx, 'PORT')), + set_servo_position: (ctx: DispatchContext) => rt._set_servo_position( + resolveNumber(ctx, 'PORT'), + resolveNumber(ctx, 'POSITION') + ), + get_servo_position: (ctx: DispatchContext) => rt._get_servo_position( + resolveNumber(ctx, 'PORT') + ), + get_servo_enabled: (ctx: DispatchContext) => rt._get_servo_enabled( + resolveNumber(ctx, 'PORT') + ), + set_servo_enabled: (ctx: DispatchContext) => rt._set_servo_enabled( + resolveNumber(ctx, 'PORT'), + resolveNumber(ctx, 'ENABLED') + ), +}); + +const analog = (rt: any) => ({ + analog: (ctx: DispatchContext) => rt._analog(resolveNumber(ctx, 'PORT')), + analog8: (ctx: DispatchContext) => rt._analog8(resolveNumber(ctx, 'PORT')), + analog10: (ctx: DispatchContext) => rt._analog10(resolveNumber(ctx, 'PORT')), + analog12: (ctx: DispatchContext) => rt._analog12(resolveNumber(ctx, 'PORT')), + analog_et: (ctx: DispatchContext) => rt._analog_et(resolveNumber(ctx, 'PORT')), + set_analog_pullup: (ctx: DispatchContext) => rt._set_analog_pullup( + resolveNumber(ctx, 'PORT'), + resolveNumber(ctx, 'PULLUP') + ), + get_analog_pullup: (ctx: DispatchContext) => rt._get_analog_pullup( + resolveNumber(ctx, 'PORT') + ), +}); + +const digital = (rt: any) => ({ + digital: (ctx: DispatchContext) => rt._digital(resolveNumber(ctx, 'PORT')), + set_digital_pullup: (ctx: DispatchContext) => rt._set_digital_pullup( + resolveNumber(ctx, 'PORT'), + resolveNumber(ctx, 'PULLUP') + ), + get_digital_pullup: (ctx: DispatchContext) => rt._get_digital_pullup( + resolveNumber(ctx, 'PORT') + ), + set_digital_output: (ctx: DispatchContext) => rt._set_digital_output( + resolveNumber(ctx, 'PORT'), + resolveNumber(ctx, 'OUT') + ), + get_digital_output: (ctx: DispatchContext) => rt._get_digital_output( + resolveNumber(ctx, 'PORT') + ), + set_digital_value: (ctx: DispatchContext) => rt._set_digital_value( + resolveNumber(ctx, 'PORT'), + resolveNumber(ctx, 'VALUE') + ), + get_digital_value: (ctx: DispatchContext) => rt._get_digital_value( + resolveNumber(ctx, 'PORT') + ), +}); // Proper typing of Worker is tricky due to conflicting DOM and WebWorker types // See GitHub issue: https://github.com/microsoft/TypeScript/issues/20595 @@ -16,6 +188,7 @@ const ctx: Worker = self as unknown as Worker; let sharedRegister_: SharedRegisters; let createSerial_: SerialU32; let sharedConsole_: SharedRingBufferUtf32; +let sharedVariables_: SharedRingBufferUtf32; /** * Prints a string to the shared console buffer, followed by a newline. @@ -137,6 +310,114 @@ const startPython = async (message: Protocol.Worker.StartRequest) => { }; +let cachedRt: string; + +const startScratch = async (message: Protocol.Worker.StartRequest) => { + if (cachedRt === undefined) { + const res = await fetch('/scratch/rt.js', { + + }); + cachedRt = await res.text(); + } + + let stoppedSent = false; + + const sendStopped = () => { + if (stoppedSent) return; + + ctx.postMessage({ + type: 'stopped', + } as Protocol.Worker.StoppedRequest); + stoppedSent = true; + }; + + const mod = dynRequire(cachedRt, { + setRegister8b: (address: number, value: number) => sharedRegister_.setRegister8b(address, value), + setRegister16b: (address: number, value: number) => sharedRegister_.setRegister16b(address, value), + setRegister32b: (address: number, value: number) => sharedRegister_.setRegister32b(address, value), + readRegister8b: (address: number) => sharedRegister_.getRegisterValue8b(address), + readRegister16b: (address: number) => sharedRegister_.getRegisterValue16b(address), + readRegister32b: (address: number) => sharedRegister_.getRegisterValue32b(address), + onStop: sendStopped + }, + print, + printErr + ); + + mod.onRuntimeInitialized = () => { + ctx.postMessage({ + type: 'start' + }); + + try { + mod._main(); + } catch (e: unknown) { + if (ExitStatusError.isExitStatusError(e)) { + print(`Program exited with status code ${e.status}`); + } else if (e instanceof Error) { + printErr(e.message); + } else { + printErr(`Program exited with an unknown error`); + } + return; + } + + const watchedVariables: Set = new Set(); + + let lastTick = Date.now(); + try { + const instance = new Instance({ + source: new DOMParser().parseFromString(message.code, "text/xml"), + show: (ctx, name) => { + watchedVariables.add(name); + print(`Variable ${name} = ${ctx.heap.get(name)}`); + }, + hide: (ctx, name) => { + watchedVariables.delete(name); + }, + tick: (ctx) => { + const now = Date.now(); + // ~10 Hz + if (now - lastTick < 100) return; + lastTick = now; + for (const name of watchedVariables) { + print(`Variable ${name} = ${ctx.heap.get(name)}`); + } + }, + modules: { + control, + data, + operator, + motor: motor(mod), + time: time(mod), + wait_for: wait_for(mod), + servo: servo(mod), + digital: digital(mod), + analog: analog(mod), + } + }); + + instance.run(); + } catch (e) { + console.error(e); + let last = e; + while (InstanceError.is(e)) { + // eslint-disable-next-line no-ex-assign + e = e.original; + last = e; + } + + if (InstanceError.is(last)) { + printErr(`${last.module}/${last.function}: ${e.name}: ${e.message}`); + } else { + printErr(e.message); + } + } finally { + sendStopped(); + } + }; +}; + /** * Initiates the execution of code based on the specified language. * @param message - Message containing the code, language, and other details. @@ -160,6 +441,10 @@ const start = async (message: Protocol.Worker.StartRequest) => { } break; } + case 'scratch': { + void startScratch(message); + break; + } } }; @@ -184,6 +469,11 @@ ctx.onmessage = (e: MessageEvent) => { tx: new SharedRingBufferU32(message.tx), rx: new SharedRingBufferU32(message.rx) }; + break; + } + case 'set-shared-variables': { + sharedVariables_ = new SharedRingBufferUtf32(message.sharedArrayBuffer); + break; } } }; diff --git a/src/simulator/babylonBindings/sensors/TouchSensor.ts b/src/simulator/babylonBindings/sensors/TouchSensor.ts index 361bd59a..3465c6a3 100644 --- a/src/simulator/babylonBindings/sensors/TouchSensor.ts +++ b/src/simulator/babylonBindings/sensors/TouchSensor.ts @@ -21,7 +21,7 @@ class TouchSensor extends SensorObject { const idCamel = id.replace(/_([a-z])/g, g => g[1].toUpperCase()); for (const collider of colliders) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any const collider_name = (collider as any)?.name; // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call if (collider_name.includes(idCamel)) { diff --git a/src/state/State/Assignment/Asset.ts b/src/state/State/Assignment/Asset.ts new file mode 100644 index 00000000..05e41b01 --- /dev/null +++ b/src/state/State/Assignment/Asset.ts @@ -0,0 +1,82 @@ +namespace Asset { + export enum Type { + Image = 'image', + Video = 'video', + Audio = 'audio', + Document = 'document', + Link = 'link', + } + + interface Base { + id: string; + } + + export interface Image extends Base { + type: Type.Image; + format: Image.Format; + } + + export namespace Image { + export enum Format { + Png = 'png', + Jpeg = 'jpeg', + Gif = 'gif', + } + } + + export interface Video extends Base { + type: Type.Video; + format: Video.Format; + } + + export namespace Video { + export enum Format { + Mp4 = 'mp4', + Webm = 'webm', + } + } + + export interface Audio extends Base { + type: Type.Audio; + format: Audio.Format; + } + + export namespace Audio { + export enum Format { + Mp3 = 'mp3', + Ogg = 'ogg', + } + } + + export interface Document extends Base { + type: Type.Document; + format: Document.Format; + } + + export namespace Document { + export enum Format { + Pdf = 'pdf', + Doc = 'doc', + Docx = 'docx', + Odt = 'odt', + Rtf = 'rtf', + Txt = 'txt', + Markdown = 'markdown', + } + } + + export interface Link extends Base { + type: Type.Link; + url: string; + } +} + +type Asset = ( + Asset.Image | + Asset.Video | + Asset.Audio | + Asset.Document | + Asset.Link +); + +export default Asset; \ No newline at end of file diff --git a/src/state/State/Assignment/StandardsLocation.ts b/src/state/State/Assignment/StandardsLocation.ts new file mode 100644 index 00000000..ff9b915c --- /dev/null +++ b/src/state/State/Assignment/StandardsLocation.ts @@ -0,0 +1,110 @@ +enum StandardsLocation { + UnitedStates = 'United States', + Alabama = 'Alabama', + Alaska = 'Alaska', + Arizona = 'Arizona', + Arkansas = 'Arkansas', + California = 'California', + Colorado = 'Colorado', + Connecticut = 'Connecticut', + Delaware = 'Delaware', + Florida = 'Florida', + Georgia = 'Georgia', + Hawaii = 'Hawaii', + Idaho = 'Idaho', + Illinois = 'Illinois', + Indiana = 'Indiana', + Iowa = 'Iowa', + Kansas = 'Kansas', + Kentucky = 'Kentucky', + Louisiana = 'Louisiana', + Maine = 'Maine', + Maryland = 'Maryland', + Massachusetts = 'Massachusetts', + Michigan = 'Michigan', + Minnesota = 'Minnesota', + Mississippi = 'Mississippi', + Missouri = 'Missouri', + Montana = 'Montana', + Nebraska = 'Nebraska', + Nevada = 'Nevada', + NewHampshire = 'New Hampshire', + NewJersey = 'New Jersey', + NewMexico = 'New Mexico', + NewYork = 'New York', + NorthCarolina = 'North Carolina', + NorthDakota = 'North Dakota', + Ohio = 'Ohio', + Oklahoma = 'Oklahoma', + Oregon = 'Oregon', + Pennsylvania = 'Pennsylvania', + RhodeIsland = 'Rhode Island', + SouthCarolina = 'South Carolina', + SouthDakota = 'South Dakota', + Tennessee = 'Tennessee', + Texas = 'Texas', + Utah = 'Utah', + Vermont = 'Vermont', + Virginia = 'Virginia', + Washington = 'Washington', + WestVirginia = 'West Virginia', + Wisconsin = 'Wisconsin', + Wyoming = 'Wyoming', +} + +namespace StandardsLocation { + export const UNITED_STATES_TERRITORIES: Set = new Set([ + StandardsLocation.Alabama, + StandardsLocation.Alaska, + StandardsLocation.Arizona, + StandardsLocation.Arkansas, + StandardsLocation.California, + StandardsLocation.Colorado, + StandardsLocation.Connecticut, + StandardsLocation.Delaware, + StandardsLocation.Florida, + StandardsLocation.Georgia, + StandardsLocation.Hawaii, + StandardsLocation.Idaho, + StandardsLocation.Illinois, + StandardsLocation.Indiana, + StandardsLocation.Iowa, + StandardsLocation.Kansas, + StandardsLocation.Kentucky, + StandardsLocation.Louisiana, + StandardsLocation.Maine, + StandardsLocation.Maryland, + StandardsLocation.Massachusetts, + StandardsLocation.Michigan, + StandardsLocation.Minnesota, + StandardsLocation.Mississippi, + StandardsLocation.Missouri, + StandardsLocation.Montana, + StandardsLocation.Nebraska, + StandardsLocation.Nevada, + StandardsLocation.NewHampshire, + StandardsLocation.NewJersey, + StandardsLocation.NewMexico, + StandardsLocation.NewYork, + StandardsLocation.NorthCarolina, + StandardsLocation.NorthDakota, + StandardsLocation.Ohio, + StandardsLocation.Oklahoma, + StandardsLocation.Oregon, + StandardsLocation.Pennsylvania, + StandardsLocation.RhodeIsland, + StandardsLocation.SouthCarolina, + StandardsLocation.SouthDakota, + StandardsLocation.Tennessee, + StandardsLocation.Texas, + StandardsLocation.Utah, + StandardsLocation.Vermont, + StandardsLocation.Virginia, + StandardsLocation.Washington, + StandardsLocation.WestVirginia, + StandardsLocation.Wisconsin, + StandardsLocation.Wyoming, + ]); +} + +export default StandardsLocation; \ No newline at end of file diff --git a/src/state/State/Assignment/Subject.ts b/src/state/State/Assignment/Subject.ts new file mode 100644 index 00000000..10380370 --- /dev/null +++ b/src/state/State/Assignment/Subject.ts @@ -0,0 +1,25 @@ +import LocalizedString from '../../../util/LocalizedString'; + +import tr from '@i18n'; + +enum Subject { + Science, + Technology, + Engineering, + Arts, + Mathematics, +} + +namespace Subject { + export const toString = (subject: Subject): LocalizedString => { + switch (subject) { + case Subject.Science: return tr('Science'); + case Subject.Technology: return tr('Technology'); + case Subject.Engineering: return tr('Engineering'); + case Subject.Arts: return tr('Arts'); + case Subject.Mathematics: return tr('Mathematics'); + } + }; +} + +export default Subject; \ No newline at end of file diff --git a/src/state/State/Assignment/index.ts b/src/state/State/Assignment/index.ts new file mode 100644 index 00000000..49b64577 --- /dev/null +++ b/src/state/State/Assignment/index.ts @@ -0,0 +1,111 @@ +import Author from '../../../db/Author'; +import LocalizedString from '../../../util/LocalizedString'; +import Subject from './Subject'; + +import tr from '@i18n'; +import Dict from '../../../util/objectOps/Dict'; +import Async from '../Async'; +import StandardsLocation from './StandardsLocation'; + +interface Assignment { + author: Author; + + name: LocalizedString; + + standardsAligned: boolean; + standardsConformance: StandardsLocation[]; + + gradeLevels: number[]; + + subjects: Subject[]; + + educatorNotes: LocalizedString; + studentNotes: LocalizedString; + + assets: Dict; +} + +export type AssignmentBrief = Pick; + +export namespace AssignmentBrief { + export const fromAssignment = ({ + author, + name, + gradeLevels, + subjects, + standardsConformance, + standardsAligned + }: Assignment): AssignmentBrief => ({ + author, + name, + gradeLevels, + subjects, + standardsConformance, + standardsAligned + }); +} + +namespace Assignment { + /** + * Convert a grade level number to a string. 0 is kindergarten, 1 is first grade, etc. + * @param gradeLevel The grade level + * @returns The string + */ + export const gradeLevelString = (gradeLevel: number) => { + switch (gradeLevel) { + case 0: return tr('Kindergarten', 'Student grade level'); + case 1: return tr('1st grade', 'Student grade level'); + case 2: return tr('2nd grade', 'Student grade level'); + case 3: return tr('3rd grade', 'Student grade level'); + case 4: return tr('4th grade', 'Student grade level'); + case 5: return tr('5th grade', 'Student grade level'); + case 6: return tr('6th grade', 'Student grade level'); + case 7: return tr('7th grade', 'Student grade level'); + case 8: return tr('8th grade', 'Student grade level'); + case 9: return tr('9th grade', 'Student grade level'); + case 10: return tr('10th grade', 'Student grade level'); + case 11: return tr('11th grade', 'Student grade level'); + case 12: return tr('12th grade', 'Student grade level'); + } + }; + + export const gradeLevelAbbreviatedString = (gradeLevel: number) => { + switch (gradeLevel) { + case 0: return tr('K', 'Student grade level; Kingergarten'); + case 1: return tr('1st', 'Student grade level'); + case 2: return tr('2nd', 'Student grade level'); + case 3: return tr('3rd', 'Student grade level'); + case 4: return tr('4th', 'Student grade level'); + case 5: return tr('5th', 'Student grade level'); + case 6: return tr('6th', 'Student grade level'); + case 7: return tr('7th', 'Student grade level'); + case 8: return tr('8th', 'Student grade level'); + case 9: return tr('9th', 'Student grade level'); + case 10: return tr('10th', 'Student grade level'); + case 11: return tr('11th', 'Student grade level'); + case 12: return tr('12th', 'Student grade level'); + } + }; + + export const gradeLevelRanges = (gradeLevels: Set): [number, number][] => { + const sorted = Array.from(gradeLevels).sort((a, b) => a - b); + const ranges: [number, number][] = []; + let start = sorted[0]; + let end = start; + for (let i = 1; i < sorted.length; i++) { + if (sorted[i] === end + 1) { + end = sorted[i]; + } else { + ranges.push([start, end]); + start = sorted[i]; + end = start; + } + } + ranges.push([start, end]); + return ranges; + }; +} + +export type AsyncAssignment = Async; + +export default Assignment; diff --git a/src/state/State/Async.ts b/src/state/State/Async.ts index 0f544c3b..610e47ec 100644 --- a/src/state/State/Async.ts +++ b/src/state/State/Async.ts @@ -273,6 +273,24 @@ namespace Async { } }; + export const latestCommon = (async: Async): B | T => { + if (!async) return undefined; + + switch (async.type) { + case Type.Unloaded: return async.brief; + case Type.Creating: return async.value; + case Type.CreateFailed: return async.value ; + case Type.Loading: return async.brief; + case Type.LoadFailed: return async.brief; + case Type.Loaded: return async.value; + case Type.Saveable: return async.value; + case Type.Saving: return async.value; + case Type.SaveFailed: return async.value; + case Type.Deleting: return async.value; + case Type.DeleteFailed: return async.value; + } + }; + export const isResident = (async: Async): boolean => { if (!async) return false; diff --git a/src/state/State/Challenge/index.ts b/src/state/State/Challenge/index.ts index 22d8461c..39e5b73a 100644 --- a/src/state/State/Challenge/index.ts +++ b/src/state/State/Challenge/index.ts @@ -11,7 +11,7 @@ interface Challenge { description: LocalizedString; author: Author; - code: { [language in ProgrammingLanguage]: string }; + code: { [language in ProgrammingLanguage]?: string }; defaultLanguage: ProgrammingLanguage; events: Dict; diff --git a/src/state/State/ChallengeCompletion/index.ts b/src/state/State/ChallengeCompletion/index.ts index 0416c103..47dc7ab8 100644 --- a/src/state/State/ChallengeCompletion/index.ts +++ b/src/state/State/ChallengeCompletion/index.ts @@ -6,7 +6,7 @@ import ProgrammingLanguage from '../../../programming/compiler/ProgrammingLangua import { ReferenceFramewUnits } from '../../../util/math/unitMath'; interface ChallengeCompletion { - code: { [language in ProgrammingLanguage]: string }; + code: { [language in ProgrammingLanguage]?: string }; currentLanguage: ProgrammingLanguage; serializedSceneDiff: string; robotLinkOrigins?: Dict>; @@ -18,9 +18,9 @@ interface ChallengeCompletion { namespace ChallengeCompletion { export const EMPTY: ChallengeCompletion = { code: { - 'c': '', - 'cpp': '', - 'python': '', + c: '', + cpp: '', + python: '', }, currentLanguage: 'c', serializedSceneDiff: JSON.stringify({ t: 'o' }), diff --git a/src/state/State/User/index.ts b/src/state/State/User/index.ts new file mode 100644 index 00000000..e4f76d02 --- /dev/null +++ b/src/state/State/User/index.ts @@ -0,0 +1,28 @@ +import Async from '../Async'; + +export interface CurriculumAccess { + /** A ISO 8601 timestamp of when the curriculum access begins. */ + startDate: string; + /** A ISO 8601 timestamp of when the curriculum access ends. */ + endDate: string; +} + +interface User { + name?: string; + cirriculumAccess?: CurriculumAccess; + myAssignments?: string[]; +} + +namespace User { + export const DEFAULT: User = { + name: undefined, + cirriculumAccess: undefined, + myAssignments: undefined, + }; +} + +export type UserBrief = Pick; + +export type AsyncUser = Async; + +export default User; \ No newline at end of file diff --git a/src/state/State/index.ts b/src/state/State/index.ts index 6be7fc3c..909dbe1c 100644 --- a/src/state/State/index.ts +++ b/src/state/State/index.ts @@ -7,7 +7,37 @@ import { AsyncChallengeCompletion } from './ChallengeCompletion'; import Documentation from './Documentation'; import DocumentationLocation from './Documentation/DocumentationLocation'; import Robot from './Robot'; -import { AsyncScene } from './Scene'; +import Scene, { AsyncScene } from './Scene'; +import User, { AsyncUser } from './User'; +import { AsyncAssignment } from './Assignment'; + +import tr from '@i18n'; +import Author from '../../db/Author'; +import Subject from './Assignment/Subject'; +import StandardsLocation from './Assignment/StandardsLocation'; + +export type Assignments = Dict; + +export namespace Assignments { + export const EMPTY: Assignments = {}; + + export const TEST: Assignments = { + 'test': Async.loaded({ + value: { + name: tr('Test Assignment'), + description: tr('This is a test assignment.'), + author: Author.organization('kipr'), + assets: {}, + educatorNotes: tr('Educator notes'), + studentNotes: tr('Student notes'), + gradeLevels: [0, 1, 2, 3, 4, 7, 8, 9, 11, 12], + subjects: [Subject.Science], + standardsAligned: true, + standardsConformance: [StandardsLocation.Alaska, StandardsLocation.Oklahoma], + } + }), + }; +} export type Scenes = Dict; @@ -65,4 +95,15 @@ export namespace DocumentationState { export interface I18n { locale: LocalizedString.Language; +} + +export interface Users { + me?: string; + users: Dict; +} + +export namespace Users { + export const EMPTY: Users = { + users: {}, + }; } \ No newline at end of file diff --git a/src/state/index.ts b/src/state/index.ts index db876de0..429cc0e8 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -3,11 +3,10 @@ import { connectRouter, RouterState, routerMiddleware } from 'connected-react-ro import { createBrowserHistory } from 'history'; import * as reducer from './reducer'; -import { DocumentationState, ChallengeCompletions, Challenges, I18n, - Robots, Scenes } from './State'; - -import { CHALLENGE_COLLECTION, CHALLENGE_COMPLETION_COLLECTION, - SCENE_COLLECTION } from '../db/constants'; +import { DocumentationState, ChallengeCompletions, Challenges, I18n, Robots, Scenes, Assignments, Users } from './State'; +// import history from './history'; +import { AsyncScene } from './State/Scene'; +import { CHALLENGE_COLLECTION, CHALLENGE_COMPLETION_COLLECTION, SCENE_COLLECTION, ASSIGNMENT_COLLECTION, USER_COLLECTION } from '../db/constants'; import Record from '../db/Record'; import Selector from '../db/Selector'; @@ -27,6 +26,8 @@ export default createStore(combineReducers({ challenges: reducer.reduceChallenges, challengeCompletions: reducer.reduceChallengeCompletions, i18n: reducer.reduceI18n, + assignments: reducer.reduceAssignments, + users: reducer.reduceUsers }), composeEnhancers( applyMiddleware( routerMiddleware((history)) @@ -43,6 +44,8 @@ export interface State { documentation: DocumentationState; router: RouterState; i18n: I18n; + assignments: Assignments; + users: Users; } export namespace State { @@ -63,6 +66,17 @@ export namespace State { id: selector.id, value: state.challengeCompletions[selector.id] }; + case USER_COLLECTION: return { + type: Record.Type.User, + id: selector.id, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + value: state.users[selector.id] + }; + case ASSIGNMENT_COLLECTION: return { + type: Record.Type.Assignment, + id: selector.id, + value: state.assignments[selector.id] + }; } return undefined; diff --git a/src/state/reducer/assignments.ts b/src/state/reducer/assignments.ts new file mode 100644 index 00000000..5805a602 --- /dev/null +++ b/src/state/reducer/assignments.ts @@ -0,0 +1,68 @@ +import Assignment, { AssignmentBrief, AsyncAssignment } from '../State/Assignment'; +import construct from '../../util/redux/construct'; +import db from '../../db'; +import { ASSIGNMENT_COLLECTION } from '../../db/constants'; +import Dict from '../../util/objectOps/Dict'; + +import store from '../'; +import Async from '../State/Async'; +import { Assignments } from '../State'; + +export namespace AssignmentsAction { + export interface SetAssignmentInternal { + type: 'assignments/set-assignment-internal'; + id: string; + assignment: AsyncAssignment; + } + + export const setAssignmentInternal = construct('assignments/set-assignment-internal'); + + export interface SetAssignmentsInternal { + type: 'assignments/set-assignments-internal'; + assignments: Dict; + } + + export const setAssignmentsInternal = construct('assignments/set-assignments-internal'); + + export interface ListAssignments { + type: 'assignments/list-assignments'; + id: string; + } + + export const listAssignments = construct('assignments/list-assignments'); +} + +export type AssignmentsAction = ( + AssignmentsAction.SetAssignmentInternal | + AssignmentsAction.SetAssignmentsInternal | + AssignmentsAction.ListAssignments +); + +export const listAssignments = async () => { + const assignments = await db.list(ASSIGNMENT_COLLECTION); + store.dispatch(AssignmentsAction.setAssignmentsInternal({ + assignments: Dict.map(assignments, assignment => Async.loaded({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + brief: AssignmentBrief.fromAssignment(assignment), + value: assignment + }) as AsyncAssignment) + })); +}; +export const reduceAssignments = (state: Assignments = Assignments.TEST, action: AssignmentsAction): Assignments => { + switch (action.type) { + case 'assignments/set-assignment-internal': return { + ...state, + [action.id]: action.assignment, + }; + case 'assignments/set-assignments-internal': { + const newState = { ...state }; + for (const [id, a] of Object.entries(action.assignments)) newState[id] = a; + return newState; + } + case 'assignments/list-assignments': { + void listAssignments(); + return state; + } + default: return state; + } +}; \ No newline at end of file diff --git a/src/state/reducer/index.ts b/src/state/reducer/index.ts index 32466173..d13a1968 100644 --- a/src/state/reducer/index.ts +++ b/src/state/reducer/index.ts @@ -4,3 +4,5 @@ export * from './challenges'; export * from './challengeCompletions'; export * from './documentation'; export * from './i18n'; +export * from './assignments'; +export * from './users'; diff --git a/src/state/reducer/users.ts b/src/state/reducer/users.ts new file mode 100644 index 00000000..d0a8f5fb --- /dev/null +++ b/src/state/reducer/users.ts @@ -0,0 +1,255 @@ +import { Users } from '../State'; +import User, { AsyncUser, UserBrief } from '../State/User'; +import construct from '../../util/redux/construct'; +import Async from '../State/Async'; +import db from '../../db'; +import Selector from '../../db/Selector'; +import store from '../'; +import { AutoSaver, errorToAsyncError } from './util'; + +export namespace UsersAction { + export interface SetMe { + type: 'users/set-me'; + me?: string; + } + + export const setMe = construct('users/set-me'); + + export interface SetUserInternal { + type: 'users/set-user-internal'; + userId: string; + user: AsyncUser; + } + + export const setUserInternal = construct('users/set-user-internal'); + + export interface AddMyAssignment { + type: 'users/add-my-assignment'; + assignmentId: string; + } + + export const addMyAssignment = construct('users/add-my-assignment'); + + export interface RemoveMyAssignment { + type: 'users/remove-my-assignment'; + assignmentId: string; + } + + export const removeMyAssignment = construct('users/remove-my-assignment'); + + export interface SetMyAssignments { + type: 'users/set-my-assignments'; + assignmentIds: string[]; + } + + export const setMyAssignments = construct('users/set-my-assignments'); + + export interface SyncMe { + type: 'users/sync-me'; + } + + export const SYNC_ME: SyncMe = { type: 'users/sync-me' }; + + export interface LoadUser { + type: 'users/load-user'; + userId: string; + } + + export const loadUser = construct('users/load-user'); + + export interface LoadOrEmptyUser { + type: 'users/load-or-empty-user'; + userId: string; + } + + export const loadOrEmptyUser = construct('users/load-or-empty-user'); +} + +export type UsersAction = ( + UsersAction.SetMe | + UsersAction.SetUserInternal | + UsersAction.AddMyAssignment | + UsersAction.RemoveMyAssignment | + UsersAction.SetMyAssignments | + UsersAction.SyncMe | + UsersAction.LoadUser | + UsersAction.LoadOrEmptyUser +); + +const sync = async (userId: string, current: Async.Saveable) => { + try { + await db.set(Selector.user(userId), current.value); + store.dispatch(UsersAction.setUserInternal({ + user: Async.loaded({ + brief: current.brief, + value: current.value + }), + userId, + })); + } catch (error) { + store.dispatch(UsersAction.setUserInternal({ + user: Async.saveFailed({ + brief: current.brief, + original: current.original, + value: current.value, + error: errorToAsyncError(error), + }), + userId, + })); + } +}; + +const load = async (userId: string, current: AsyncUser | undefined) => { + const brief = Async.brief(current); + try { + const value = await db.get(Selector.user(userId)); + store.dispatch(UsersAction.setUserInternal({ + user: Async.loaded({ brief, value }), + userId, + })); + } catch (error) { + store.dispatch(UsersAction.setUserInternal({ + user: Async.loadFailed({ brief, error: errorToAsyncError(error) }), + userId, + })); + } +}; + +const loadOrEmpty = async (userId: string, current: AsyncUser | undefined) => { + const brief = Async.brief(current); + try { + const value = await db.get(Selector.user(userId)); + store.dispatch(UsersAction.setUserInternal({ + user: Async.loaded({ brief, value }), + userId, + })); + } catch (error) { + const err = errorToAsyncError(error); + if (err.code === 404) { + store.dispatch(UsersAction.setUserInternal({ + user: Async.saveable({ brief, original: User.DEFAULT, value: User.DEFAULT }), + userId, + })); + } else { + store.dispatch(UsersAction.setUserInternal({ + user: Async.loadFailed({ brief, error: err }), + userId, + })); + } + } +}; + +const syncAll = async () => { + const state = store.getState(); + for (const userId in state.users.users) { + const user = state.users.users[userId]; + if (user.type !== Async.Type.Saveable) continue; + await sync(userId, user); + } +}; + +const autoSaver = new AutoSaver(1000, syncAll); + +export const reduceUsers = (state: Users = Users.EMPTY, action: UsersAction): Users => { + switch (action.type) { + case 'users/set-me': return { + ...state, + me: action.me, + }; + case 'users/set-user-internal': return { + ...state, + users: { + ...state.users, + [action.userId]: action.user, + }, + }; + case 'users/add-my-assignment': { + autoSaver.touch(); + return { + ...state, + users: { + ...state.users, + [state.me]: Async.mutate(state.users[state.me], user => { + if (!user.myAssignments) user.myAssignments = []; + user.myAssignments.push(action.assignmentId); + }), + }, + }; + } + case 'users/remove-my-assignment': { + autoSaver.touch(); + return { + ...state, + users: { + ...state.users, + [state.me]: Async.mutate(state.users[state.me], user => { + if (!user.myAssignments) return; + user.myAssignments = user.myAssignments.filter(id => id !== action.assignmentId); + }), + }, + }; + } + case 'users/set-my-assignments': { + autoSaver.touch(); + return { + ...state, + users: { + ...state.users, + [state.me]: Async.mutate(state.users[state.me], user => { + user.myAssignments = action.assignmentIds; + }), + }, + }; + } + case 'users/sync-me': { + const userId = state.me; + if (!userId) return state; + + const current = state.users[userId]; + if (current.type !== Async.Type.Saveable) return; + + void sync(userId, current); + + return { + ...state, + users: { + ...state.users, + [userId]: Async.saving(current), + }, + }; + } + case 'users/load-user': { + const userId = action.userId; + const current = state.users[userId]; + + void load(userId, current); + + return { + ...state, + users: { + ...state.users, + [userId]: Async.loading({ + brief: Async.brief(current), + }), + }, + }; + } + case 'users/load-or-empty-user': { + const userId = action.userId; + const current = state.users[userId]; + + void loadOrEmpty(userId, current); + + return { + ...state, + users: { + ...state.users, + [userId]: Async.loading({ + brief: Async.brief(current), + }), + }, + }; + } + default: return state; + } +}; \ No newline at end of file diff --git a/src/state/reducer/util.ts b/src/state/reducer/util.ts index d3bf9556..ef2a9676 100644 --- a/src/state/reducer/util.ts +++ b/src/state/reducer/util.ts @@ -36,3 +36,31 @@ export const errorToAsyncError = (error: unknown): Async.Error => { }; throw error; }; + +export class AutoSaver { + private readonly save_: () => Promise; + private readonly interval_: number; + private timer_: number | undefined = undefined; + + constructor(interval: number, save: () => Promise) { + this.save_ = save; + this.interval_ = interval; + // eslint-disable-next-line @typescript-eslint/no-misused-promises + this.timer_ = window.setInterval(this.save_, this.interval_); + } + + touch() { + if (this.timer_ !== undefined) { + window.clearInterval(this.timer_); + // eslint-disable-next-line @typescript-eslint/no-misused-promises + this.timer_ = window.setInterval(this.save_, this.interval_); + } + } + + destroy() { + if (this.timer_ !== undefined) { + window.clearInterval(this.timer_); + this.timer_ = undefined; + } + } +} \ No newline at end of file diff --git a/src/types/scratch.d.ts b/src/types/scratch.d.ts new file mode 100644 index 00000000..2da6782d --- /dev/null +++ b/src/types/scratch.d.ts @@ -0,0 +1,30 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +declare namespace Blockly { + interface Workspace { + addChangeListener(listener: (event: any) => void): void; + removeChangeListener(listener: (event: any) => void): void; + clear(); + } + + function inject(container: Element, options: any): Workspace; + + function svgResize(workspace: Workspace); + + namespace Xml { + function workspaceToDom(workspace: Workspace): XMLDocument; + function domToWorkspace(dom: XMLDocument, workspace: unknown): void; + function domToPrettyText(xml: XMLDocument): string; + function textToDom(text: string): XMLDocument; + } + + interface Toolbox { + setVisible(visible: boolean): void; + } + + interface MainWorkspace { + getToolbox(): Toolbox; + } + + function getMainWorkspace(): MainWorkspace; + +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index df752e16..7fd05325 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,7 @@ "baseUrl": "src", "paths": { "@i18n": ["util/i18n"], + "state/*": ["state/*"] } }, "types": [ diff --git a/yarn.lock b/yarn.lock index 7118ef3a..aa0b2ede 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1450,6 +1450,18 @@ dependencies: "@babel/types" "^7.3.0" +"@types/chai@^4.3.3": + version "4.3.20" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.20.tgz#cb291577ed342ca92600430841a00329ba05cecc" + integrity sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ== + +"@types/debug@^4.0.0": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" + integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== + dependencies: + "@types/ms" "*" + "@types/emscripten@^1.39.6": version "1.39.7" resolved "https://registry.yarnpkg.com/@types/emscripten/-/emscripten-1.39.7.tgz#3025183ea56e12bf4d096aadc48ce74ca051233d" @@ -1481,6 +1493,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.49.tgz#3facb98ebcd4114a4ecef74e0de2175b56fd4464" integrity sha512-K1AFuMe8a+pXmfHTtnwBvqoEylNKVeaiKYkjmcEAdytMQVJ/i9Fu7sc13GxgXdO49gkE7Hy8SyJonUZUn+eVaw== +"@types/gapi@^0.0.44": + version "0.0.44" + resolved "https://registry.yarnpkg.com/@types/gapi/-/gapi-0.0.44.tgz#f097f7a0f59d63a59098a08a62a560ca168426fb" + integrity sha512-hsgJMfZ/pMwI15UlAYHMNwj8DRoigo1odhbPwEXdp19ZQwQAXbcRrpzaDsfc+9XM6RtGpvl4Ja7uW8A+KPCa7w== + "@types/gettext-parser@^4.0.2": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/gettext-parser/-/gettext-parser-4.0.2.tgz#2048a92f137ec64fd504d62e8f407ad3b0e7e43e" @@ -1504,6 +1521,13 @@ dependencies: "@types/node" "*" +"@types/hast@^2.0.0": + version "2.3.10" + resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.10.tgz#5c9d9e0b304bbb8879b857225c5ebab2d81d7643" + integrity sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw== + dependencies: + "@types/unist" "^2" + "@types/history@^4.7.11", "@types/history@^4.7.2": version "4.7.11" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" @@ -1559,11 +1583,23 @@ resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== +"@types/mdast@^3.0.0": + version "3.0.15" + resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.15.tgz#49c524a263f30ffa28b71ae282f813ed000ab9f5" + integrity sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ== + dependencies: + "@types/unist" "^2" + "@types/minimatch@*": version "3.0.5" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ== +"@types/ms@*": + version "0.7.34" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" + integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== + "@types/node-fetch@^2.5.12": version "2.5.12" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.12.tgz#8a6f779b1d4e60b7a57fb6fd48d84fb545b9cc66" @@ -1587,6 +1623,11 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== +"@types/prop-types@^15.0.0": + version "15.7.13" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.13.tgz#2af91918ee12d9d32914feb13f5326658461b451" + integrity sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA== + "@types/q@^1.5.1": version "1.5.5" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.5.tgz#75a2a8e7d8ab4b230414505d92335d1dcb53a6df" @@ -1597,12 +1638,12 @@ resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== -"@types/react-dom@^16.9.5": - version "16.9.14" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.14.tgz#674b8f116645fe5266b40b525777fc6bb8eb3bcd" - integrity sha512-FIX2AVmPTGP30OUJ+0vadeIFJJ07Mh1m+U0rxfgyW34p3rTlXI+nlenvAxNn4BP36YyI9IJ/+UJ7Wu22N1pI7A== +"@types/react-dom@17.0.1": + version "17.0.1" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.1.tgz#d92d77d020bfb083e07cc8e0ac9f933599a4d56a" + integrity sha512-yIVyopxQb8IDZ7SOHeTovurFq+fXiPICa+GV3gp0Xedsl+MwQlMLKmvrnEjFbQxjliH5YVAEWFh975eVNmKj7Q== dependencies: - "@types/react" "^16" + "@types/react" "*" "@types/react-redux@^7.1.16", "@types/react-redux@^7.1.18": version "7.1.18" @@ -1631,15 +1672,6 @@ "@types/scheduler" "*" csstype "^3.0.2" -"@types/react@^16", "@types/react@^16.9.25": - version "16.14.11" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.11.tgz#992a0cd4b66b9f27315042b5d96e976717368f04" - integrity sha512-Don0MtsZZ3fjwTJ2BsoqkyOy7e176KplEAKOpr/4XDdzinlyJBn9yfsKn5mcSgn4kh1B22+3tBnzBC1z63ybtQ== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - "@types/readable-stream@*": version "2.3.15" resolved "https://registry.yarnpkg.com/@types/readable-stream/-/readable-stream-2.3.15.tgz#3d79c9ceb1b6a57d5f6e6976f489b9b5384321ae" @@ -1692,6 +1724,11 @@ dependencies: csstype "^3.0.2" +"@types/unist@^2", "@types/unist@^2.0.0": + version "2.0.11" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.11.tgz#11af57b127e32487774841f7a4e54eab166d03c4" + integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA== + "@types/uuid@^8.3.1": version "8.3.1" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.1.tgz#1a32969cf8f0364b3d8c8af9cc3555b7805df14f" @@ -1704,6 +1741,11 @@ dependencies: "@types/node" "*" +"@types/xmldom@^0.1.31": + version "0.1.34" + resolved "https://registry.yarnpkg.com/@types/xmldom/-/xmldom-0.1.34.tgz#a752f73bdf09cc6d78b3d3b2e7ca4dd04cc96fd2" + integrity sha512-7eZFfxI9XHYjJJuugddV6N5YNeXgQE1lArWOcd1eCOKWb/FGs5SIjacSYuEJuwhsGS3gy4RuZ5EUIcqYscuPDA== + "@types/yargs-parser@*": version "21.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" @@ -2288,6 +2330,11 @@ babylonjs-gltf2interface@6.18.0: resolved "https://registry.yarnpkg.com/babylonjs-gltf2interface/-/babylonjs-gltf2interface-6.18.0.tgz#a63540b5e0a2b90970177b2579cd4fc5068b3283" integrity sha512-gSXoYec7BFawuvdrvsBXKqVdkpuxznlX57c0qFP+/AkeAnb5Cvo3tGLKN0Iz6w4SLBgg7Td7hvxwQTxXkL/NRA== +bail@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d" + integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw== + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -2634,9 +2681,9 @@ camelcase@^6.2.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001219, caniuse-lite@^1.0.30001449: - version "1.0.30001565" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001565.tgz" - integrity sha512-xrE//a3O7TP0vaJ8ikzkD2c2NgcVUvsEe2IvFTntV4Yd1Z9FVzh+gW+enX96L0psrbaFMcVcH2l90xNuGDWc8w== + version "1.0.30001666" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001666.tgz" + integrity sha512-gD14ICmoV5ZZM1OdzPWmpx+q4GyefaK06zi8hmfHV5xe4/2nOQX3+Dw5o+fSqOws2xVwL9j+anOPFwHzdEdV4g== caw@^2.0.0, caw@^2.0.1: version "2.0.1" @@ -2681,6 +2728,11 @@ char-regex@^1.0.2: resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== +character-entities@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22" + integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ== + chokidar@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" @@ -2867,6 +2919,11 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" +comma-separated-tokens@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" + integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== + commander@^2.20.0, commander@^2.8.1, commander@^2.9.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -3160,6 +3217,13 @@ debug@^3.0.1, debug@^3.1.1, debug@^3.2.6: dependencies: ms "^2.1.1" +debug@^4.0.0: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: version "4.3.2" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" @@ -3172,6 +3236,13 @@ decamelize@^1.1.2, decamelize@^1.2.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= +decode-named-character-reference@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e" + integrity sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg== + dependencies: + character-entities "^2.0.0" + decode-uri-component@^0.2.0: version "0.2.2" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" @@ -3329,6 +3400,11 @@ depd@~1.1.2: resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= +dequal@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + destroy@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" @@ -3359,6 +3435,11 @@ diff@^4.0.1: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== +diff@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" + integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -3971,6 +4052,11 @@ express-http-proxy@^1.6.3: es6-promise "^4.1.1" raw-body "^2.3.0" +express-rate-limit@^7.4.0: + version "7.4.0" + resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-7.4.0.tgz#5db412b8de83fa07ddb40f610c585ac8c1dab988" + integrity sha512-v1204w3cXu5gCDmAvgvzI6qjzZzoMWKnyVDk3ACgfswTQLYiGen+r8w0VnXnGMmzEN/g8fwIQ4JrFFd4ZP6ssg== + express@^4.17.1: version "4.18.2" resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" @@ -4038,6 +4124,11 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" +extend@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + extglob@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" @@ -4801,6 +4892,11 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hast-util-whitespace@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz#0ec64e257e6fc216c7d14c8a1b74d27d650b4557" + integrity sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng== + he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" @@ -5165,6 +5261,11 @@ ini@^1.3.4: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== +inline-style-parser@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" + integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== + inline-style-prefixer@^5.1.0: version "5.1.2" resolved "https://registry.yarnpkg.com/inline-style-prefixer/-/inline-style-prefixer-5.1.2.tgz#e5a5a3515e25600e016b71e39138971228486c33" @@ -5268,6 +5369,11 @@ is-buffer@^1.1.5: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== +is-buffer@^2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" + integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== + is-callable@^1.1.4, is-callable@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e" @@ -5450,6 +5556,11 @@ is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= +is-plain-obj@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" + integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== + is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" @@ -5596,9 +5707,15 @@ isurl@^1.0.0-alpha5: has-to-string-tag-x "^1.2.0" is-object "^1.0.1" -"ivygate@https://github.com/kipr/ivygate#v0.1.5": +"itch@https://github.com/chrismbirmingham/itch#36": + version "1.0.0" + resolved "https://github.com/chrismbirmingham/itch#9b4db919283b1ebe7e889fd5c0c2679a365d79ae" + dependencies: + xmldom "^0.6.0" + +"ivygate@https://github.com/kipr/ivygate#v0.1.8": version "0.1.5" - resolved "https://github.com/kipr/ivygate#7fa32843112b0dc15e0c7e8ef5d4a70a557e34be" + resolved "https://github.com/kipr/ivygate#02e367e303bce33e518b9b5e12bdc19393977fc6" dependencies: monaco "^1.201704190613.0" monaco-editor "^0.23.0" @@ -6097,11 +6214,19 @@ kind-of@^6.0.0, kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== +"kipr-scratch@file:dependencies/kipr-scratch/kipr-scratch": + version "1.0.0" + kleur@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +kleur@^4.0.3: + version "4.1.5" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" + integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== + kuler@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" @@ -6362,6 +6487,54 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +mdast-util-definitions@^5.0.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz#9910abb60ac5d7115d6819b57ae0bcef07a3f7a7" + integrity sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA== + dependencies: + "@types/mdast" "^3.0.0" + "@types/unist" "^2.0.0" + unist-util-visit "^4.0.0" + +mdast-util-from-markdown@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz#9421a5a247f10d31d2faed2a30df5ec89ceafcf0" + integrity sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww== + dependencies: + "@types/mdast" "^3.0.0" + "@types/unist" "^2.0.0" + decode-named-character-reference "^1.0.0" + mdast-util-to-string "^3.1.0" + micromark "^3.0.0" + micromark-util-decode-numeric-character-reference "^1.0.0" + micromark-util-decode-string "^1.0.0" + micromark-util-normalize-identifier "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + unist-util-stringify-position "^3.0.0" + uvu "^0.5.0" + +mdast-util-to-hast@^12.1.0: + version "12.3.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz#045d2825fb04374e59970f5b3f279b5700f6fb49" + integrity sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw== + dependencies: + "@types/hast" "^2.0.0" + "@types/mdast" "^3.0.0" + mdast-util-definitions "^5.0.0" + micromark-util-sanitize-uri "^1.1.0" + trim-lines "^3.0.0" + unist-util-generated "^2.0.0" + unist-util-position "^4.0.0" + unist-util-visit "^4.0.0" + +mdast-util-to-string@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz#66f7bb6324756741c5f47a53557f0cbf16b6f789" + integrity sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg== + dependencies: + "@types/mdast" "^3.0.0" + mdn-data@2.0.14: version "2.0.14" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" @@ -6426,6 +6599,200 @@ methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== +micromark-core-commonmark@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz#1386628df59946b2d39fb2edfd10f3e8e0a75bb8" + integrity sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw== + dependencies: + decode-named-character-reference "^1.0.0" + micromark-factory-destination "^1.0.0" + micromark-factory-label "^1.0.0" + micromark-factory-space "^1.0.0" + micromark-factory-title "^1.0.0" + micromark-factory-whitespace "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-chunked "^1.0.0" + micromark-util-classify-character "^1.0.0" + micromark-util-html-tag-name "^1.0.0" + micromark-util-normalize-identifier "^1.0.0" + micromark-util-resolve-all "^1.0.0" + micromark-util-subtokenize "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.1" + uvu "^0.5.0" + +micromark-factory-destination@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz#eb815957d83e6d44479b3df640f010edad667b9f" + integrity sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-factory-label@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz#cc95d5478269085cfa2a7282b3de26eb2e2dec68" + integrity sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + +micromark-factory-space@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz#c8f40b0640a0150751d3345ed885a080b0d15faf" + integrity sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-factory-title@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz#dd0fe951d7a0ac71bdc5ee13e5d1465ad7f50ea1" + integrity sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ== + dependencies: + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-factory-whitespace@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz#798fb7489f4c8abafa7ca77eed6b5745853c9705" + integrity sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ== + dependencies: + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-util-character@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-1.2.0.tgz#4fedaa3646db249bc58caeb000eb3549a8ca5dcc" + integrity sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg== + dependencies: + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-util-chunked@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz#37a24d33333c8c69a74ba12a14651fd9ea8a368b" + integrity sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ== + dependencies: + micromark-util-symbol "^1.0.0" + +micromark-util-classify-character@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz#6a7f8c8838e8a120c8e3c4f2ae97a2bff9190e9d" + integrity sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-util-combine-extensions@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz#192e2b3d6567660a85f735e54d8ea6e3952dbe84" + integrity sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA== + dependencies: + micromark-util-chunked "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-util-decode-numeric-character-reference@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz#b1e6e17009b1f20bc652a521309c5f22c85eb1c6" + integrity sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw== + dependencies: + micromark-util-symbol "^1.0.0" + +micromark-util-decode-string@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz#dc12b078cba7a3ff690d0203f95b5d5537f2809c" + integrity sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ== + dependencies: + decode-named-character-reference "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-decode-numeric-character-reference "^1.0.0" + micromark-util-symbol "^1.0.0" + +micromark-util-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz#92e4f565fd4ccb19e0dcae1afab9a173bbeb19a5" + integrity sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw== + +micromark-util-html-tag-name@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz#48fd7a25826f29d2f71479d3b4e83e94829b3588" + integrity sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q== + +micromark-util-normalize-identifier@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz#7a73f824eb9f10d442b4d7f120fecb9b38ebf8b7" + integrity sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q== + dependencies: + micromark-util-symbol "^1.0.0" + +micromark-util-resolve-all@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz#4652a591ee8c8fa06714c9b54cd6c8e693671188" + integrity sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA== + dependencies: + micromark-util-types "^1.0.0" + +micromark-util-sanitize-uri@^1.0.0, micromark-util-sanitize-uri@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz#613f738e4400c6eedbc53590c67b197e30d7f90d" + integrity sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-encode "^1.0.0" + micromark-util-symbol "^1.0.0" + +micromark-util-subtokenize@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz#941c74f93a93eaf687b9054aeb94642b0e92edb1" + integrity sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A== + dependencies: + micromark-util-chunked "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + +micromark-util-symbol@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz#813cd17837bdb912d069a12ebe3a44b6f7063142" + integrity sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag== + +micromark-util-types@^1.0.0, micromark-util-types@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-1.1.0.tgz#e6676a8cae0bb86a2171c498167971886cb7e283" + integrity sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg== + +micromark@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/micromark/-/micromark-3.2.0.tgz#1af9fef3f995ea1ea4ac9c7e2f19c48fd5c006e9" + integrity sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA== + dependencies: + "@types/debug" "^4.0.0" + debug "^4.0.0" + decode-named-character-reference "^1.0.0" + micromark-core-commonmark "^1.0.1" + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-chunked "^1.0.0" + micromark-util-combine-extensions "^1.0.0" + micromark-util-decode-numeric-character-reference "^1.0.0" + micromark-util-encode "^1.0.0" + micromark-util-normalize-identifier "^1.0.0" + micromark-util-resolve-all "^1.0.0" + micromark-util-sanitize-uri "^1.0.0" + micromark-util-subtokenize "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.1" + uvu "^0.5.0" + micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" @@ -6611,6 +6978,11 @@ mri@1.1.4: resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.4.tgz#7cb1dd1b9b40905f1fac053abe25b6720f44744a" integrity sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w== +mri@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" + integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -6621,7 +6993,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.1.1: +ms@2.1.3, ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -7455,6 +7827,15 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" +prop-types@^15.0.0, prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" @@ -7464,14 +7845,10 @@ prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: object-assign "^4.1.1" react-is "^16.8.1" -prop-types@^15.8.1: - version "15.8.1" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" - integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== - dependencies: - loose-envify "^1.4.0" - object-assign "^4.1.1" - react-is "^16.13.1" +property-information@^6.0.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.5.0.tgz#6212fbb52ba757e92ef4fb9d657563b933b7ffec" + integrity sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig== proto-list@~1.2.1: version "1.2.4" @@ -7639,6 +8016,27 @@ react-loading@^2.0.3: resolved "https://registry.yarnpkg.com/react-loading/-/react-loading-2.0.3.tgz#e8138fb0c3e4674e481b320802ac7048ae14ffb9" integrity sha512-Vdqy79zq+bpeWJqC+xjltUjuGApyoItPgL0vgVfcJHhqwU7bAMKzysfGW/ADu6i0z0JiOCRJjo+IkFNkRNbA3A== +react-markdown@^8.0.1: + version "8.0.7" + resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-8.0.7.tgz#c8dbd1b9ba5f1c5e7e5f2a44de465a3caafdf89b" + integrity sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ== + dependencies: + "@types/hast" "^2.0.0" + "@types/prop-types" "^15.0.0" + "@types/unist" "^2.0.0" + comma-separated-tokens "^2.0.0" + hast-util-whitespace "^2.0.0" + prop-types "^15.0.0" + property-information "^6.0.0" + react-is "^18.0.0" + remark-parse "^10.0.0" + remark-rehype "^10.0.0" + space-separated-tokens "^2.0.0" + style-to-object "^0.4.0" + unified "^10.0.0" + unist-util-visit "^4.0.0" + vfile "^5.0.0" + react-redux@^7.2.4: version "7.2.4" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225" @@ -7819,6 +8217,25 @@ relateurl@^0.2.7: resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= +remark-parse@^10.0.0: + version "10.0.2" + resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-10.0.2.tgz#ca241fde8751c2158933f031a4e3efbaeb8bc262" + integrity sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-from-markdown "^1.0.0" + unified "^10.0.0" + +remark-rehype@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-10.1.0.tgz#32dc99d2034c27ecaf2e0150d22a6dcccd9a6279" + integrity sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw== + dependencies: + "@types/hast" "^2.0.0" + "@types/mdast" "^3.0.0" + mdast-util-to-hast "^12.1.0" + unified "^10.0.0" + remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" @@ -7989,6 +8406,13 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +sade@^1.7.3: + version "1.8.1" + resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701" + integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A== + dependencies: + mri "^1.1.0" + safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" @@ -8441,6 +8865,11 @@ source-map@^0.7.3, source-map@~0.7.2: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== +space-separated-tokens@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" + integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== + spdx-correct@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" @@ -8705,6 +9134,13 @@ style-loader@^2.0.0: loader-utils "^2.0.0" schema-utils "^3.0.0" +style-to-object@^0.4.0: + version "0.4.4" + resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.4.4.tgz#266e3dfd56391a7eefb7770423612d043c3f33ec" + integrity sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg== + dependencies: + inline-style-parser "0.1.1" + styletron-client@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/styletron-client/-/styletron-client-3.0.4.tgz#50c3fcdc7f45ed0693d68dc174bd0f2fa607cc57" @@ -8999,6 +9435,11 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= +trim-lines@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" + integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== + trim-newlines@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" @@ -9016,6 +9457,11 @@ triple-beam@^1.3.0: resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== +trough@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/trough/-/trough-2.2.0.tgz#94a60bd6bd375c152c1df911a4b11d5b0256f50f" + integrity sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw== + ts-jest@^29.0.5: version "29.0.5" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.0.5.tgz#c5557dcec8fe434fcb8b70c3e21c6b143bfce066" @@ -9151,6 +9597,19 @@ unbzip2-stream@^1.0.9: buffer "^5.2.1" through "^2.3.8" +unified@^10.0.0: + version "10.1.2" + resolved "https://registry.yarnpkg.com/unified/-/unified-10.1.2.tgz#b1d64e55dafe1f0b98bb6c719881103ecf6c86df" + integrity sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q== + dependencies: + "@types/unist" "^2.0.0" + bail "^2.0.0" + extend "^3.0.0" + is-buffer "^2.0.0" + is-plain-obj "^4.0.0" + trough "^2.0.0" + vfile "^5.0.0" + union-value@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" @@ -9161,6 +9620,49 @@ union-value@^1.0.0: is-extendable "^0.1.1" set-value "^2.0.1" +unist-util-generated@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-2.0.1.tgz#e37c50af35d3ed185ac6ceacb6ca0afb28a85cae" + integrity sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A== + +unist-util-is@^5.0.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-5.2.1.tgz#b74960e145c18dcb6226bc57933597f5486deae9" + integrity sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw== + dependencies: + "@types/unist" "^2.0.0" + +unist-util-position@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-4.0.4.tgz#93f6d8c7d6b373d9b825844645877c127455f037" + integrity sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg== + dependencies: + "@types/unist" "^2.0.0" + +unist-util-stringify-position@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz#03ad3348210c2d930772d64b489580c13a7db39d" + integrity sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg== + dependencies: + "@types/unist" "^2.0.0" + +unist-util-visit-parents@^5.1.1: + version "5.1.3" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz#b4520811b0ca34285633785045df7a8d6776cfeb" + integrity sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^5.0.0" + +unist-util-visit@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-4.1.2.tgz#125a42d1eb876283715a3cb5cceaa531828c72e2" + integrity sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^5.0.0" + unist-util-visit-parents "^5.1.1" + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -9288,6 +9790,16 @@ uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +uvu@^0.5.0: + version "0.5.6" + resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.6.tgz#2754ca20bcb0bb59b64e9985e84d2e81058502df" + integrity sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA== + dependencies: + dequal "^2.0.0" + diff "^5.0.0" + kleur "^4.0.3" + sade "^1.7.3" + v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" @@ -9325,6 +9837,24 @@ vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== +vfile-message@^3.0.0: + version "3.1.4" + resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-3.1.4.tgz#15a50816ae7d7c2d1fa87090a7f9f96612b59dea" + integrity sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw== + dependencies: + "@types/unist" "^2.0.0" + unist-util-stringify-position "^3.0.0" + +vfile@^5.0.0: + version "5.3.7" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-5.3.7.tgz#de0677e6683e3380fafc46544cfe603118826ab7" + integrity sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g== + dependencies: + "@types/unist" "^2.0.0" + is-buffer "^2.0.0" + unist-util-stringify-position "^3.0.0" + vfile-message "^3.0.0" + walker@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" @@ -9619,6 +10149,11 @@ ws@^8.4.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.4.2.tgz#18e749868d8439f2268368829042894b6907aa0b" integrity sha512-Kbk4Nxyq7/ZWqr/tarI9yIt/+iNNFOjBXEWgTb4ydaNHBNGgvf2QHbS9fdfsndfjFlFwEd4Al+mw83YkaD10ZA== +xmldom@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.6.0.tgz#43a96ecb8beece991cef382c08397d82d4d0c46f" + integrity sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg== + xtend@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"