parent
99f4311eb7
commit
70fde7f10f
@ -0,0 +1,24 @@
|
||||
# WebStorm settings
|
||||
/.idea
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
@ -0,0 +1,17 @@
|
||||
tasks:
|
||||
- init: >
|
||||
git clone https://github.com/algorithm-visualizer/server.git &&
|
||||
cd server &&
|
||||
npm install &&
|
||||
echo -e "GITHUB_CLIENT_ID=dummy\nGITHUB_CLIENT_SECRET=dummy\nAWS_ACCESS_KEY_ID=dummy\nAWS_SECRET_ACCESS_KEY=dummy" > .env.local &&
|
||||
cd ..
|
||||
command: cd server && npm run watch
|
||||
- init: >
|
||||
npm install &&
|
||||
echo 'DANGEROUSLY_DISABLE_HOST_CHECK=true' > .env.local
|
||||
command: npm start
|
||||
ports:
|
||||
- port: 3000
|
||||
onOpen: notify
|
||||
- port: 8080
|
||||
onOpen: ignore
|
||||
@ -0,0 +1,76 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at parkjs814@gmail.com. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
@ -0,0 +1,60 @@
|
||||
# Contributing
|
||||
|
||||
> #### Table of Contents
|
||||
> - [Running Locally](#running-locally)
|
||||
> - [Running in Gitpod](#running-in-gitpod)
|
||||
> - [Directory Structure](#directory-structure)
|
||||
|
||||
Are you a first-timer in contributing to open source? [These guidelines](https://opensource.guide/how-to-contribute/#how-to-submit-a-contribution) from GitHub might help!
|
||||
|
||||
## Running Locally
|
||||
|
||||
1. Fork this repository.
|
||||
|
||||
2. Clone your forked repo to your machine.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/<your-username>/algorithm-visualizer.git
|
||||
```
|
||||
|
||||
3. Choose whether to run [`server`](https://github.com/algorithm-visualizer/server) on your machine or to use the remote server.
|
||||
- If you choose to run the server locally as well, follow the instructions [here](https://github.com/algorithm-visualizer/server/blob/master/CONTRIBUTING.md#running-locally).
|
||||
|
||||
- If you choose to use the remote server, **temporarily** (i.e., don't commit this change) modify `package.json` as follows:
|
||||
```diff
|
||||
- "proxy": "http://localhost:8080",
|
||||
+ "proxy": "https://algorithm-visualizer.org",
|
||||
```
|
||||
|
||||
4. Install dependencies, and run the web app.
|
||||
|
||||
```bash
|
||||
cd algorithm-visualizer
|
||||
|
||||
npm install
|
||||
|
||||
npm start
|
||||
```
|
||||
|
||||
5. Open [`http://localhost:3000/`](http://localhost:3000/) in a web browser.
|
||||
|
||||
## Running in Gitpod
|
||||
|
||||
You can also run `algorithm-visualizer` in Gitpod, a free online dev environment for GitHub.
|
||||
|
||||
[](https://gitpod.io/#https://github.com/algorithm-visualizer/algorithm-visualizer)
|
||||
|
||||
## Directory Structure
|
||||
|
||||
- [**branding/**](branding) contains representative image files.
|
||||
- [**public/**](public) contains static files to be served.
|
||||
- [**src/**](src) contains source code.
|
||||
- [**apis/**](src/apis) defines outgoing API requests.
|
||||
- [**common/**](src/common) contains commonly used files.
|
||||
- [**components/**](src/components) contains UI components.
|
||||
- [**core/**](src/core) processes visualization.
|
||||
- [**layouts/**](src/core/layouts) layout tracers.
|
||||
- [**renderers/**](src/core/renderers) renders visualization data.
|
||||
- [**tracers/**](src/core/tracers) interprets visualizing commands into visualization data.
|
||||
- [**files/**](src/files) contains markdown or skeleton files to be shown in the code editor.
|
||||
- [**reducers/**](src/reducers) contains Redux reducers.
|
||||
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Jinseo Jason Park
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@ -1,2 +1,54 @@
|
||||
# algorithm-visualizer
|
||||
# Algorithm Visualizer
|
||||
|
||||
## Introduction
|
||||
Welcome to Algorithm Visualizer, an interactive online platform designed to bring algorithms to life through visualization. Whether you're a student, teacher, or professional, our platform provides an engaging way to explore and understand various algorithms.
|
||||
|
||||
[](https://github.com/algorithm-visualizer/algorithm-visualizer/graphs/contributors)
|
||||
[](https://github.com/algorithm-visualizer/algorithm-visualizer/blob/master/LICENSE)
|
||||
|
||||
## Languages and Frameworks Used
|
||||
[](https://skillicons.dev)
|
||||
|
||||
|
||||
## Key Features
|
||||
<ul>
|
||||
<li>
|
||||
|
||||
### Visualize algorithms from code:
|
||||
Algorithm Visualizer allows you to witness algorithms in action by visualizing code written in various programming languages. This visual approach facilitates a better understanding of algorithmic behavior..</li>
|
||||
<li>
|
||||
|
||||
### Learn about Algorithms:
|
||||
Explore our collection of tutorials, articles, and videos that serve as valuable resources for learning about algorithms.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
## algorithms
|
||||
In this repository, you'll find visualizations of algorithms showcased in the website's side menu. Contributions here directly impact the educational content available on the platform. https://github.com/algorithm-visualizer/algorithms</li>
|
||||
</ul>
|
||||
|
||||
|
||||
## tracers
|
||||
Explore the various visualization libraries in different programming languages. These libraries extract visualization commands from code.
|
||||
https://github.com/search?q=topic%3Avisualization-library+org%3Aalgorithm-visualizer&type=Repositories</li>
|
||||
</ul>
|
||||
|
||||
## Live Demo
|
||||
Learning an algorithm gets much easier with visualizing it. Don't get what we mean? Check it out:
|
||||
|
||||
[**algorithm-visualizer.org**](https://algorithm-visualizer.org/)
|
||||
|
||||
## Contributing
|
||||
|
||||
Our project consists of multiple repositories, each playing a crucial role in the Algorithm Visualizer ecosystem. If you're interested in contributing, check out the guidelines for the specific repository:
|
||||
|
||||
|
||||
- [**`algorithm-visualizer`**](https://github.com/algorithm-visualizer/algorithm-visualizer) is a web app written in React. It contains UI components and interprets commands into visualizations. Check out [the contributing guidelines](CONTRIBUTING.md).
|
||||
|
||||
- [**`server`**](https://github.com/algorithm-visualizer/server) serves the web app and provides APIs that it needs on the fly. (e.g., GitHub sign in, compiling/running code, etc.)
|
||||
|
||||
- [**`algorithms`**](https://github.com/algorithm-visualizer/algorithms) contains visualizations of algorithms shown on the side menu of the website.
|
||||
|
||||
- [**`tracers.*`**](https://github.com/search?q=topic%3Avisualization-library+org%3Aalgorithm-visualizer&type=Repositories) are visualization libraries written in each supported language. They extract visualizing commands from code.
|
||||
|
||||
Ready to contribute? Explore the repositories and become part of the Algorithm Visualizer community!
|
||||
|
After Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 260 KiB |
@ -0,0 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"baseUrl": "src"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "@algorithm-visualizer/algorithm-visualizer",
|
||||
"version": "2.0.0",
|
||||
"title": "Algorithm Visualizer",
|
||||
"description": "Algorithm Visualizer is an interactive online platform that visualizes algorithms from code.",
|
||||
"scripts": {
|
||||
"start": "NODE_OPTIONS=--openssl-legacy-provider react-scripts start",
|
||||
"build": "NODE_OPTIONS=--openssl-legacy-provider react-scripts build",
|
||||
"test": "NODE_OPTIONS=--openssl-legacy-provider react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"proxy": "https://algorithm-visualizer.org",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.15.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome": "^1.1.8",
|
||||
"@fortawesome/fontawesome-free-brands": "^5.0.13",
|
||||
"@fortawesome/fontawesome-free-solid": "^5.0.13",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.19",
|
||||
"@fortawesome/react-fontawesome": "0.1.4",
|
||||
"axios": "^0.19.0",
|
||||
"bluebird": "latest",
|
||||
"brace": "latest",
|
||||
"chart.js": "^2.8.0",
|
||||
"js-cookie": "^2.2.0",
|
||||
"sass": "^1.69.0",
|
||||
"query-string": "^6.7.0",
|
||||
"raw-loader": "^3.0.0",
|
||||
"react": "^16.8.6",
|
||||
"react-ace": "^7.0.2",
|
||||
"react-chartjs-2": "^2.7.6",
|
||||
"react-dom": "^16.8.6",
|
||||
"react-helmet": "^5.2.1",
|
||||
"react-input-autosize": "^2.2.1",
|
||||
"react-input-range": "^1.3.0",
|
||||
"react-markdown": "^4.0.8",
|
||||
"react-redux": "^7.0.3",
|
||||
"react-router": "^5.0.1",
|
||||
"react-router-dom": "^5.0.1",
|
||||
"react-router-redux": "^4.0.8",
|
||||
"react-scripts": "^3.0.1",
|
||||
"react-toastify": "^5.2.1",
|
||||
"redbox-react": "^1.6.0",
|
||||
"redux": "^4.0.1",
|
||||
"redux-actions": "^2.6.5",
|
||||
"remove-markdown": "^0.3.0",
|
||||
"screenfull": "^4.2.0",
|
||||
"sprintf-js": "^1.1.2",
|
||||
"uuid": "^3.3.2"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
icons/icon-32x32.png
|
||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 39 KiB |
@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-78128848-1"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
|
||||
function gtag() {
|
||||
dataLayer.push(arguments);
|
||||
}
|
||||
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'UA-78128848-1');
|
||||
</script>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta name="theme-color" content="#393939">
|
||||
<meta name="description" content="Algorithm Visualizer is an interactive online platform that visualizes algorithms from code." data-react-helmet="true" />
|
||||
<meta property="og:image"
|
||||
content="https://raw.githubusercontent.com/algorithm-visualizer/algorithm-visualizer/master/branding/screenshot.png" />
|
||||
<meta property="og:site_name" content="Algorithm Visualizer" />
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.png" type="image/png">
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:400,700" rel="stylesheet">
|
||||
<title>Algorithm Visualizer</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "Algorithm Visualizer",
|
||||
"short_name": "Algorithm Visualizer",
|
||||
"start_url": "/?utm_source=homescreen",
|
||||
"display": "fullscreen",
|
||||
"orientation": "landscape",
|
||||
"theme_color": "#393939",
|
||||
"background_color": "#393939",
|
||||
"description": "Algorithm Visualizer is an interactive online platform that visualizes algorithms from code.",
|
||||
"icons": [
|
||||
{
|
||||
"src": "icons/icon-32x32.png",
|
||||
"sizes": "32x32",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /scratch-paper/
|
||||
@ -0,0 +1,112 @@
|
||||
import Promise from 'bluebird';
|
||||
import axios from 'axios';
|
||||
|
||||
axios.interceptors.response.use(response => response.data);
|
||||
|
||||
const request = (url, process) => {
|
||||
const tokens = url.split('/');
|
||||
const baseURL = /^https?:\/\//i.test(url) ? '' : '/api';
|
||||
return (...args) => {
|
||||
const mappedURL = baseURL + tokens.map((token, i) => token.startsWith(':') ? args.shift() : token).join('/');
|
||||
return Promise.resolve(process(mappedURL, args));
|
||||
};
|
||||
};
|
||||
|
||||
const GET = URL => {
|
||||
return request(URL, (mappedURL, args) => {
|
||||
const [params, cancelToken] = args;
|
||||
return axios.get(mappedURL, { params, cancelToken });
|
||||
});
|
||||
};
|
||||
|
||||
const DELETE = URL => {
|
||||
return request(URL, (mappedURL, args) => {
|
||||
const [params, cancelToken] = args;
|
||||
return axios.delete(mappedURL, { params, cancelToken });
|
||||
});
|
||||
};
|
||||
|
||||
const POST = URL => {
|
||||
return request(URL, (mappedURL, args) => {
|
||||
const [body, params, cancelToken] = args;
|
||||
return axios.post(mappedURL, body, { params, cancelToken });
|
||||
});
|
||||
};
|
||||
|
||||
const PUT = URL => {
|
||||
return request(URL, (mappedURL, args) => {
|
||||
const [body, params, cancelToken] = args;
|
||||
return axios.put(mappedURL, body, { params, cancelToken });
|
||||
});
|
||||
};
|
||||
|
||||
const PATCH = URL => {
|
||||
return request(URL, (mappedURL, args) => {
|
||||
const [body, params, cancelToken] = args;
|
||||
return axios.patch(mappedURL, body, { params, cancelToken });
|
||||
});
|
||||
};
|
||||
|
||||
const AlgorithmApi = {
|
||||
getCategories: GET('/algorithms'),
|
||||
getAlgorithm: GET('/algorithms/:categoryKey/:algorithmKey'),
|
||||
};
|
||||
|
||||
const VisualizationApi = {
|
||||
getVisualization: GET('/visualizations/:visualizationId'),
|
||||
};
|
||||
|
||||
const GitHubApi = {
|
||||
auth: token => Promise.resolve(axios.defaults.headers.common['Authorization'] = token && `token ${token}`),
|
||||
getUser: GET('https://api.github.com/user'),
|
||||
listGists: GET('https://api.github.com/gists'),
|
||||
createGist: POST('https://api.github.com/gists'),
|
||||
editGist: PATCH('https://api.github.com/gists/:id'),
|
||||
getGist: GET('https://api.github.com/gists/:id'),
|
||||
deleteGist: DELETE('https://api.github.com/gists/:id'),
|
||||
forkGist: POST('https://api.github.com/gists/:id/forks'),
|
||||
};
|
||||
|
||||
const TracerApi = {
|
||||
md: ({ code }) => Promise.resolve([{
|
||||
key: 'markdown',
|
||||
method: 'MarkdownTracer',
|
||||
args: ['Markdown'],
|
||||
}, {
|
||||
key: 'markdown',
|
||||
method: 'set',
|
||||
args: [code],
|
||||
}, {
|
||||
key: null,
|
||||
method: 'setRoot',
|
||||
args: ['markdown'],
|
||||
}]),
|
||||
json: ({ code }) => new Promise(resolve => resolve(JSON.parse(code))),
|
||||
js: ({ code }, params, cancelToken) => new Promise((resolve, reject) => {
|
||||
const worker = new Worker('/api/tracers/js/worker');
|
||||
if (cancelToken) {
|
||||
cancelToken.promise.then(cancel => {
|
||||
worker.terminate();
|
||||
reject(cancel);
|
||||
});
|
||||
}
|
||||
worker.onmessage = e => {
|
||||
worker.terminate();
|
||||
resolve(e.data);
|
||||
};
|
||||
worker.onerror = error => {
|
||||
worker.terminate();
|
||||
reject(error);
|
||||
};
|
||||
worker.postMessage(code);
|
||||
}),
|
||||
cpp: POST('/tracers/cpp'),
|
||||
java: POST('/tracers/java'),
|
||||
};
|
||||
|
||||
export {
|
||||
AlgorithmApi,
|
||||
VisualizationApi,
|
||||
GitHubApi,
|
||||
TracerApi,
|
||||
};
|
||||
@ -0,0 +1,25 @@
|
||||
import { CODE_CPP, CODE_JAVA, CODE_JS } from 'files';
|
||||
|
||||
const languages = [{
|
||||
name: 'JavaScript',
|
||||
ext: 'js',
|
||||
mode: 'javascript',
|
||||
skeleton: CODE_JS,
|
||||
}, {
|
||||
name: 'C++',
|
||||
ext: 'cpp',
|
||||
mode: 'c_cpp',
|
||||
skeleton: CODE_CPP,
|
||||
}, {
|
||||
name: 'Java',
|
||||
ext: 'java',
|
||||
mode: 'java',
|
||||
skeleton: CODE_JAVA,
|
||||
}];
|
||||
|
||||
const exts = languages.map(language => language.ext);
|
||||
|
||||
export {
|
||||
languages,
|
||||
exts,
|
||||
};
|
||||
@ -0,0 +1,25 @@
|
||||
$theme-dark: #242424;
|
||||
$theme-normal: #393939;
|
||||
$theme-light: #505050;
|
||||
$color-font: #bbbbbb;
|
||||
$color-shadow: rgba(#000000, .2);
|
||||
$color-overlay: rgba(#ffffff, .1);
|
||||
$color-alert: #f3bd58;
|
||||
$color-selected: #2962ff;
|
||||
$color-patched: #c51162;
|
||||
$color-highlight: #29d;
|
||||
$color-active: #00e676;
|
||||
|
||||
:export {
|
||||
themeDark: $theme-dark;
|
||||
themeNormal: $theme-normal;
|
||||
themeLight: $theme-light;
|
||||
colorFont: $color-font;
|
||||
colorShadow: $color-shadow;
|
||||
colorOverlay: $color-overlay;
|
||||
colorAlert: $color-alert;
|
||||
colorSelected: $color-selected;
|
||||
colorPatched: $color-patched;
|
||||
colorHighlight: $color-highlight;
|
||||
colorActive: $color-active;
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
$line-height: 32px;
|
||||
$font-size-normal: 12px;
|
||||
$font-size-large: 14px;
|
||||
|
||||
:export {
|
||||
lineHeight: $line-height;
|
||||
fontSizeNormal: $font-size-normal;
|
||||
fontSizeLarge: $font-size-large;
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
$font-family-normal: 'Roboto', sans-serif;
|
||||
$font-family-monospace: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
|
||||
@ -0,0 +1,3 @@
|
||||
@import "colors";
|
||||
@import "dimensions";
|
||||
@import "fonts";
|
||||
@ -0,0 +1,50 @@
|
||||
const classes = (...arr) => arr.filter(v => v).join(' ');
|
||||
|
||||
const distance = (a, b) => {
|
||||
const dx = a.x - b.x;
|
||||
const dy = a.y - b.y;
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
};
|
||||
|
||||
const extension = fileName => /(?:\.([^.]+))?$/.exec(fileName)[1];
|
||||
|
||||
const refineGist = gist => {
|
||||
const gistId = gist.id;
|
||||
const title = gist.description;
|
||||
delete gist.files['algorithm-visualizer'];
|
||||
const { login, avatar_url } = gist.owner;
|
||||
const files = Object.values(gist.files).map(file => ({
|
||||
name: file.filename,
|
||||
content: file.content,
|
||||
contributors: [{ login, avatar_url }],
|
||||
}));
|
||||
return { login, gistId, title, files };
|
||||
};
|
||||
|
||||
const createFile = (name, content, contributors) => ({ name, content, contributors });
|
||||
|
||||
const createProjectFile = (name, content) => createFile(name, content, [{
|
||||
login: 'algorithm-visualizer',
|
||||
avatar_url: 'https://github.com/algorithm-visualizer.png',
|
||||
}]);
|
||||
|
||||
const createUserFile = (name, content) => createFile(name, content, undefined);
|
||||
|
||||
const isSaved = ({ titles, files, lastTitles, lastFiles }) => {
|
||||
const serialize = (titles, files) => JSON.stringify({
|
||||
titles,
|
||||
files: files.map(({ name, content }) => ({ name, content })),
|
||||
});
|
||||
return serialize(titles, files) === serialize(lastTitles, lastFiles);
|
||||
};
|
||||
|
||||
export {
|
||||
classes,
|
||||
distance,
|
||||
extension,
|
||||
refineGist,
|
||||
createFile,
|
||||
createProjectFile,
|
||||
createUserFile,
|
||||
isSaved,
|
||||
};
|
||||
@ -0,0 +1,30 @@
|
||||
@import "~common/stylesheet/index";
|
||||
|
||||
.app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
height: 100%;
|
||||
background-color: $theme-normal;
|
||||
|
||||
.header {
|
||||
}
|
||||
|
||||
.workspace {
|
||||
flex: 1;
|
||||
|
||||
.visualization_viewer {
|
||||
background-color: $theme-dark;
|
||||
}
|
||||
|
||||
.editor_tab_container {
|
||||
}
|
||||
}
|
||||
|
||||
.toast_container {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 99;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,254 @@
|
||||
import React from 'react';
|
||||
import Cookies from 'js-cookie';
|
||||
import { connect } from 'react-redux';
|
||||
import Promise from 'bluebird';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import queryString from 'query-string';
|
||||
import {
|
||||
BaseComponent,
|
||||
CodeEditor,
|
||||
Header,
|
||||
Navigator,
|
||||
ResizableContainer,
|
||||
TabContainer,
|
||||
ToastContainer,
|
||||
VisualizationViewer,
|
||||
} from 'components';
|
||||
import { AlgorithmApi, GitHubApi, VisualizationApi } from 'apis';
|
||||
import { actions } from 'reducers';
|
||||
import { createUserFile, extension, refineGist } from 'common/util';
|
||||
import { exts, languages } from 'common/config';
|
||||
import { SCRATCH_PAPER_README_MD } from 'files';
|
||||
import styles from './App.module.scss';
|
||||
|
||||
class App extends BaseComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
workspaceVisibles: [true, true, true],
|
||||
workspaceWeights: [1, 2, 2],
|
||||
};
|
||||
|
||||
this.codeEditorRef = React.createRef();
|
||||
|
||||
this.ignoreHistoryBlock = this.ignoreHistoryBlock.bind(this);
|
||||
this.handleClickTitleBar = this.handleClickTitleBar.bind(this);
|
||||
this.loadScratchPapers = this.loadScratchPapers.bind(this);
|
||||
this.handleChangeWorkspaceWeights = this.handleChangeWorkspaceWeights.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.signIn = this.signIn.bind(this);
|
||||
window.signOut = this.signOut.bind(this);
|
||||
|
||||
const { params } = this.props.match;
|
||||
const { search } = this.props.location;
|
||||
this.loadAlgorithm(params, queryString.parse(search));
|
||||
|
||||
const accessToken = Cookies.get('access_token');
|
||||
if (accessToken) this.signIn(accessToken);
|
||||
|
||||
AlgorithmApi.getCategories()
|
||||
.then(({ categories }) => this.props.setCategories(categories))
|
||||
.catch(this.handleError);
|
||||
|
||||
this.toggleHistoryBlock(true);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
delete window.signIn;
|
||||
delete window.signOut;
|
||||
|
||||
this.toggleHistoryBlock(false);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { params } = nextProps.match;
|
||||
const { search } = nextProps.location;
|
||||
if (params !== this.props.match.params || search !== this.props.location.search) {
|
||||
const { categoryKey, algorithmKey, gistId } = params;
|
||||
const { algorithm, scratchPaper } = nextProps.current;
|
||||
if (algorithm && algorithm.categoryKey === categoryKey && algorithm.algorithmKey === algorithmKey) return;
|
||||
if (scratchPaper && scratchPaper.gistId === gistId) return;
|
||||
this.loadAlgorithm(params, queryString.parse(search));
|
||||
}
|
||||
}
|
||||
|
||||
toggleHistoryBlock(enable = !this.unblock) {
|
||||
if (enable) {
|
||||
const warningMessage = 'Are you sure you want to discard changes?';
|
||||
window.onbeforeunload = () => {
|
||||
const { saved } = this.props.current;
|
||||
if (!saved) return warningMessage;
|
||||
};
|
||||
this.unblock = this.props.history.block((location) => {
|
||||
if (location.pathname === this.props.location.pathname) return;
|
||||
const { saved } = this.props.current;
|
||||
if (!saved) return warningMessage;
|
||||
});
|
||||
} else {
|
||||
window.onbeforeunload = undefined;
|
||||
this.unblock();
|
||||
this.unblock = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
ignoreHistoryBlock(process) {
|
||||
this.toggleHistoryBlock(false);
|
||||
process();
|
||||
this.toggleHistoryBlock(true);
|
||||
}
|
||||
|
||||
signIn(accessToken) {
|
||||
Cookies.set('access_token', accessToken);
|
||||
GitHubApi.auth(accessToken)
|
||||
.then(() => GitHubApi.getUser())
|
||||
.then(user => {
|
||||
const { login, avatar_url } = user;
|
||||
this.props.setUser({ login, avatar_url });
|
||||
})
|
||||
.then(() => this.loadScratchPapers())
|
||||
.catch(() => this.signOut());
|
||||
}
|
||||
|
||||
signOut() {
|
||||
Cookies.remove('access_token');
|
||||
GitHubApi.auth(undefined)
|
||||
.then(() => {
|
||||
this.props.setUser(undefined);
|
||||
})
|
||||
.then(() => this.props.setScratchPapers([]));
|
||||
}
|
||||
|
||||
loadScratchPapers() {
|
||||
const per_page = 100;
|
||||
const paginateGists = (page = 1, scratchPapers = []) => GitHubApi.listGists({
|
||||
per_page,
|
||||
page,
|
||||
timestamp: Date.now(),
|
||||
}).then(gists => {
|
||||
scratchPapers.push(...gists.filter(gist => 'algorithm-visualizer' in gist.files).map(gist => ({
|
||||
key: gist.id,
|
||||
name: gist.description,
|
||||
files: Object.keys(gist.files),
|
||||
})));
|
||||
if (gists.length < per_page) {
|
||||
return scratchPapers;
|
||||
} else {
|
||||
return paginateGists(page + 1, scratchPapers);
|
||||
}
|
||||
});
|
||||
return paginateGists()
|
||||
.then(scratchPapers => this.props.setScratchPapers(scratchPapers))
|
||||
.catch(this.handleError);
|
||||
}
|
||||
|
||||
loadAlgorithm({ categoryKey, algorithmKey, gistId }, { visualizationId }) {
|
||||
const { ext } = this.props.env;
|
||||
const fetch = () => {
|
||||
if (window.__PRELOADED_ALGORITHM__) {
|
||||
this.props.setAlgorithm(window.__PRELOADED_ALGORITHM__);
|
||||
delete window.__PRELOADED_ALGORITHM__;
|
||||
} else if (window.__PRELOADED_ALGORITHM__ === null) {
|
||||
delete window.__PRELOADED_ALGORITHM__;
|
||||
return Promise.reject(new Error('Algorithm Not Found'));
|
||||
} else if (categoryKey && algorithmKey) {
|
||||
return AlgorithmApi.getAlgorithm(categoryKey, algorithmKey)
|
||||
.then(({ algorithm }) => this.props.setAlgorithm(algorithm));
|
||||
} else if (gistId === 'new' && visualizationId) {
|
||||
return VisualizationApi.getVisualization(visualizationId)
|
||||
.then(content => {
|
||||
this.props.setScratchPaper({
|
||||
login: undefined,
|
||||
gistId,
|
||||
title: 'Untitled',
|
||||
files: [SCRATCH_PAPER_README_MD, createUserFile('visualization.json', JSON.stringify(content))],
|
||||
});
|
||||
});
|
||||
} else if (gistId === 'new') {
|
||||
const language = languages.find(language => language.ext === ext);
|
||||
this.props.setScratchPaper({
|
||||
login: undefined,
|
||||
gistId,
|
||||
title: 'Untitled',
|
||||
files: [SCRATCH_PAPER_README_MD, language.skeleton],
|
||||
});
|
||||
} else if (gistId) {
|
||||
return GitHubApi.getGist(gistId, { timestamp: Date.now() })
|
||||
.then(refineGist)
|
||||
.then(this.props.setScratchPaper);
|
||||
} else {
|
||||
this.props.setHome();
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
fetch()
|
||||
.then(() => {
|
||||
this.selectDefaultTab();
|
||||
return null; // to suppress unnecessary bluebird warning
|
||||
})
|
||||
.catch(error => {
|
||||
this.handleError(error);
|
||||
this.props.history.push('/');
|
||||
});
|
||||
}
|
||||
|
||||
selectDefaultTab() {
|
||||
const { ext } = this.props.env;
|
||||
const { files } = this.props.current;
|
||||
const editingFile = files.find(file => extension(file.name) === 'json') ||
|
||||
files.find(file => extension(file.name) === ext) ||
|
||||
files.find(file => exts.includes(extension(file.name))) ||
|
||||
files[files.length - 1];
|
||||
this.props.setEditingFile(editingFile);
|
||||
}
|
||||
|
||||
handleChangeWorkspaceWeights(workspaceWeights) {
|
||||
this.setState({ workspaceWeights });
|
||||
this.codeEditorRef.current.handleResize();
|
||||
}
|
||||
|
||||
toggleNavigatorOpened(navigatorOpened = !this.state.workspaceVisibles[0]) {
|
||||
const workspaceVisibles = [...this.state.workspaceVisibles];
|
||||
workspaceVisibles[0] = navigatorOpened;
|
||||
this.setState({ workspaceVisibles });
|
||||
}
|
||||
|
||||
handleClickTitleBar() {
|
||||
this.toggleNavigatorOpened();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { workspaceVisibles, workspaceWeights } = this.state;
|
||||
const { titles, description, saved } = this.props.current;
|
||||
|
||||
const title = `${saved ? '' : '(Unsaved) '}${titles.join(' - ')}`;
|
||||
const [navigatorOpened] = workspaceVisibles;
|
||||
|
||||
return (
|
||||
<div className={styles.app}>
|
||||
<Helmet>
|
||||
<title>Educoder</title>
|
||||
<meta name="description" content={description}/>
|
||||
</Helmet>
|
||||
<Header className={styles.header} onClickTitleBar={this.handleClickTitleBar}
|
||||
navigatorOpened={navigatorOpened} loadScratchPapers={this.loadScratchPapers}
|
||||
ignoreHistoryBlock={this.ignoreHistoryBlock}/>
|
||||
<ResizableContainer className={styles.workspace} horizontal weights={workspaceWeights}
|
||||
visibles={workspaceVisibles} onChangeWeights={this.handleChangeWorkspaceWeights}>
|
||||
<Navigator/>
|
||||
<VisualizationViewer className={styles.visualization_viewer}/>
|
||||
<TabContainer className={styles.editor_tab_container}>
|
||||
<CodeEditor ref={this.codeEditorRef}/>
|
||||
</TabContainer>
|
||||
</ResizableContainer>
|
||||
<ToastContainer className={styles.toast_container}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(({ current, env }) => ({ current, env }), actions)(
|
||||
App,
|
||||
);
|
||||
@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
|
||||
class BaseComponent extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleError = this.handleError.bind(this);
|
||||
}
|
||||
|
||||
handleError(error) {
|
||||
if (error.response) {
|
||||
const { data, statusText } = error.response;
|
||||
const message = data ? typeof data === 'string' ? data : JSON.stringify(data) : statusText;
|
||||
console.error(message);
|
||||
this.props.showErrorToast(message);
|
||||
} else {
|
||||
console.error(error.message);
|
||||
this.props.showErrorToast(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default BaseComponent;
|
||||
@ -0,0 +1,79 @@
|
||||
@import "~common/stylesheet/index";
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 0 12px;
|
||||
margin: 0;
|
||||
|
||||
.icon {
|
||||
margin-right: 4px;
|
||||
|
||||
&.image {
|
||||
width: 1.6em;
|
||||
height: 1.6em;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&.reverse {
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.icon {
|
||||
margin-right: 0;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&.icon_only {
|
||||
.icon {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $color-overlay;
|
||||
}
|
||||
|
||||
&.primary {
|
||||
&:hover {
|
||||
background-color: $color-shadow;
|
||||
|
||||
&:active {
|
||||
box-shadow: 0px 0px 10px 3px #1a1a1a inset;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $color-shadow;
|
||||
box-shadow: 0px 0px 10px 3px #1a1a1a inset;
|
||||
font-weight: bold;
|
||||
|
||||
.icon {
|
||||
color: $color-active;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: $color-shadow;
|
||||
|
||||
&:hover {
|
||||
color: rgba($color-font, .8);
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
background-color: $color-shadow;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&.confirming {
|
||||
color: $color-alert;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import faExclamationCircle from '@fortawesome/fontawesome-free-solid/faExclamationCircle';
|
||||
import faSpinner from '@fortawesome/fontawesome-free-solid/faSpinner';
|
||||
import { classes } from 'common/util';
|
||||
import { Ellipsis } from 'components';
|
||||
import styles from './Button.module.scss';
|
||||
|
||||
class Button extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
confirming: false,
|
||||
};
|
||||
|
||||
this.timeout = null;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.timeout) {
|
||||
window.clearTimeout(this.timeout);
|
||||
this.timeout = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let { className, children, to, href, onClick, icon, reverse, selected, disabled, primary, active, confirmNeeded, inProgress, ...rest } = this.props;
|
||||
const { confirming } = this.state;
|
||||
|
||||
if (confirmNeeded) {
|
||||
if (confirming) {
|
||||
className = classes(styles.confirming, className);
|
||||
icon = faExclamationCircle;
|
||||
children = <Ellipsis key="text">Click to Confirm</Ellipsis>;
|
||||
const onClickOriginal = onClick;
|
||||
onClick = () => {
|
||||
if (onClickOriginal) onClickOriginal();
|
||||
if (this.timeout) {
|
||||
window.clearTimeout(this.timeout);
|
||||
this.timeout = undefined;
|
||||
this.setState({ confirming: false });
|
||||
}
|
||||
};
|
||||
} else {
|
||||
to = null;
|
||||
href = null;
|
||||
onClick = () => {
|
||||
this.setState({ confirming: true });
|
||||
this.timeout = window.setTimeout(() => {
|
||||
this.timeout = undefined;
|
||||
this.setState({ confirming: false });
|
||||
}, 2000);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const iconOnly = !children;
|
||||
const props = {
|
||||
className: classes(styles.button, reverse && styles.reverse, selected && styles.selected, disabled && styles.disabled, primary && styles.primary, active && styles.active, iconOnly && styles.icon_only, className),
|
||||
to: disabled ? null : to,
|
||||
href: disabled ? null : href,
|
||||
onClick: disabled ? null : onClick,
|
||||
children: [
|
||||
icon && (
|
||||
typeof icon === 'string' ?
|
||||
<div className={classes(styles.icon, styles.image)} key="icon"
|
||||
style={{ backgroundImage: `url(${icon})` }} /> :
|
||||
<FontAwesomeIcon className={styles.icon} fixedWidth icon={inProgress ? faSpinner : icon} spin={inProgress}
|
||||
key="icon" />
|
||||
),
|
||||
children,
|
||||
],
|
||||
...rest,
|
||||
};
|
||||
|
||||
return to ? (
|
||||
<Link {...props} />
|
||||
) : href ? (
|
||||
<a rel="noopener" target="_blank" {...props} />
|
||||
) : (
|
||||
<div {...props} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Button;
|
||||
|
||||
@ -0,0 +1,63 @@
|
||||
@import "~common/stylesheet/index";
|
||||
|
||||
.code_editor {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
.ace_editor {
|
||||
flex: 1;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
min-width: 0 !important;
|
||||
min-height: 0 !important;
|
||||
|
||||
.current_line_marker {
|
||||
background-color: rgba($color-highlight, 0.4);
|
||||
border: 1px solid $color-highlight;
|
||||
position: absolute;
|
||||
width: 100% !important;
|
||||
|
||||
animation: line_highlight .1s;
|
||||
}
|
||||
|
||||
@keyframes line_highlight {
|
||||
from {
|
||||
background-color: rgba($color-highlight, 0.1);
|
||||
}
|
||||
to {
|
||||
background-color: rgba($color-highlight, 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contributors_viewer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
padding: 4px;
|
||||
background-color: $theme-normal;
|
||||
|
||||
.contributor {
|
||||
height: 28px;
|
||||
padding: 0 6px;
|
||||
font-weight: bold;
|
||||
|
||||
&.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.empty {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.delete {
|
||||
height: $line-height;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,83 @@
|
||||
import React from 'react';
|
||||
import faTrashAlt from '@fortawesome/fontawesome-free-solid/faTrashAlt';
|
||||
import faUser from '@fortawesome/fontawesome-free-solid/faUser';
|
||||
import { classes, extension } from 'common/util';
|
||||
import { actions } from 'reducers';
|
||||
import { connect } from 'react-redux';
|
||||
import { languages } from 'common/config';
|
||||
import { Button, Ellipsis, FoldableAceEditor } from 'components';
|
||||
import styles from './CodeEditor.module.scss';
|
||||
|
||||
class CodeEditor extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.aceEditorRef = React.createRef();
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
this.aceEditorRef.current.resize();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className } = this.props;
|
||||
const { editingFile } = this.props.current;
|
||||
const { user } = this.props.env;
|
||||
const { lineIndicator } = this.props.player;
|
||||
|
||||
if (!editingFile) return null;
|
||||
|
||||
const fileExt = extension(editingFile.name);
|
||||
const language = languages.find(language => language.ext === fileExt);
|
||||
const mode = language ? language.mode :
|
||||
fileExt === 'md' ? 'markdown' :
|
||||
fileExt === 'json' ? 'json' :
|
||||
'plain_text';
|
||||
|
||||
return (
|
||||
<div className={classes(styles.code_editor, className)}>
|
||||
<FoldableAceEditor
|
||||
className={styles.ace_editor}
|
||||
ref={this.aceEditorRef}
|
||||
mode={mode}
|
||||
theme="tomorrow_night_eighties"
|
||||
name="code_editor"
|
||||
editorProps={{ $blockScrolling: true }}
|
||||
onChange={code => this.props.modifyFile(editingFile, code)}
|
||||
markers={lineIndicator ? [{
|
||||
startRow: lineIndicator.lineNumber,
|
||||
startCol: 0,
|
||||
endRow: lineIndicator.lineNumber,
|
||||
endCol: Infinity,
|
||||
className: styles.current_line_marker,
|
||||
type: 'line',
|
||||
inFront: true,
|
||||
_key: lineIndicator.cursor,
|
||||
}] : []}
|
||||
value={editingFile.content}/>
|
||||
<div className={classes(styles.contributors_viewer, className)}>
|
||||
{/* <span className={classes(styles.contributor, styles.label)}>Contributed by</span>
|
||||
{
|
||||
(editingFile.contributors || [user || { login: 'guest', avatar_url: faUser }]).map(contributor => (
|
||||
<Button className={styles.contributor} icon={contributor.avatar_url} key={contributor.login}
|
||||
href={`https://github.com/${contributor.login}`}>
|
||||
{contributor.login}
|
||||
</Button>
|
||||
))
|
||||
} */}
|
||||
<div className={styles.empty}>
|
||||
<div className={styles.empty}/>
|
||||
<Button className={styles.delete} icon={faTrashAlt} primary confirmNeeded
|
||||
onClick={() => this.props.deleteFile(editingFile)}>
|
||||
<Ellipsis>Delete File</Ellipsis>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(({ current, env, player }) => ({ current, env, player }), actions, null, { forwardRef: true })(
|
||||
CodeEditor,
|
||||
);
|
||||
@ -0,0 +1,38 @@
|
||||
@import "~common/stylesheet/index";
|
||||
|
||||
.divider {
|
||||
position: relative;
|
||||
z-index: 97;
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
background-color: $theme-light;
|
||||
content: '';
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
width: 7px;
|
||||
margin: 0 -3px;
|
||||
cursor: ew-resize;
|
||||
|
||||
&:after {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 3px;
|
||||
width: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
height: 7px;
|
||||
margin: -3px 0;
|
||||
cursor: ns-resize;
|
||||
|
||||
&:after {
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 3px;
|
||||
height: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { classes } from 'common/util';
|
||||
import styles from './Divider.module.scss';
|
||||
|
||||
class Divider extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleMouseDown = this.handleMouseDown.bind(this);
|
||||
this.handleMouseMove = this.handleMouseMove.bind(this);
|
||||
this.handleMouseUp = this.handleMouseUp.bind(this);
|
||||
}
|
||||
|
||||
handleMouseDown(e) {
|
||||
this.target = e.target;
|
||||
document.addEventListener('mousemove', this.handleMouseMove);
|
||||
document.addEventListener('mouseup', this.handleMouseUp);
|
||||
}
|
||||
|
||||
handleMouseMove(e) {
|
||||
const { onResize } = this.props;
|
||||
if (onResize) onResize(this.target.parentElement, e.clientX, e.clientY);
|
||||
}
|
||||
|
||||
handleMouseUp(e) {
|
||||
document.removeEventListener('mousemove', this.handleMouseMove);
|
||||
document.removeEventListener('mouseup', this.handleMouseUp);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, horizontal } = this.props;
|
||||
|
||||
return (
|
||||
<div className={classes(styles.divider, horizontal ? styles.horizontal : styles.vertical, className)}
|
||||
onMouseDown={this.handleMouseDown} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Divider;
|
||||
@ -0,0 +1,7 @@
|
||||
@import "~common/stylesheet/index";
|
||||
|
||||
.ellipsis {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import styles from './Ellipsis.module.scss';
|
||||
import { classes } from 'common/util';
|
||||
|
||||
class Ellipsis extends React.Component {
|
||||
render() {
|
||||
const { className, children } = this.props;
|
||||
|
||||
return (
|
||||
<span className={classes(styles.ellipsis, className)}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Ellipsis;
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
@import "~common/stylesheet/index";
|
||||
|
||||
.category {
|
||||
justify-content: space-between;
|
||||
|
||||
.icon {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.expandable_list_item {
|
||||
background-color: $color-shadow;
|
||||
border-bottom: 1px solid $theme-dark;
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import faCaretDown from '@fortawesome/fontawesome-free-solid/faCaretDown';
|
||||
import faCaretRight from '@fortawesome/fontawesome-free-solid/faCaretRight';
|
||||
import styles from './ExpandableListItem.module.scss';
|
||||
import { ListItem } from 'components';
|
||||
import { classes } from 'common/util';
|
||||
|
||||
class ExpandableListItem extends React.Component {
|
||||
render() {
|
||||
const { className, children, opened, ...props } = this.props;
|
||||
|
||||
return opened ? (
|
||||
<div className={classes(styles.expandable_list_item, className)}>
|
||||
<ListItem className={styles.category} {...props}>
|
||||
<FontAwesomeIcon className={styles.icon} fixedWidth icon={faCaretDown} />
|
||||
</ListItem>
|
||||
{children}
|
||||
</div>
|
||||
) : (
|
||||
<ListItem className={classes(styles.category, className)} {...props}>
|
||||
<FontAwesomeIcon className={styles.icon} fixedWidth icon={faCaretRight} />
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ExpandableListItem;
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
import { connect } from 'react-redux';
|
||||
import AceEditor from 'react-ace';
|
||||
import 'brace/mode/plain_text';
|
||||
import 'brace/mode/markdown';
|
||||
import 'brace/mode/json';
|
||||
import 'brace/mode/javascript';
|
||||
import 'brace/mode/c_cpp';
|
||||
import 'brace/mode/java';
|
||||
import 'brace/theme/tomorrow_night_eighties';
|
||||
import 'brace/ext/searchbox';
|
||||
import { actions } from 'reducers';
|
||||
|
||||
class FoldableAceEditor extends AceEditor {
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
|
||||
const { shouldBuild } = this.props.current;
|
||||
if (shouldBuild) this.foldTracers();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState, snapshot) {
|
||||
super.componentDidUpdate(prevProps, prevState, snapshot);
|
||||
|
||||
const { editingFile, shouldBuild } = this.props.current;
|
||||
if (editingFile !== prevProps.current.editingFile) {
|
||||
if (shouldBuild) this.foldTracers();
|
||||
}
|
||||
}
|
||||
|
||||
foldTracers() {
|
||||
const session = this.editor.getSession();
|
||||
for (let row = 0; row < session.getLength(); row++) {
|
||||
if (!/^\s*\/\/.+{\s*$/.test(session.getLine(row))) continue;
|
||||
const range = session.getFoldWidgetRange(row);
|
||||
if (range) {
|
||||
session.addFold('...', range);
|
||||
row = range.end.row;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resize() {
|
||||
this.editor.resize();
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(({ current }) => ({ current }), actions, null, { forwardRef: true })(
|
||||
FoldableAceEditor,
|
||||
);
|
||||
@ -0,0 +1,68 @@
|
||||
@import "~common/stylesheet/index";
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid $theme-light;
|
||||
|
||||
.section {
|
||||
height: $line-height;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
.title_bar {
|
||||
font-size: $font-size-large;
|
||||
font-weight: bold;
|
||||
min-width: 0;
|
||||
|
||||
.nav_arrow {
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.nav_caret {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.input_title {
|
||||
padding: 4px 8px;
|
||||
background-color: $theme-light;
|
||||
}
|
||||
}
|
||||
|
||||
.btn_dropdown {
|
||||
position: relative;
|
||||
font-weight: bold;
|
||||
|
||||
&:active {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
z-index: 98;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
box-shadow: 0 0 8px $color-shadow;
|
||||
background-color: $theme-light;
|
||||
margin-top: $line-height;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.dropdown {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,188 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router';
|
||||
import AutosizeInput from 'react-input-autosize';
|
||||
import screenfull from 'screenfull';
|
||||
import Promise from 'bluebird';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import faAngleRight from '@fortawesome/fontawesome-free-solid/faAngleRight';
|
||||
import faCaretDown from '@fortawesome/fontawesome-free-solid/faCaretDown';
|
||||
import faCaretRight from '@fortawesome/fontawesome-free-solid/faCaretRight';
|
||||
import faCodeBranch from '@fortawesome/fontawesome-free-solid/faCodeBranch';
|
||||
import faExpandArrowsAlt from '@fortawesome/fontawesome-free-solid/faExpandArrowsAlt';
|
||||
import faGithub from '@fortawesome/fontawesome-free-brands/faGithub';
|
||||
import faTrashAlt from '@fortawesome/fontawesome-free-solid/faTrashAlt';
|
||||
import faSave from '@fortawesome/fontawesome-free-solid/faSave';
|
||||
import faFacebook from '@fortawesome/fontawesome-free-brands/faFacebook';
|
||||
import faStar from '@fortawesome/fontawesome-free-solid/faStar';
|
||||
import { GitHubApi } from 'apis';
|
||||
import { classes, refineGist } from 'common/util';
|
||||
import { actions } from 'reducers';
|
||||
import { languages } from 'common/config';
|
||||
import { BaseComponent, Button, Ellipsis, ListItem, Player } from 'components';
|
||||
import styles from './Header.module.scss';
|
||||
|
||||
class Header extends BaseComponent {
|
||||
handleClickFullScreen() {
|
||||
if (screenfull.enabled) {
|
||||
if (screenfull.isFullscreen) {
|
||||
screenfull.exit();
|
||||
} else {
|
||||
screenfull.request();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleChangeTitle(e) {
|
||||
const { value } = e.target;
|
||||
this.props.modifyTitle(value);
|
||||
}
|
||||
|
||||
saveGist() {
|
||||
const { user } = this.props.env;
|
||||
const { scratchPaper, titles, files, lastFiles, editingFile } = this.props.current;
|
||||
const gist = {
|
||||
description: titles[titles.length - 1],
|
||||
files: {},
|
||||
};
|
||||
files.forEach(file => {
|
||||
gist.files[file.name] = {
|
||||
content: file.content,
|
||||
};
|
||||
});
|
||||
lastFiles.forEach(lastFile => {
|
||||
if (!(lastFile.name in gist.files)) {
|
||||
gist.files[lastFile.name] = null;
|
||||
}
|
||||
});
|
||||
gist.files['algorithm-visualizer'] = {
|
||||
content: 'https://algorithm-visualizer.org/',
|
||||
};
|
||||
const save = gist => {
|
||||
if (!user) return Promise.reject(new Error('Sign In Required'));
|
||||
if (scratchPaper && scratchPaper.login) {
|
||||
if (scratchPaper.login === user.login) {
|
||||
return GitHubApi.editGist(scratchPaper.gistId, gist);
|
||||
} else {
|
||||
return GitHubApi.forkGist(scratchPaper.gistId).then(forkedGist => GitHubApi.editGist(forkedGist.id, gist));
|
||||
}
|
||||
}
|
||||
return GitHubApi.createGist(gist);
|
||||
};
|
||||
save(gist)
|
||||
.then(refineGist)
|
||||
.then(newScratchPaper => {
|
||||
this.props.setScratchPaper(newScratchPaper);
|
||||
this.props.setEditingFile(newScratchPaper.files.find(file => file.name === editingFile.name));
|
||||
if (!(scratchPaper && scratchPaper.gistId === newScratchPaper.gistId)) {
|
||||
this.props.history.push(`/scratch-paper/${newScratchPaper.gistId}`);
|
||||
}
|
||||
})
|
||||
.then(this.props.loadScratchPapers)
|
||||
.catch(this.handleError);
|
||||
}
|
||||
|
||||
hasPermission() {
|
||||
const { scratchPaper } = this.props.current;
|
||||
const { user } = this.props.env;
|
||||
if (!scratchPaper) return false;
|
||||
if (scratchPaper.gistId !== 'new') {
|
||||
if (!user) return false;
|
||||
if (scratchPaper.login !== user.login) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
deleteGist() {
|
||||
const { scratchPaper } = this.props.current;
|
||||
const { gistId } = scratchPaper;
|
||||
if (gistId === 'new') {
|
||||
this.props.ignoreHistoryBlock(() => this.props.history.push('/'));
|
||||
} else {
|
||||
GitHubApi.deleteGist(gistId)
|
||||
.then(() => {
|
||||
this.props.ignoreHistoryBlock(() => this.props.history.push('/'));
|
||||
})
|
||||
.then(this.props.loadScratchPapers)
|
||||
.catch(this.handleError);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, onClickTitleBar, navigatorOpened } = this.props;
|
||||
const { scratchPaper, titles, saved } = this.props.current;
|
||||
const { ext, user } = this.props.env;
|
||||
|
||||
const permitted = this.hasPermission();
|
||||
|
||||
return (
|
||||
<header className={classes(styles.header, className)}>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.section}>
|
||||
<Button className={styles.title_bar} onClick={onClickTitleBar}>
|
||||
{/* {
|
||||
titles.map((title, i) => [
|
||||
scratchPaper && i === 1 ?
|
||||
<AutosizeInput className={styles.input_title} key={`title-${i}`} value={title}
|
||||
onClick={e => e.stopPropagation()} onChange={e => this.handleChangeTitle(e)}/> :
|
||||
<Ellipsis key={`title-${i}`}>{title}</Ellipsis>,
|
||||
i < titles.length - 1 &&
|
||||
<FontAwesomeIcon className={styles.nav_arrow} fixedWidth icon={faAngleRight} key={`arrow-${i}`}/>,
|
||||
])
|
||||
} */}
|
||||
<FontAwesomeIcon className={styles.nav_caret} fixedWidth
|
||||
icon={navigatorOpened ? faCaretDown : faCaretRight}/>
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.section}>
|
||||
{/* <Button icon={permitted ? faSave : faCodeBranch} primary disabled={permitted && saved}
|
||||
onClick={() => this.saveGist()}>{permitted ? 'Save' : 'Fork'}</Button>
|
||||
{
|
||||
permitted &&
|
||||
<Button icon={faTrashAlt} primary onClick={() => this.deleteGist()} confirmNeeded>Delete</Button>
|
||||
} */}
|
||||
{/* <Button icon={faFacebook} primary
|
||||
href={`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(window.location.href)}`}>Share</Button> */}
|
||||
<Button icon={faExpandArrowsAlt} primary
|
||||
onClick={() => this.handleClickFullScreen()}>Fullscreen</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.section}>
|
||||
{/* {
|
||||
user ?
|
||||
<Button className={styles.btn_dropdown} icon={user.avatar_url}>
|
||||
{user.login}
|
||||
<div className={styles.dropdown}>
|
||||
<ListItem label="Sign Out" href="/api/auth/destroy" rel="opener"/>
|
||||
</div>
|
||||
</Button> :
|
||||
<Button icon={faGithub} primary href="/api/auth/request" rel="opener">
|
||||
<Ellipsis>Sign In</Ellipsis>
|
||||
</Button>
|
||||
} */}
|
||||
<Button className={styles.btn_dropdown} icon={faStar}>
|
||||
{languages.find(language => language.ext === ext).name}
|
||||
<div className={styles.dropdown}>
|
||||
{
|
||||
languages.map(language => language.ext === ext ? null : (
|
||||
<ListItem key={language.ext} onClick={() => this.props.setExt(language.ext)}
|
||||
label={language.name}/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<Player className={styles.section}/>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(
|
||||
connect(({ current, env }) => ({ current, env }), actions)(
|
||||
Header,
|
||||
),
|
||||
);
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
@import "~common/stylesheet/index";
|
||||
|
||||
.list_item {
|
||||
height: $line-height;
|
||||
|
||||
.label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&.indent {
|
||||
padding-left: 24px;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import styles from './ListItem.module.scss';
|
||||
import { classes } from 'common/util';
|
||||
import { Button, Ellipsis } from 'components';
|
||||
|
||||
class ListItem extends React.Component {
|
||||
render() {
|
||||
const { className, children, indent, label, ...props } = this.props;
|
||||
|
||||
return (
|
||||
<Button className={classes(styles.list_item, indent && styles.indent, className)} {...props}>
|
||||
<Ellipsis className={styles.label}>{label}</Ellipsis>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ListItem;
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
@import "~common/stylesheet/index";
|
||||
|
||||
.navigator {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
|
||||
.search_bar_container {
|
||||
height: $line-height;
|
||||
padding: 0 8px;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
border-bottom: 1px solid $theme-light;
|
||||
|
||||
&:focus-within {
|
||||
background-color: $color-overlay;
|
||||
}
|
||||
|
||||
.search_icon {
|
||||
align-self: center;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.search_bar {
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.algorithm_list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
max-height: 30vh;
|
||||
border-top: 1px solid $theme-light;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,136 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import faSearch from '@fortawesome/fontawesome-free-solid/faSearch';
|
||||
import faCode from '@fortawesome/fontawesome-free-solid/faCode';
|
||||
import faBook from '@fortawesome/fontawesome-free-solid/faBook';
|
||||
import faGithub from '@fortawesome/fontawesome-free-brands/faGithub';
|
||||
import { ExpandableListItem, ListItem } from 'components';
|
||||
import { classes } from 'common/util';
|
||||
import { actions } from 'reducers';
|
||||
import styles from './Navigator.module.scss';
|
||||
|
||||
class Navigator extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
categoriesOpened: {},
|
||||
scratchPaperOpened: true,
|
||||
query: '',
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { algorithm } = this.props.current;
|
||||
if (algorithm) {
|
||||
this.toggleCategory(algorithm.categoryKey, true);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { algorithm } = nextProps.current;
|
||||
if (algorithm) {
|
||||
this.toggleCategory(algorithm.categoryKey, true);
|
||||
}
|
||||
}
|
||||
|
||||
toggleCategory(key, categoryOpened = !this.state.categoriesOpened[key]) {
|
||||
const categoriesOpened = {
|
||||
...this.state.categoriesOpened,
|
||||
[key]: categoryOpened,
|
||||
};
|
||||
this.setState({ categoriesOpened });
|
||||
}
|
||||
|
||||
toggleScratchPaper(scratchPaperOpened = !this.state.scratchPaperOpened) {
|
||||
this.setState({ scratchPaperOpened });
|
||||
}
|
||||
|
||||
handleChangeQuery(e) {
|
||||
const { categories } = this.props.directory;
|
||||
const categoriesOpened = {};
|
||||
const query = e.target.value;
|
||||
categories.forEach(category => {
|
||||
if (this.testQuery(category.name) || category.algorithms.find(algorithm => this.testQuery(algorithm.name))) {
|
||||
categoriesOpened[category.key] = true;
|
||||
}
|
||||
});
|
||||
this.setState({ categoriesOpened, query });
|
||||
}
|
||||
|
||||
testQuery(value) {
|
||||
const { query } = this.state;
|
||||
const refine = string => string.replace(/-/g, ' ').replace(/[^\w ]/g, '');
|
||||
const refinedQuery = refine(query);
|
||||
const refinedValue = refine(value);
|
||||
return new RegExp(`(^| )${refinedQuery}`, 'i').test(refinedValue) ||
|
||||
new RegExp(refinedQuery, 'i').test(refinedValue.split(' ').map(v => v && v[0]).join(''));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { categoriesOpened, scratchPaperOpened, query } = this.state;
|
||||
const { className } = this.props;
|
||||
const { categories, scratchPapers } = this.props.directory;
|
||||
const { algorithm, scratchPaper } = this.props.current;
|
||||
|
||||
const categoryKey = algorithm && algorithm.categoryKey;
|
||||
const algorithmKey = algorithm && algorithm.algorithmKey;
|
||||
const gistId = scratchPaper && scratchPaper.gistId;
|
||||
|
||||
return (
|
||||
<nav className={classes(styles.navigator, className)}>
|
||||
<div className={styles.search_bar_container}>
|
||||
<FontAwesomeIcon fixedWidth icon={faSearch} className={styles.search_icon}/>
|
||||
<input type="text" className={styles.search_bar} aria-label="Search" placeholder="Search ..." autoFocus
|
||||
value={query} onChange={e => this.handleChangeQuery(e)}/>
|
||||
</div>
|
||||
<div className={styles.algorithm_list}>
|
||||
{
|
||||
categories.map(category => {
|
||||
const categoryOpened = categoriesOpened[category.key];
|
||||
let algorithms = category.algorithms;
|
||||
if (!this.testQuery(category.name)) {
|
||||
algorithms = algorithms.filter(algorithm => this.testQuery(algorithm.name));
|
||||
if (!algorithms.length) return null;
|
||||
}
|
||||
return (
|
||||
<ExpandableListItem key={category.key} onClick={() => this.toggleCategory(category.key)}
|
||||
label={category.name}
|
||||
opened={categoryOpened}>
|
||||
{
|
||||
algorithms.map(algorithm => (
|
||||
<ListItem indent key={algorithm.key}
|
||||
selected={category.key === categoryKey && algorithm.key === algorithmKey}
|
||||
to={`/${category.key}/${algorithm.key}`} label={algorithm.name}/>
|
||||
))
|
||||
}
|
||||
</ExpandableListItem>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<div className={styles.footer}>
|
||||
<ExpandableListItem icon={faCode} label="Scratch Paper" onClick={() => this.toggleScratchPaper()}
|
||||
opened={scratchPaperOpened}>
|
||||
<ListItem indent label="New ..." to="/scratch-paper/new"/>
|
||||
{
|
||||
scratchPapers.map(scratchPaper => (
|
||||
<ListItem indent key={scratchPaper.key} selected={scratchPaper.key === gistId}
|
||||
to={`/scratch-paper/${scratchPaper.key}`} label={scratchPaper.name}/>
|
||||
))
|
||||
}
|
||||
</ExpandableListItem>
|
||||
{/* <ListItem icon={faBook} label="API Reference"
|
||||
href="https://github.com/algorithm-visualizer/algorithm-visualizer/wiki"/>
|
||||
<ListItem icon={faGithub} label="Fork me on GitHub"
|
||||
href="https://github.com/algorithm-visualizer/algorithm-visualizer"/> */}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(({ current, directory, env }) => ({ current, directory, env }), actions)(
|
||||
Navigator,
|
||||
);
|
||||
@ -0,0 +1,52 @@
|
||||
@import "~common/stylesheet/index";
|
||||
|
||||
.player {
|
||||
.progress_bar {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.speed {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background-color: $color-shadow;
|
||||
}
|
||||
|
||||
.range {
|
||||
position: relative;
|
||||
height: 16px;
|
||||
width: 60px;
|
||||
margin-left: 8px;
|
||||
|
||||
.range_label_container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.range_track {
|
||||
top: 50%;
|
||||
height: 6px;
|
||||
margin-top: -3px;
|
||||
background-color: $theme-light;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.range_slider {
|
||||
top: 0;
|
||||
width: 6px;
|
||||
height: 12px;
|
||||
margin-left: -3px;
|
||||
margin-top: -3px;
|
||||
appearance: none;
|
||||
background-color: $color-font;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,188 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import InputRange from 'react-input-range';
|
||||
import axios from 'axios';
|
||||
import faPlay from '@fortawesome/fontawesome-free-solid/faPlay';
|
||||
import faChevronLeft from '@fortawesome/fontawesome-free-solid/faChevronLeft';
|
||||
import faChevronRight from '@fortawesome/fontawesome-free-solid/faChevronRight';
|
||||
import faPause from '@fortawesome/fontawesome-free-solid/faPause';
|
||||
import faWrench from '@fortawesome/fontawesome-free-solid/faWrench';
|
||||
import { classes, extension } from 'common/util';
|
||||
import { TracerApi } from 'apis';
|
||||
import { actions } from 'reducers';
|
||||
import { BaseComponent, Button, ProgressBar } from 'components';
|
||||
import styles from './Player.module.scss';
|
||||
|
||||
class Player extends BaseComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
speed: 2,
|
||||
playing: false,
|
||||
building: false,
|
||||
};
|
||||
|
||||
this.tracerApiSource = null;
|
||||
|
||||
this.reset();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { editingFile, shouldBuild } = this.props.current;
|
||||
if (shouldBuild) this.build(editingFile);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { editingFile, shouldBuild } = nextProps.current;
|
||||
if (editingFile !== this.props.current.editingFile) {
|
||||
if (shouldBuild) this.build(editingFile);
|
||||
}
|
||||
}
|
||||
|
||||
reset(commands = []) {
|
||||
const chunks = [{
|
||||
commands: [],
|
||||
lineNumber: undefined,
|
||||
}];
|
||||
while (commands.length) {
|
||||
const command = commands.shift();
|
||||
const { key, method, args } = command;
|
||||
if (key === null && method === 'delay') {
|
||||
const [lineNumber] = args;
|
||||
chunks[chunks.length - 1].lineNumber = lineNumber;
|
||||
chunks.push({
|
||||
commands: [],
|
||||
lineNumber: undefined,
|
||||
});
|
||||
} else {
|
||||
chunks[chunks.length - 1].commands.push(command);
|
||||
}
|
||||
}
|
||||
this.props.setChunks(chunks);
|
||||
this.props.setCursor(0);
|
||||
this.pause();
|
||||
this.props.setLineIndicator(undefined);
|
||||
}
|
||||
|
||||
build(file) {
|
||||
this.reset();
|
||||
if (!file) return;
|
||||
|
||||
if (this.tracerApiSource) this.tracerApiSource.cancel();
|
||||
this.tracerApiSource = axios.CancelToken.source();
|
||||
this.setState({ building: true });
|
||||
|
||||
const ext = extension(file.name);
|
||||
if (ext in TracerApi) {
|
||||
TracerApi[ext]({ code: file.content }, undefined, this.tracerApiSource.token)
|
||||
.then(commands => {
|
||||
this.tracerApiSource = null;
|
||||
this.setState({ building: false });
|
||||
this.reset(commands);
|
||||
this.next();
|
||||
})
|
||||
.catch(error => {
|
||||
if (axios.isCancel(error)) return;
|
||||
this.tracerApiSource = null;
|
||||
this.setState({ building: false });
|
||||
this.handleError(error);
|
||||
});
|
||||
} else {
|
||||
this.setState({ building: false });
|
||||
this.handleError(new Error('Language Not Supported'));
|
||||
}
|
||||
}
|
||||
|
||||
isValidCursor(cursor) {
|
||||
const { chunks } = this.props.player;
|
||||
return 1 <= cursor && cursor <= chunks.length;
|
||||
}
|
||||
|
||||
prev() {
|
||||
this.pause();
|
||||
const cursor = this.props.player.cursor - 1;
|
||||
if (!this.isValidCursor(cursor)) return false;
|
||||
this.props.setCursor(cursor);
|
||||
return true;
|
||||
}
|
||||
|
||||
resume(wrap = false) {
|
||||
this.pause();
|
||||
if (this.next() || (wrap && this.props.setCursor(1))) {
|
||||
const interval = 4000 / Math.pow(Math.E, this.state.speed);
|
||||
this.timer = window.setTimeout(() => this.resume(), interval);
|
||||
this.setState({ playing: true });
|
||||
}
|
||||
}
|
||||
|
||||
pause() {
|
||||
if (this.timer) {
|
||||
window.clearTimeout(this.timer);
|
||||
this.timer = undefined;
|
||||
this.setState({ playing: false });
|
||||
}
|
||||
}
|
||||
|
||||
next() {
|
||||
this.pause();
|
||||
const cursor = this.props.player.cursor + 1;
|
||||
if (!this.isValidCursor(cursor)) return false;
|
||||
this.props.setCursor(cursor);
|
||||
return true;
|
||||
}
|
||||
|
||||
handleChangeSpeed(speed) {
|
||||
this.setState({ speed });
|
||||
}
|
||||
|
||||
handleChangeProgress(progress) {
|
||||
const { chunks } = this.props.player;
|
||||
const cursor = Math.max(1, Math.min(chunks.length, Math.round(progress * chunks.length)));
|
||||
this.pause();
|
||||
this.props.setCursor(cursor);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className } = this.props;
|
||||
const { editingFile } = this.props.current;
|
||||
const { chunks, cursor } = this.props.player;
|
||||
const { speed, playing, building } = this.state;
|
||||
|
||||
return (
|
||||
<div className={classes(styles.player, className)}>
|
||||
<Button icon={faWrench} primary disabled={building} inProgress={building}
|
||||
onClick={() => this.build(editingFile)}>
|
||||
{building ? 'Building' : 'Build'}
|
||||
</Button>
|
||||
{
|
||||
playing ? (
|
||||
<Button icon={faPause} primary active onClick={() => this.pause()}>Pause</Button>
|
||||
) : (
|
||||
<Button icon={faPlay} primary onClick={() => this.resume(true)}>Play</Button>
|
||||
)
|
||||
}
|
||||
<Button icon={faChevronLeft} primary disabled={!this.isValidCursor(cursor - 1)} onClick={() => this.prev()}/>
|
||||
<ProgressBar className={styles.progress_bar} current={cursor} total={chunks.length}
|
||||
onChangeProgress={progress => this.handleChangeProgress(progress)}/>
|
||||
<Button icon={faChevronRight} reverse primary disabled={!this.isValidCursor(cursor + 1)}
|
||||
onClick={() => this.next()}/>
|
||||
<div className={styles.speed}>
|
||||
Speed
|
||||
<InputRange
|
||||
classNames={{
|
||||
inputRange: styles.range,
|
||||
labelContainer: styles.range_label_container,
|
||||
slider: styles.range_slider,
|
||||
track: styles.range_track,
|
||||
}} minValue={0} maxValue={4} step={.5} value={speed}
|
||||
onChange={speed => this.handleChangeSpeed(speed)}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(({ current, player }) => ({ current, player }), actions)(
|
||||
Player,
|
||||
);
|
||||
@ -0,0 +1,31 @@
|
||||
@import "~common/stylesheet/index";
|
||||
|
||||
.progress_bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
background-color: $theme-light;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
|
||||
> * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.active {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
background-color: $color-active;
|
||||
}
|
||||
|
||||
.label {
|
||||
position: absolute;
|
||||
color: $theme-dark;
|
||||
|
||||
.current {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { classes } from 'common/util';
|
||||
import styles from './ProgressBar.module.scss';
|
||||
|
||||
class ProgressBar extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleMouseDown = this.handleMouseDown.bind(this);
|
||||
this.handleMouseMove = this.handleMouseMove.bind(this);
|
||||
this.handleMouseUp = this.handleMouseUp.bind(this);
|
||||
}
|
||||
|
||||
handleMouseDown(e) {
|
||||
this.target = e.target;
|
||||
this.handleMouseMove(e);
|
||||
document.addEventListener('mousemove', this.handleMouseMove);
|
||||
document.addEventListener('mouseup', this.handleMouseUp);
|
||||
}
|
||||
|
||||
handleMouseMove(e) {
|
||||
const { left } = this.target.getBoundingClientRect();
|
||||
const { offsetWidth } = this.target;
|
||||
const { onChangeProgress } = this.props;
|
||||
const progress = (e.clientX - left) / offsetWidth;
|
||||
if (onChangeProgress) onChangeProgress(progress);
|
||||
}
|
||||
|
||||
handleMouseUp(e) {
|
||||
document.removeEventListener('mousemove', this.handleMouseMove);
|
||||
document.removeEventListener('mouseup', this.handleMouseUp);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, total, current } = this.props;
|
||||
|
||||
return (
|
||||
<div className={classes(styles.progress_bar, className)} onMouseDown={this.handleMouseDown}>
|
||||
<div className={styles.active} style={{ width: `${current / total * 100}%` }} />
|
||||
<div className={styles.label}>
|
||||
<span className={styles.current}>{current}</span> / {total}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ProgressBar;
|
||||
@ -0,0 +1,26 @@
|
||||
@import "~common/stylesheet/index";
|
||||
|
||||
.resizable_container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
|
||||
&.horizontal {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
overflow: hidden;
|
||||
|
||||
&.horizontal {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import { classes } from 'common/util';
|
||||
import { Divider } from 'components';
|
||||
import styles from './ResizableContainer.module.scss';
|
||||
|
||||
class ResizableContainer extends React.Component {
|
||||
handleResize(prevIndex, index, targetElement, clientX, clientY) {
|
||||
const { horizontal, visibles, onChangeWeights } = this.props;
|
||||
const weights = [...this.props.weights];
|
||||
|
||||
const { left, top } = targetElement.getBoundingClientRect();
|
||||
const { offsetWidth, offsetHeight } = targetElement.parentElement;
|
||||
const position = horizontal ? clientX - left : clientY - top;
|
||||
const containerSize = horizontal ? offsetWidth : offsetHeight;
|
||||
|
||||
let totalWeight = 0;
|
||||
let subtotalWeight = 0;
|
||||
weights.forEach((weight, i) => {
|
||||
if (visibles && !visibles[i]) return;
|
||||
totalWeight += weight;
|
||||
if (i < index) subtotalWeight += weight;
|
||||
});
|
||||
const newWeight = position / containerSize * totalWeight;
|
||||
let deltaWeight = newWeight - subtotalWeight;
|
||||
deltaWeight = Math.max(deltaWeight, -weights[prevIndex]);
|
||||
deltaWeight = Math.min(deltaWeight, weights[index]);
|
||||
weights[prevIndex] += deltaWeight;
|
||||
weights[index] -= deltaWeight;
|
||||
onChangeWeights(weights);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, children, horizontal, weights, visibles } = this.props;
|
||||
|
||||
const elements = [];
|
||||
let lastIndex = -1;
|
||||
const totalWeight = weights.filter((weight, i) => !visibles || visibles[i])
|
||||
.reduce((sumWeight, weight) => sumWeight + weight, 0);
|
||||
children.forEach((child, i) => {
|
||||
if (!visibles || visibles[i]) {
|
||||
if (~lastIndex) {
|
||||
const prevIndex = lastIndex;
|
||||
elements.push(
|
||||
<Divider key={`divider-${i}`} horizontal={horizontal}
|
||||
onResize={((target, dx, dy) => this.handleResize(prevIndex, i, target, dx, dy))} />,
|
||||
);
|
||||
}
|
||||
elements.push(
|
||||
<div key={i} className={classes(styles.wrapper)} style={{
|
||||
flexGrow: weights[i] / totalWeight,
|
||||
}}>
|
||||
{child}
|
||||
</div>,
|
||||
);
|
||||
lastIndex = i;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={classes(styles.resizable_container, horizontal && styles.horizontal, className)}>
|
||||
{elements}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ResizableContainer;
|
||||
@ -0,0 +1,70 @@
|
||||
@import "~common/stylesheet/index";
|
||||
|
||||
.tab_container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
|
||||
.tab_bar {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
height: $line-height;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 0 12px;
|
||||
margin: 0;
|
||||
border-bottom: 1px solid $theme-light;
|
||||
|
||||
.input_title {
|
||||
input {
|
||||
&:hover,
|
||||
&:focus {
|
||||
margin: -4px;
|
||||
padding: 4px;
|
||||
background-color: $theme-normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border-left: 1px solid $theme-light;
|
||||
border-right: 1px solid $theme-light;
|
||||
margin: 0 -1px;
|
||||
border-bottom: none;
|
||||
background-color: $theme-dark;
|
||||
}
|
||||
|
||||
&.fake {
|
||||
pointer-events: none;
|
||||
|
||||
&:first-child {
|
||||
flex-shrink: 0;
|
||||
width: $line-height / 2;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
background-color: $theme-dark;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import AutosizeInput from 'react-input-autosize';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import faPlus from '@fortawesome/fontawesome-free-solid/faPlus';
|
||||
import { classes } from 'common/util';
|
||||
import { actions } from 'reducers';
|
||||
import { languages } from 'common/config';
|
||||
import styles from './TabContainer.module.scss';
|
||||
|
||||
class TabContainer extends React.Component {
|
||||
handleAddFile() {
|
||||
const { ext } = this.props.env;
|
||||
const { files } = this.props.current;
|
||||
const language = languages.find(language => language.ext === ext);
|
||||
const newFile = { ...language.skeleton };
|
||||
let count = 0;
|
||||
while (files.some(file => file.name === newFile.name)) newFile.name = `code-${++count}.${ext}`;
|
||||
this.props.addFile(newFile);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, children } = this.props;
|
||||
const { editingFile, files } = this.props.current;
|
||||
|
||||
return (
|
||||
<div className={classes(styles.tab_container, className)}>
|
||||
<div className={styles.tab_bar}>
|
||||
<div className={classes(styles.title, styles.fake)}/>
|
||||
{
|
||||
files.map((file, i) => file === editingFile ? (
|
||||
<div className={classes(styles.title, styles.selected)} key={i}
|
||||
onClick={() => this.props.setEditingFile(file)}>
|
||||
<AutosizeInput className={styles.input_title} value={file.name}
|
||||
onClick={e => e.stopPropagation()}
|
||||
onChange={e => this.props.renameFile(file, e.target.value)}/>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.title} key={i} onClick={() => this.props.setEditingFile(file)}>
|
||||
{file.name}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
<div className={styles.title} onClick={() => this.handleAddFile()}>
|
||||
<FontAwesomeIcon fixedWidth icon={faPlus}/>
|
||||
</div>
|
||||
<div className={classes(styles.title, styles.fake)}/>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(({ current, env }) => ({ current, env }), actions)(
|
||||
TabContainer,
|
||||
);
|
||||
@ -0,0 +1,29 @@
|
||||
@import "~common/stylesheet/index";
|
||||
|
||||
.toast_container {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
padding: 16px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.toast {
|
||||
width: 280px;
|
||||
border: 1px solid;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
margin: 8px;
|
||||
white-space: pre-wrap;
|
||||
pointer-events: auto;
|
||||
font-family: $font-family-monospace;
|
||||
|
||||
&.success {
|
||||
border-color: rgb(0, 150, 0);
|
||||
background-color: rgba(0, 120, 0, .8);
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-color: rgb(150, 0, 0);
|
||||
background-color: rgba(120, 0, 0, .8);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { actions } from 'reducers';
|
||||
import { classes } from 'common/util';
|
||||
import styles from './ToastContainer.module.scss';
|
||||
|
||||
class ToastContainer extends React.Component {
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const newToasts = nextProps.toast.toasts.filter(toast => !this.props.toast.toasts.includes(toast));
|
||||
newToasts.forEach(toast => {
|
||||
window.setTimeout(() => this.props.hideToast(toast.id), 3000);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className } = this.props;
|
||||
const { toasts } = this.props.toast;
|
||||
|
||||
return (
|
||||
<div className={classes(styles.toast_container, className)}>
|
||||
{
|
||||
toasts.map(toast => (
|
||||
<div className={classes(styles.toast, styles[toast.type])} key={toast.id}>
|
||||
{toast.message}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(({ toast }) => ({ toast }), actions)(
|
||||
ToastContainer,
|
||||
);
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
@import "~common/stylesheet/index";
|
||||
|
||||
.visualization_viewer {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
min-height: 0;
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { BaseComponent } from 'components';
|
||||
import { actions } from 'reducers';
|
||||
import styles from './VisualizationViewer.module.scss';
|
||||
import * as TracerClasses from 'core/tracers';
|
||||
import * as LayoutClasses from 'core/layouts';
|
||||
import { classes } from 'common/util';
|
||||
|
||||
class VisualizationViewer extends BaseComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.root = null;
|
||||
this.objects = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { chunks, cursor } = this.props.player;
|
||||
this.update(chunks, cursor);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { chunks, cursor } = nextProps.player;
|
||||
const { chunks: oldChunks, cursor: oldCursor } = this.props.player;
|
||||
if (chunks !== oldChunks || cursor !== oldCursor) {
|
||||
this.update(chunks, cursor, oldChunks, oldCursor);
|
||||
}
|
||||
}
|
||||
|
||||
update(chunks, cursor, oldChunks = [], oldCursor = 0) {
|
||||
let applyingChunks;
|
||||
if (cursor > oldCursor) {
|
||||
applyingChunks = chunks.slice(oldCursor, cursor);
|
||||
} else {
|
||||
this.reset();
|
||||
applyingChunks = chunks.slice(0, cursor);
|
||||
}
|
||||
applyingChunks.forEach(chunk => this.applyChunk(chunk));
|
||||
|
||||
const lastChunk = applyingChunks[applyingChunks.length - 1];
|
||||
if (lastChunk && lastChunk.lineNumber !== undefined) {
|
||||
this.props.setLineIndicator({ lineNumber: lastChunk.lineNumber, cursor });
|
||||
} else {
|
||||
this.props.setLineIndicator(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
applyCommand(command) {
|
||||
const { key, method, args } = command;
|
||||
try {
|
||||
if (key === null && method === 'setRoot') {
|
||||
const [root] = args;
|
||||
this.root = this.objects[root];
|
||||
} else if (method === 'destroy') {
|
||||
delete this.objects[key];
|
||||
} else if (method in LayoutClasses) {
|
||||
const [children] = args;
|
||||
const LayoutClass = LayoutClasses[method];
|
||||
this.objects[key] = new LayoutClass(key, key => this.objects[key], children);
|
||||
} else if (method in TracerClasses) {
|
||||
const className = method;
|
||||
const [title = className] = args;
|
||||
const TracerClass = TracerClasses[className];
|
||||
this.objects[key] = new TracerClass(key, key => this.objects[key], title);
|
||||
} else {
|
||||
this.objects[key][method](...args);
|
||||
}
|
||||
} catch (error) {
|
||||
this.handleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
applyChunk(chunk) {
|
||||
chunk.commands.forEach(command => this.applyCommand(command));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className } = this.props;
|
||||
|
||||
return (
|
||||
<div className={classes(styles.visualization_viewer, className)}>
|
||||
{
|
||||
this.root && this.root.render()
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(({ player }) => ({ player }), actions)(
|
||||
VisualizationViewer,
|
||||
);
|
||||
@ -0,0 +1,17 @@
|
||||
export { default as App } from './App';
|
||||
export { default as BaseComponent } from './BaseComponent';
|
||||
export { default as Button } from './Button';
|
||||
export { default as CodeEditor } from './CodeEditor';
|
||||
export { default as Divider } from './Divider';
|
||||
export { default as Ellipsis } from './Ellipsis';
|
||||
export { default as ExpandableListItem } from './ExpandableListItem';
|
||||
export { default as FoldableAceEditor } from './FoldableAceEditor';
|
||||
export { default as Header } from './Header';
|
||||
export { default as ListItem } from './ListItem';
|
||||
export { default as Navigator } from './Navigator';
|
||||
export { default as Player } from './Player';
|
||||
export { default as ProgressBar } from './ProgressBar';
|
||||
export { default as ResizableContainer } from './ResizableContainer';
|
||||
export { default as TabContainer } from './TabContainer';
|
||||
export { default as ToastContainer } from './ToastContainer';
|
||||
export { default as VisualizationViewer } from './VisualizationViewer';
|
||||
@ -0,0 +1,6 @@
|
||||
import { Layout } from 'core/layouts';
|
||||
|
||||
class HorizontalLayout extends Layout {
|
||||
}
|
||||
|
||||
export default HorizontalLayout;
|
||||
@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { ResizableContainer } from 'components';
|
||||
import { HorizontalLayout } from 'core/layouts';
|
||||
|
||||
class Layout {
|
||||
constructor(key, getObject, children) {
|
||||
this.key = key;
|
||||
this.getObject = getObject;
|
||||
this.children = children.map(key => this.getObject(key));
|
||||
this.weights = children.map(() => 1);
|
||||
this.ref = React.createRef();
|
||||
|
||||
this.handleChangeWeights = this.handleChangeWeights.bind(this);
|
||||
}
|
||||
|
||||
add(key, index = this.children.length) {
|
||||
const child = this.getObject(key);
|
||||
this.children.splice(index, 0, child);
|
||||
this.weights.splice(index, 0, 1);
|
||||
}
|
||||
|
||||
remove(key) {
|
||||
const child = this.getObject(key);
|
||||
const index = this.children.indexOf(child);
|
||||
if (~index) {
|
||||
this.children.splice(index, 1);
|
||||
this.weights.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
removeAll() {
|
||||
this.children = [];
|
||||
this.weights = [];
|
||||
}
|
||||
|
||||
handleChangeWeights(weights) {
|
||||
this.weights.splice(0, this.weights.length, ...weights);
|
||||
this.ref.current.forceUpdate();
|
||||
}
|
||||
|
||||
render() {
|
||||
const horizontal = this instanceof HorizontalLayout;
|
||||
|
||||
return (
|
||||
<ResizableContainer key={this.key} ref={this.ref} weights={this.weights} horizontal={horizontal}
|
||||
onChangeWeights={this.handleChangeWeights}>
|
||||
{
|
||||
this.children.map(tracer => tracer && tracer.render())
|
||||
}
|
||||
</ResizableContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Layout;
|
||||
@ -0,0 +1,6 @@
|
||||
import { Layout } from 'core/layouts';
|
||||
|
||||
class VerticalLayout extends Layout {
|
||||
}
|
||||
|
||||
export default VerticalLayout;
|
||||
@ -0,0 +1,3 @@
|
||||
export { default as Layout } from './Layout';
|
||||
export { default as HorizontalLayout } from './HorizontalLayout';
|
||||
export { default as VerticalLayout } from './VerticalLayout';
|
||||
@ -0,0 +1 @@
|
||||
@import "~common/stylesheet/index";
|
||||
@ -0,0 +1,7 @@
|
||||
import { Array2DRenderer } from 'core/renderers';
|
||||
|
||||
class Array1DRenderer extends Array2DRenderer {
|
||||
}
|
||||
|
||||
export default Array1DRenderer;
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
@import "~common/stylesheet/index";
|
||||
|
||||
.array_2d {
|
||||
flex-shrink: 0;
|
||||
display: table;
|
||||
border-collapse: collapse;
|
||||
|
||||
.row {
|
||||
display: table-row;
|
||||
height: 28px;
|
||||
|
||||
.col {
|
||||
display: table-cell;
|
||||
text-align: center;
|
||||
min-width: 28px;
|
||||
background-color: $theme-normal;
|
||||
border: 1px solid $theme-light;
|
||||
padding: 0 4px;
|
||||
|
||||
.value {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: $color-selected;
|
||||
}
|
||||
|
||||
&.patched {
|
||||
background-color: $color-patched;
|
||||
}
|
||||
|
||||
&.index {
|
||||
background: none;
|
||||
border: none;
|
||||
color: $theme-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import { Array1DRenderer, Renderer } from 'core/renderers';
|
||||
import styles from './Array2DRenderer.module.scss';
|
||||
import { classes } from 'common/util';
|
||||
|
||||
class Array2DRenderer extends Renderer {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.togglePan(true);
|
||||
this.toggleZoom(true);
|
||||
}
|
||||
|
||||
renderData() {
|
||||
const { data } = this.props.data;
|
||||
|
||||
const isArray1D = this instanceof Array1DRenderer;
|
||||
let longestRow = data.reduce((longestRow, row) => longestRow.length < row.length ? row : longestRow, []);
|
||||
|
||||
return (
|
||||
<table className={styles.array_2d}
|
||||
style={{ marginLeft: -this.centerX * 2, marginTop: -this.centerY * 2, transform: `scale(${this.zoom})` }}>
|
||||
<tbody>
|
||||
<tr className={styles.row}>
|
||||
{
|
||||
!isArray1D &&
|
||||
<td className={classes(styles.col, styles.index)} />
|
||||
}
|
||||
{
|
||||
longestRow.map((_, i) => (
|
||||
<td className={classes(styles.col, styles.index)} key={i}>
|
||||
<span className={styles.value}>{i}</span>
|
||||
</td>
|
||||
))
|
||||
}
|
||||
</tr>
|
||||
{
|
||||
data.map((row, i) => (
|
||||
<tr className={styles.row} key={i}>
|
||||
{
|
||||
!isArray1D &&
|
||||
<td className={classes(styles.col, styles.index)}>
|
||||
<span className={styles.value}>{i}</span>
|
||||
</td>
|
||||
}
|
||||
{
|
||||
row.map((col, j) => (
|
||||
<td className={classes(styles.col, col.selected && styles.selected, col.patched && styles.patched)}
|
||||
key={j}>
|
||||
<span className={styles.value}>{this.toString(col.value)}</span>
|
||||
</td>
|
||||
))
|
||||
}
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Array2DRenderer;
|
||||
|
||||
@ -0,0 +1 @@
|
||||
@import "~common/stylesheet/index";
|
||||
@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import { Bar } from 'react-chartjs-2';
|
||||
import { Array1DRenderer } from 'core/renderers';
|
||||
import styles from './ChartRenderer.module.scss';
|
||||
|
||||
class ChartRenderer extends Array1DRenderer {
|
||||
renderData() {
|
||||
const { data: [row] } = this.props.data;
|
||||
|
||||
const chartData = {
|
||||
labels: row.map(col => `${col.value}`),
|
||||
datasets: [{
|
||||
backgroundColor: row.map(col => col.patched ? styles.colorPatched : col.selected ? styles.colorSelected : styles.colorFont),
|
||||
data: row.map(col => col.value),
|
||||
}],
|
||||
};
|
||||
return (
|
||||
<Bar data={chartData} options={{
|
||||
scales: {
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}]
|
||||
},
|
||||
animation: false,
|
||||
legend: false,
|
||||
responsive: true,
|
||||
maintainAspectRatio: false
|
||||
}} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ChartRenderer;
|
||||
|
||||
@ -0,0 +1,98 @@
|
||||
@import "~common/stylesheet/index";
|
||||
|
||||
.graph {
|
||||
flex: 1;
|
||||
align-self: stretch;
|
||||
|
||||
.node {
|
||||
.circle {
|
||||
fill: $theme-light;
|
||||
stroke: $color-font;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.id {
|
||||
fill: $color-font;
|
||||
alignment-baseline: central;
|
||||
text-anchor: middle;
|
||||
}
|
||||
|
||||
.weight {
|
||||
fill: white;
|
||||
font-weight: bold;
|
||||
alignment-baseline: central;
|
||||
text-anchor: left;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
.circle {
|
||||
fill: $color-selected;
|
||||
stroke: $color-selected;
|
||||
}
|
||||
}
|
||||
|
||||
&.visited {
|
||||
.circle {
|
||||
fill: $color-patched;
|
||||
stroke: $color-patched;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edge {
|
||||
.line {
|
||||
stroke: $color-font;
|
||||
stroke-width: 2;
|
||||
|
||||
&.directed {
|
||||
marker-end: url(#markerArrow);
|
||||
}
|
||||
}
|
||||
|
||||
.weight {
|
||||
fill: $color-font;
|
||||
alignment-baseline: baseline;
|
||||
text-anchor: middle;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
.line {
|
||||
stroke: $color-selected;
|
||||
|
||||
&.directed {
|
||||
marker-end: url(#markerArrowSelected);
|
||||
}
|
||||
}
|
||||
|
||||
.weight {
|
||||
fill: $color-selected;
|
||||
}
|
||||
}
|
||||
|
||||
&.visited {
|
||||
.line {
|
||||
stroke: $color-patched;
|
||||
|
||||
&.directed {
|
||||
marker-end: url(#markerArrowVisited);
|
||||
}
|
||||
}
|
||||
|
||||
.weight {
|
||||
fill: $color-patched;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.arrow {
|
||||
fill: $color-font;
|
||||
|
||||
&.selected {
|
||||
fill: $color-selected;
|
||||
}
|
||||
|
||||
&.visited {
|
||||
fill: $color-patched;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,126 @@
|
||||
import React from 'react';
|
||||
import { Renderer } from 'core/renderers';
|
||||
import { classes, distance } from 'common/util';
|
||||
import styles from './GraphRenderer.module.scss';
|
||||
|
||||
class GraphRenderer extends Renderer {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.elementRef = React.createRef();
|
||||
this.selectedNode = null;
|
||||
|
||||
this.togglePan(true);
|
||||
this.toggleZoom(true);
|
||||
}
|
||||
|
||||
handleMouseDown(e) {
|
||||
super.handleMouseDown(e);
|
||||
const coords = this.computeCoords(e);
|
||||
const { nodes, dimensions } = this.props.data;
|
||||
const { nodeRadius } = dimensions;
|
||||
this.selectedNode = nodes.find(node => distance(coords, node) <= nodeRadius);
|
||||
}
|
||||
|
||||
handleMouseMove(e) {
|
||||
if (this.selectedNode) {
|
||||
const { x, y } = this.computeCoords(e);
|
||||
const node = this.props.data.findNode(this.selectedNode.id);
|
||||
node.x = x;
|
||||
node.y = y;
|
||||
this.refresh();
|
||||
} else {
|
||||
super.handleMouseMove(e);
|
||||
}
|
||||
}
|
||||
|
||||
computeCoords(e) {
|
||||
const svg = this.elementRef.current;
|
||||
const s = svg.createSVGPoint();
|
||||
s.x = e.clientX;
|
||||
s.y = e.clientY;
|
||||
const { x, y } = s.matrixTransform(svg.getScreenCTM().inverse());
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
renderData() {
|
||||
const { nodes, edges, isDirected, isWeighted, dimensions } = this.props.data;
|
||||
const { baseWidth, baseHeight, nodeRadius, arrowGap, nodeWeightGap, edgeWeightGap } = dimensions;
|
||||
const viewBox = [
|
||||
(this.centerX - baseWidth / 2) * this.zoom,
|
||||
(this.centerY - baseHeight / 2) * this.zoom,
|
||||
baseWidth * this.zoom,
|
||||
baseHeight * this.zoom,
|
||||
];
|
||||
return (
|
||||
<svg className={styles.graph} viewBox={viewBox} ref={this.elementRef}>
|
||||
<defs>
|
||||
<marker id="markerArrow" markerWidth="4" markerHeight="4" refX="2" refY="2" orient="auto">
|
||||
<path d="M0,0 L0,4 L4,2 L0,0" className={styles.arrow} />
|
||||
</marker>
|
||||
<marker id="markerArrowSelected" markerWidth="4" markerHeight="4" refX="2" refY="2" orient="auto">
|
||||
<path d="M0,0 L0,4 L4,2 L0,0" className={classes(styles.arrow, styles.selected)} />
|
||||
</marker>
|
||||
<marker id="markerArrowVisited" markerWidth="4" markerHeight="4" refX="2" refY="2" orient="auto">
|
||||
<path d="M0,0 L0,4 L4,2 L0,0" className={classes(styles.arrow, styles.visited)} />
|
||||
</marker>
|
||||
</defs>
|
||||
{
|
||||
edges.sort((a, b) => a.visitedCount - b.visitedCount).map(edge => {
|
||||
const { source, target, weight, visitedCount, selectedCount } = edge;
|
||||
const sourceNode = this.props.data.findNode(source);
|
||||
const targetNode = this.props.data.findNode(target);
|
||||
if (!sourceNode || !targetNode) return undefined;
|
||||
const { x: sx, y: sy } = sourceNode;
|
||||
let { x: ex, y: ey } = targetNode;
|
||||
const mx = (sx + ex) / 2;
|
||||
const my = (sy + ey) / 2;
|
||||
const dx = ex - sx;
|
||||
const dy = ey - sy;
|
||||
const degree = Math.atan2(dy, dx) / Math.PI * 180;
|
||||
if (isDirected) {
|
||||
const length = Math.sqrt(dx * dx + dy * dy);
|
||||
if (length !== 0) {
|
||||
ex = sx + dx / length * (length - nodeRadius - arrowGap);
|
||||
ey = sy + dy / length * (length - nodeRadius - arrowGap);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<g className={classes(styles.edge, selectedCount && styles.selected, visitedCount && styles.visited)}
|
||||
key={`${source}-${target}`}>
|
||||
<path d={`M${sx},${sy} L${ex},${ey}`} className={classes(styles.line, isDirected && styles.directed)} />
|
||||
{
|
||||
isWeighted &&
|
||||
<g transform={`translate(${mx},${my})`}>
|
||||
<text className={styles.weight} transform={`rotate(${degree})`}
|
||||
y={-edgeWeightGap}>{this.toString(weight)}</text>
|
||||
</g>
|
||||
}
|
||||
</g>
|
||||
);
|
||||
})
|
||||
}
|
||||
{
|
||||
nodes.map(node => {
|
||||
const { id, x, y, weight, visitedCount, selectedCount } = node;
|
||||
return (
|
||||
<g className={classes(styles.node, selectedCount && styles.selected, visitedCount && styles.visited)}
|
||||
key={id} transform={`translate(${x},${y})`}>
|
||||
<circle className={styles.circle} r={nodeRadius} />
|
||||
<text className={styles.id}>{id}</text>
|
||||
{
|
||||
isWeighted &&
|
||||
<text className={styles.weight} x={nodeRadius + nodeWeightGap}>{this.toString(weight)}</text>
|
||||
}
|
||||
</g>
|
||||
);
|
||||
})
|
||||
}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default GraphRenderer;
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
@import "~common/stylesheet/index";
|
||||
|
||||
.log {
|
||||
flex: 1;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
overflow-y: auto;
|
||||
|
||||
.content {
|
||||
padding: 24px;
|
||||
font-family: $font-family-monospace;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { Renderer } from 'core/renderers';
|
||||
import styles from './LogRenderer.module.scss';
|
||||
|
||||
class LogRenderer extends Renderer {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.elementRef = React.createRef();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState, snapshot) {
|
||||
super.componentDidUpdate(prevProps, prevState, snapshot);
|
||||
const div = this.elementRef.current;
|
||||
div.scrollTop = div.scrollHeight;
|
||||
}
|
||||
|
||||
renderData() {
|
||||
const { log } = this.props.data;
|
||||
|
||||
return (
|
||||
<div className={styles.log} ref={this.elementRef}>
|
||||
<div className={styles.content} dangerouslySetInnerHTML={{ __html: log }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default LogRenderer;
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
@import "~common/stylesheet/index";
|
||||
|
||||
.markdown {
|
||||
flex: 1;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
overflow-y: auto;
|
||||
|
||||
.content {
|
||||
padding: 24px;
|
||||
font-size: $font-size-large;
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
import { Renderer } from 'core/renderers';
|
||||
import styles from './MarkdownRenderer.module.scss';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
|
||||
class MarkdownRenderer extends Renderer {
|
||||
renderData() {
|
||||
const { markdown } = this.props.data;
|
||||
|
||||
const heading = ({ level, children, ...rest }) => {
|
||||
const HeadingComponent = [
|
||||
props => <h1 {...props} />,
|
||||
props => <h2 {...props} />,
|
||||
props => <h3 {...props} />,
|
||||
props => <h4 {...props} />,
|
||||
props => <h5 {...props} />,
|
||||
props => <h6 {...props} />,
|
||||
][level - 1];
|
||||
|
||||
const idfy = text => text.toLowerCase().trim().replace(/[^\w \-]/g, '').replace(/ /g, '-');
|
||||
|
||||
const getText = children => {
|
||||
return React.Children.map(children, child => {
|
||||
if (!child) return '';
|
||||
if (typeof child === 'string') return child;
|
||||
if ('props' in child) return getText(child.props.children);
|
||||
return '';
|
||||
}).join('');
|
||||
};
|
||||
|
||||
const id = idfy(getText(children));
|
||||
|
||||
return (
|
||||
<HeadingComponent id={id} {...rest}>
|
||||
{children}
|
||||
</HeadingComponent>
|
||||
);
|
||||
};
|
||||
|
||||
const link = ({ href, ...rest }) => {
|
||||
return /^#/.test(href) ? (
|
||||
<a href={href} {...rest} />
|
||||
) : (
|
||||
<a href={href} rel="noopener" target="_blank" {...rest} />
|
||||
);
|
||||
};
|
||||
|
||||
const image = ({ src, ...rest }) => {
|
||||
let newSrc = src;
|
||||
let style = { maxWidth: '100%' };
|
||||
const CODECOGS = 'https://latex.codecogs.com/svg.latex?';
|
||||
const WIKIMEDIA_IMAGE = 'https://upload.wikimedia.org/wikipedia/';
|
||||
const WIKIMEDIA_MATH = 'https://wikimedia.org/api/rest_v1/media/math/render/svg/';
|
||||
if (src.startsWith(CODECOGS)) {
|
||||
const latex = src.substring(CODECOGS.length);
|
||||
newSrc = `${CODECOGS}\\color{White}${latex}`;
|
||||
} else if (src.startsWith(WIKIMEDIA_IMAGE)) {
|
||||
style.backgroundColor = 'white';
|
||||
} else if (src.startsWith(WIKIMEDIA_MATH)) {
|
||||
style.filter = 'invert(100%)';
|
||||
}
|
||||
return <img src={newSrc} style={style} {...rest} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.markdown}>
|
||||
<ReactMarkdown className={styles.content} source={markdown} renderers={{ heading, link, image }}
|
||||
escapeHtml={false}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MarkdownRenderer;
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
@import "~common/stylesheet/index";
|
||||
|
||||
.renderer {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 0;
|
||||
|
||||
&:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.title {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: $theme-light;
|
||||
color: $color-font;
|
||||
padding: 4px 6px;
|
||||
font-size: $font-size-large;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,111 @@
|
||||
import React from 'react';
|
||||
import styles from './Renderer.module.scss';
|
||||
import { Ellipsis } from 'components';
|
||||
import { classes } from 'common/util';
|
||||
|
||||
class Renderer extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleMouseDown = this.handleMouseDown.bind(this);
|
||||
this.handleMouseMove = this.handleMouseMove.bind(this);
|
||||
this.handleMouseUp = this.handleMouseUp.bind(this);
|
||||
this.handleWheel = this.handleWheel.bind(this);
|
||||
|
||||
this._handleMouseDown = this.handleMouseDown;
|
||||
this._handleWheel = this.handleWheel;
|
||||
this.togglePan(false);
|
||||
this.toggleZoom(false);
|
||||
|
||||
this.lastX = null;
|
||||
this.lastY = null;
|
||||
this.centerX = 0;
|
||||
this.centerY = 0;
|
||||
this.zoom = 1;
|
||||
this.zoomFactor = 1.01;
|
||||
this.zoomMax = 20;
|
||||
this.zoomMin = 1 / 20;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState, snapshot) {
|
||||
}
|
||||
|
||||
togglePan(enable = !this.handleMouseDown) {
|
||||
this.handleMouseDown = enable ? this._handleMouseDown : undefined;
|
||||
}
|
||||
|
||||
toggleZoom(enable = !this.handleWheel) {
|
||||
this.handleWheel = enable ? this._handleWheel : undefined;
|
||||
}
|
||||
|
||||
handleMouseDown(e) {
|
||||
const { clientX, clientY } = e;
|
||||
this.lastX = clientX;
|
||||
this.lastY = clientY;
|
||||
document.addEventListener('mousemove', this.handleMouseMove);
|
||||
document.addEventListener('mouseup', this.handleMouseUp);
|
||||
}
|
||||
|
||||
handleMouseMove(e) {
|
||||
const { clientX, clientY } = e;
|
||||
const dx = clientX - this.lastX;
|
||||
const dy = clientY - this.lastY;
|
||||
this.centerX -= dx;
|
||||
this.centerY -= dy;
|
||||
this.refresh();
|
||||
this.lastX = clientX;
|
||||
this.lastY = clientY;
|
||||
}
|
||||
|
||||
handleMouseUp(e) {
|
||||
document.removeEventListener('mousemove', this.handleMouseMove);
|
||||
document.removeEventListener('mouseup', this.handleMouseUp);
|
||||
}
|
||||
|
||||
handleWheel(e) {
|
||||
e.preventDefault();
|
||||
const { deltaY } = e;
|
||||
this.zoom *= Math.pow(this.zoomFactor, deltaY);
|
||||
this.zoom = Math.min(this.zoomMax, Math.max(this.zoomMin, this.zoom));
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
toString(value) {
|
||||
switch (typeof(value)) {
|
||||
case 'number':
|
||||
return [Number.POSITIVE_INFINITY, Number.MAX_SAFE_INTEGER, 0x7fffffff].includes(value) ? '∞' :
|
||||
[Number.NEGATIVE_INFINITY, Number.MIN_SAFE_INTEGER, -0x80000000].includes(value) ? '-∞' :
|
||||
Number.isInteger(value) ? value.toString() :
|
||||
value.toFixed(3);
|
||||
case 'boolean':
|
||||
return value ? 'T' : 'F';
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
renderData() {
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, title } = this.props;
|
||||
|
||||
return (
|
||||
<div className={classes(styles.renderer, className)} onMouseDown={this.handleMouseDown}
|
||||
onWheel={this.handleWheel}>
|
||||
<Ellipsis className={styles.title}>{title}</Ellipsis>
|
||||
{
|
||||
this.renderData()
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Renderer;
|
||||
|
||||
@ -0,0 +1 @@
|
||||
@import "~common/stylesheet/index";
|
||||
@ -0,0 +1,53 @@
|
||||
import React from 'react'
|
||||
import {Scatter} from 'react-chartjs-2'
|
||||
import Array2DRenderer from '../Array2DRenderer'
|
||||
|
||||
const convertToObjectArray = ([x, y]) => ({ x, y })
|
||||
const colors = ['white', 'green', 'blue', 'red', 'yellow', 'cyan']
|
||||
|
||||
class ScatterRenderer extends Array2DRenderer {
|
||||
renderData() {
|
||||
const { data } = this.props.data
|
||||
|
||||
const datasets = data.map((series, index) => (
|
||||
{
|
||||
backgroundColor: colors[index],
|
||||
data: series.map(s => convertToObjectArray(s.value)),
|
||||
label: Math.random(),
|
||||
radius: (index + 1) * 2,
|
||||
}))
|
||||
|
||||
const chartData = {
|
||||
datasets,
|
||||
}
|
||||
|
||||
return <Scatter data={chartData} options={{
|
||||
legend: false,
|
||||
animation: false,
|
||||
layout: {
|
||||
padding: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
top: 20,
|
||||
bottom: 20,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
yAxes: [
|
||||
{
|
||||
ticks: {
|
||||
beginAtZero: false,
|
||||
},
|
||||
}],
|
||||
xAxes: [
|
||||
{
|
||||
ticks: {
|
||||
beginAtZero: false,
|
||||
},
|
||||
}],
|
||||
},
|
||||
}}/>
|
||||
}
|
||||
}
|
||||
|
||||
export default ScatterRenderer
|
||||
@ -0,0 +1,8 @@
|
||||
export { default as Renderer } from './Renderer';
|
||||
export { default as MarkdownRenderer } from './MarkdownRenderer';
|
||||
export { default as LogRenderer } from './LogRenderer';
|
||||
export { default as Array2DRenderer } from './Array2DRenderer';
|
||||
export { default as Array1DRenderer } from './Array1DRenderer';
|
||||
export { default as ChartRenderer } from './ChartRenderer';
|
||||
export { default as GraphRenderer } from './GraphRenderer';
|
||||
export { default as ScatterRenderer } from './ScatterRenderer';
|
||||
@ -0,0 +1,46 @@
|
||||
import { Array2DTracer } from 'core/tracers';
|
||||
import { Array1DRenderer } from 'core/renderers';
|
||||
|
||||
class Array1DTracer extends Array2DTracer {
|
||||
getRendererClass() {
|
||||
return Array1DRenderer;
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init();
|
||||
this.chartTracer = null;
|
||||
}
|
||||
|
||||
set(array1d = []) {
|
||||
const array2d = [array1d];
|
||||
super.set(array2d);
|
||||
this.syncChartTracer();
|
||||
}
|
||||
|
||||
patch(x, v) {
|
||||
super.patch(0, x, v);
|
||||
}
|
||||
|
||||
depatch(x) {
|
||||
super.depatch(0, x);
|
||||
}
|
||||
|
||||
select(sx, ex = sx) {
|
||||
super.select(0, sx, 0, ex);
|
||||
}
|
||||
|
||||
deselect(sx, ex = sx) {
|
||||
super.deselect(0, sx, 0, ex);
|
||||
}
|
||||
|
||||
chart(key) {
|
||||
this.chartTracer = key ? this.getObject(key) : null;
|
||||
this.syncChartTracer();
|
||||
}
|
||||
|
||||
syncChartTracer() {
|
||||
if (this.chartTracer) this.chartTracer.data = this.data;
|
||||
}
|
||||
}
|
||||
|
||||
export default Array1DTracer;
|
||||
@ -0,0 +1,65 @@
|
||||
import { Tracer } from 'core/tracers';
|
||||
import { Array2DRenderer } from 'core/renderers';
|
||||
|
||||
class Element {
|
||||
constructor(value) {
|
||||
this.value = value;
|
||||
this.patched = false;
|
||||
this.selected = false;
|
||||
}
|
||||
}
|
||||
|
||||
class Array2DTracer extends Tracer {
|
||||
getRendererClass() {
|
||||
return Array2DRenderer;
|
||||
}
|
||||
|
||||
set(array2d = []) {
|
||||
this.data = array2d.map(array1d => [...array1d].map(value => new Element(value)));
|
||||
super.set();
|
||||
}
|
||||
|
||||
patch(x, y, v = this.data[x][y].value) {
|
||||
if (!this.data[x][y]) this.data[x][y] = new Element();
|
||||
this.data[x][y].value = v;
|
||||
this.data[x][y].patched = true;
|
||||
}
|
||||
|
||||
depatch(x, y) {
|
||||
this.data[x][y].patched = false;
|
||||
}
|
||||
|
||||
select(sx, sy, ex = sx, ey = sy) {
|
||||
for (let x = sx; x <= ex; x++) {
|
||||
for (let y = sy; y <= ey; y++) {
|
||||
this.data[x][y].selected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectRow(x, sy, ey) {
|
||||
this.select(x, sy, x, ey);
|
||||
}
|
||||
|
||||
selectCol(y, sx, ex) {
|
||||
this.select(sx, y, ex, y);
|
||||
}
|
||||
|
||||
deselect(sx, sy, ex = sx, ey = sy) {
|
||||
for (let x = sx; x <= ex; x++) {
|
||||
for (let y = sy; y <= ey; y++) {
|
||||
this.data[x][y].selected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deselectRow(x, sy, ey) {
|
||||
this.deselect(x, sy, x, ey);
|
||||
}
|
||||
|
||||
deselectCol(y, sx, ex) {
|
||||
this.deselect(sx, y, ex, y);
|
||||
}
|
||||
}
|
||||
|
||||
export default Array2DTracer;
|
||||
@ -0,0 +1,10 @@
|
||||
import { Array1DTracer } from 'core/tracers';
|
||||
import { ChartRenderer } from 'core/renderers';
|
||||
|
||||
class ChartTracer extends Array1DTracer {
|
||||
getRendererClass() {
|
||||
return ChartRenderer;
|
||||
}
|
||||
}
|
||||
|
||||
export default ChartTracer;
|
||||
@ -0,0 +1,261 @@
|
||||
import { Tracer } from 'core/tracers';
|
||||
import { distance } from 'common/util';
|
||||
import { GraphRenderer } from 'core/renderers';
|
||||
|
||||
class GraphTracer extends Tracer {
|
||||
getRendererClass() {
|
||||
return GraphRenderer;
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init();
|
||||
this.dimensions = {
|
||||
baseWidth: 320,
|
||||
baseHeight: 320,
|
||||
padding: 32,
|
||||
nodeRadius: 12,
|
||||
arrowGap: 4,
|
||||
nodeWeightGap: 4,
|
||||
edgeWeightGap: 4,
|
||||
};
|
||||
this.isDirected = true;
|
||||
this.isWeighted = false;
|
||||
this.callLayout = { method: this.layoutCircle, args: [] };
|
||||
this.logTracer = null;
|
||||
}
|
||||
|
||||
set(array2d = []) {
|
||||
this.nodes = [];
|
||||
this.edges = [];
|
||||
for (let i = 0; i < array2d.length; i++) {
|
||||
this.addNode(i);
|
||||
for (let j = 0; j < array2d.length; j++) {
|
||||
const value = array2d[i][j];
|
||||
if (value) {
|
||||
this.addEdge(i, j, this.isWeighted ? value : null);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.layout();
|
||||
super.set();
|
||||
}
|
||||
|
||||
directed(isDirected = true) {
|
||||
this.isDirected = isDirected;
|
||||
}
|
||||
|
||||
weighted(isWeighted = true) {
|
||||
this.isWeighted = isWeighted;
|
||||
}
|
||||
|
||||
addNode(id, weight = null, x = 0, y = 0, visitedCount = 0, selectedCount = 0) {
|
||||
if (this.findNode(id)) return;
|
||||
this.nodes.push({ id, weight, x, y, visitedCount, selectedCount });
|
||||
this.layout();
|
||||
}
|
||||
|
||||
updateNode(id, weight, x, y, visitedCount, selectedCount) {
|
||||
const node = this.findNode(id);
|
||||
const update = { weight, x, y, visitedCount, selectedCount };
|
||||
Object.keys(update).forEach(key => {
|
||||
if (update[key] === undefined) delete update[key];
|
||||
});
|
||||
Object.assign(node, update);
|
||||
}
|
||||
|
||||
removeNode(id) {
|
||||
const node = this.findNode(id);
|
||||
if (!node) return;
|
||||
const index = this.nodes.indexOf(node);
|
||||
this.nodes.splice(index, 1);
|
||||
this.layout();
|
||||
}
|
||||
|
||||
addEdge(source, target, weight = null, visitedCount = 0, selectedCount = 0) {
|
||||
if (this.findEdge(source, target)) return;
|
||||
this.edges.push({ source, target, weight, visitedCount, selectedCount });
|
||||
this.layout();
|
||||
}
|
||||
|
||||
updateEdge(source, target, weight, visitedCount, selectedCount) {
|
||||
const edge = this.findEdge(source, target);
|
||||
const update = { weight, visitedCount, selectedCount };
|
||||
Object.keys(update).forEach(key => {
|
||||
if (update[key] === undefined) delete update[key];
|
||||
});
|
||||
Object.assign(edge, update);
|
||||
}
|
||||
|
||||
removeEdge(source, target) {
|
||||
const edge = this.findEdge(source, target);
|
||||
if (!edge) return;
|
||||
const index = this.edges.indexOf(edge);
|
||||
this.edges.splice(index, 1);
|
||||
this.layout();
|
||||
}
|
||||
|
||||
findNode(id) {
|
||||
return this.nodes.find(node => node.id === id);
|
||||
}
|
||||
|
||||
findEdge(source, target, isDirected = this.isDirected) {
|
||||
if (isDirected) {
|
||||
return this.edges.find(edge => edge.source === source && edge.target === target);
|
||||
} else {
|
||||
return this.edges.find(edge =>
|
||||
(edge.source === source && edge.target === target) ||
|
||||
(edge.source === target && edge.target === source));
|
||||
}
|
||||
}
|
||||
|
||||
findLinkedEdges(source, isDirected = this.isDirected) {
|
||||
if (isDirected) {
|
||||
return this.edges.filter(edge => edge.source === source);
|
||||
} else {
|
||||
return this.edges.filter(edge => edge.source === source || edge.target === source);
|
||||
}
|
||||
}
|
||||
|
||||
findLinkedNodeIds(source, isDirected = this.isDirected) {
|
||||
const edges = this.findLinkedEdges(source, isDirected);
|
||||
return edges.map(edge => edge.source === source ? edge.target : edge.source);
|
||||
}
|
||||
|
||||
findLinkedNodes(source, isDirected = this.isDirected) {
|
||||
const ids = this.findLinkedNodeIds(source, isDirected);
|
||||
return ids.map(id => this.findNode(id));
|
||||
}
|
||||
|
||||
getRect() {
|
||||
const { baseWidth, baseHeight, padding } = this.dimensions;
|
||||
const left = -baseWidth / 2 + padding;
|
||||
const top = -baseHeight / 2 + padding;
|
||||
const right = baseWidth / 2 - padding;
|
||||
const bottom = baseHeight / 2 - padding;
|
||||
const width = right - left;
|
||||
const height = bottom - top;
|
||||
return { left, top, right, bottom, width, height };
|
||||
}
|
||||
|
||||
layout() {
|
||||
const { method, args } = this.callLayout;
|
||||
method.apply(this, args);
|
||||
}
|
||||
|
||||
layoutCircle() {
|
||||
this.callLayout = { method: this.layoutCircle, args: arguments };
|
||||
const rect = this.getRect();
|
||||
const unitAngle = 2 * Math.PI / this.nodes.length;
|
||||
let angle = -Math.PI / 2;
|
||||
for (const node of this.nodes) {
|
||||
const x = Math.cos(angle) * rect.width / 2;
|
||||
const y = Math.sin(angle) * rect.height / 2;
|
||||
node.x = x;
|
||||
node.y = y;
|
||||
angle += unitAngle;
|
||||
}
|
||||
}
|
||||
|
||||
layoutTree(root = 0, sorted = false) {
|
||||
this.callLayout = { method: this.layoutTree, args: arguments };
|
||||
const rect = this.getRect();
|
||||
|
||||
if (this.nodes.length === 1) {
|
||||
const [node] = this.nodes;
|
||||
node.x = (rect.left + rect.right) / 2;
|
||||
node.y = (rect.top + rect.bottom) / 2;
|
||||
return;
|
||||
}
|
||||
|
||||
let maxDepth = 0;
|
||||
const leafCounts = {};
|
||||
let marked = {};
|
||||
const recursiveAnalyze = (id, depth) => {
|
||||
marked[id] = true;
|
||||
leafCounts[id] = 0;
|
||||
if (maxDepth < depth) maxDepth = depth;
|
||||
const linkedNodeIds = this.findLinkedNodeIds(id, false);
|
||||
for (const linkedNodeId of linkedNodeIds) {
|
||||
if (marked[linkedNodeId]) continue;
|
||||
leafCounts[id] += recursiveAnalyze(linkedNodeId, depth + 1);
|
||||
}
|
||||
if (leafCounts[id] === 0) leafCounts[id] = 1;
|
||||
return leafCounts[id];
|
||||
};
|
||||
recursiveAnalyze(root, 0);
|
||||
|
||||
const hGap = rect.width / leafCounts[root];
|
||||
const vGap = rect.height / maxDepth;
|
||||
marked = {};
|
||||
const recursivePosition = (node, h, v) => {
|
||||
marked[node.id] = true;
|
||||
node.x = rect.left + (h + leafCounts[node.id] / 2) * hGap;
|
||||
node.y = rect.top + v * vGap;
|
||||
const linkedNodes = this.findLinkedNodes(node.id, false);
|
||||
if (sorted) linkedNodes.sort((a, b) => a.id - b.id);
|
||||
for (const linkedNode of linkedNodes) {
|
||||
if (marked[linkedNode.id]) continue;
|
||||
recursivePosition(linkedNode, h, v + 1);
|
||||
h += leafCounts[linkedNode.id];
|
||||
}
|
||||
};
|
||||
const rootNode = this.findNode(root);
|
||||
recursivePosition(rootNode, 0, 0);
|
||||
}
|
||||
|
||||
layoutRandom() {
|
||||
this.callLayout = { method: this.layoutRandom, args: arguments };
|
||||
const rect = this.getRect();
|
||||
const placedNodes = [];
|
||||
for (const node of this.nodes) {
|
||||
do {
|
||||
node.x = rect.left + Math.random() * rect.width;
|
||||
node.y = rect.top + Math.random() * rect.height;
|
||||
} while (placedNodes.find(placedNode => distance(node, placedNode) < 48));
|
||||
placedNodes.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
visit(target, source, weight) {
|
||||
this.visitOrLeave(true, target, source, weight);
|
||||
}
|
||||
|
||||
leave(target, source, weight) {
|
||||
this.visitOrLeave(false, target, source, weight);
|
||||
}
|
||||
|
||||
visitOrLeave(visit, target, source = null, weight) {
|
||||
const edge = this.findEdge(source, target);
|
||||
if (edge) edge.visitedCount += visit ? 1 : -1;
|
||||
const node = this.findNode(target);
|
||||
if (weight !== undefined) node.weight = weight;
|
||||
node.visitedCount += visit ? 1 : -1;
|
||||
if (this.logTracer) {
|
||||
this.logTracer.println(visit ? (source || '') + ' -> ' + target : (source || '') + ' <- ' + target);
|
||||
}
|
||||
}
|
||||
|
||||
select(target, source) {
|
||||
this.selectOrDeselect(true, target, source);
|
||||
}
|
||||
|
||||
deselect(target, source) {
|
||||
this.selectOrDeselect(false, target, source);
|
||||
}
|
||||
|
||||
selectOrDeselect(select, target, source = null) {
|
||||
const edge = this.findEdge(source, target);
|
||||
if (edge) edge.selectedCount += select ? 1 : -1;
|
||||
const node = this.findNode(target);
|
||||
node.selectedCount += select ? 1 : -1;
|
||||
if (this.logTracer) {
|
||||
this.logTracer.println(select ? (source || '') + ' => ' + target : (source || '') + ' <= ' + target);
|
||||
}
|
||||
}
|
||||
|
||||
log(key) {
|
||||
this.logTracer = key ? this.getObject(key) : null;
|
||||
}
|
||||
}
|
||||
|
||||
export default GraphTracer;
|
||||
@ -0,0 +1,28 @@
|
||||
import { sprintf } from 'sprintf-js';
|
||||
import { Tracer } from 'core/tracers';
|
||||
import { LogRenderer } from 'core/renderers';
|
||||
|
||||
class LogTracer extends Tracer {
|
||||
getRendererClass() {
|
||||
return LogRenderer;
|
||||
}
|
||||
|
||||
set(log = '') {
|
||||
this.log = log;
|
||||
super.set();
|
||||
}
|
||||
|
||||
print(message) {
|
||||
this.log += message;
|
||||
}
|
||||
|
||||
println(message) {
|
||||
this.print(message + '\n');
|
||||
}
|
||||
|
||||
printf(format, ...args) {
|
||||
this.print(sprintf(format, ...args));
|
||||
}
|
||||
}
|
||||
|
||||
export default LogTracer;
|
||||
@ -0,0 +1,15 @@
|
||||
import { Tracer } from 'core/tracers';
|
||||
import { MarkdownRenderer } from 'core/renderers';
|
||||
|
||||
class MarkdownTracer extends Tracer {
|
||||
getRendererClass() {
|
||||
return MarkdownRenderer;
|
||||
}
|
||||
|
||||
set(markdown = '') {
|
||||
this.markdown = markdown;
|
||||
super.set();
|
||||
}
|
||||
}
|
||||
|
||||
export default MarkdownTracer;
|
||||
@ -0,0 +1,10 @@
|
||||
import { Array2DTracer } from 'core/tracers';
|
||||
import { ScatterRenderer } from 'core/renderers';
|
||||
|
||||
class ScatterTracer extends Array2DTracer {
|
||||
getRendererClass() {
|
||||
return ScatterRenderer;
|
||||
}
|
||||
}
|
||||
|
||||
export default ScatterTracer;
|
||||
@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { Renderer } from 'core/renderers';
|
||||
|
||||
class Tracer {
|
||||
constructor(key, getObject, title) {
|
||||
this.key = key;
|
||||
this.getObject = getObject;
|
||||
this.title = title;
|
||||
this.init();
|
||||
this.reset();
|
||||
}
|
||||
|
||||
getRendererClass() {
|
||||
return Renderer;
|
||||
}
|
||||
|
||||
init() {
|
||||
}
|
||||
|
||||
render() {
|
||||
const RendererClass = this.getRendererClass();
|
||||
return (
|
||||
<RendererClass key={this.key} title={this.title} data={this} />
|
||||
);
|
||||
}
|
||||
|
||||
set() {
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.set();
|
||||
}
|
||||
}
|
||||
|
||||
export default Tracer;
|
||||
@ -0,0 +1,8 @@
|
||||
export { default as Tracer } from './Tracer';
|
||||
export { default as MarkdownTracer } from './MarkdownTracer';
|
||||
export { default as LogTracer } from './LogTracer';
|
||||
export { default as Array2DTracer } from './Array2DTracer';
|
||||
export { default as Array1DTracer } from './Array1DTracer';
|
||||
export { default as ChartTracer } from './ChartTracer';
|
||||
export { default as GraphTracer } from './GraphTracer';
|
||||
export { default as ScatterTracer} from './ScatterTracer';
|
||||
@ -0,0 +1 @@
|
||||
../../../README.md
|
||||
@ -0,0 +1,12 @@
|
||||
import { createProjectFile, createUserFile } from 'common/util';
|
||||
|
||||
const getName = filePath => filePath.split('/').pop();
|
||||
const getContent = filePath => require('!raw-loader!./' + filePath).default;
|
||||
const readProjectFile = filePath => createProjectFile(getName(filePath), getContent(filePath));
|
||||
const readUserFile = filePath => createUserFile(getName(filePath), getContent(filePath));
|
||||
|
||||
export const CODE_CPP = readUserFile('skeletons/code.cpp');
|
||||
export const CODE_JAVA = readUserFile('skeletons/code.java');
|
||||
export const CODE_JS = readUserFile('skeletons/code.js');
|
||||
export const ROOT_README_MD = readProjectFile('algorithm-visualizer/README.md');
|
||||
export const SCRATCH_PAPER_README_MD = readProjectFile('scratch-paper/README.md');
|
||||
@ -0,0 +1,28 @@
|
||||
# Scratch Paper
|
||||
|
||||
Visualize your own code here!
|
||||
|
||||
## Learning About Tracers
|
||||
|
||||
The project [Algorithm Visualizer](https://github.com/algorithm-visualizer) has a visualization library in each
|
||||
supported language ([JavaScript](https://github.com/algorithm-visualizer/tracers.js)
|
||||
, [C++](https://github.com/algorithm-visualizer/tracers.cpp),
|
||||
and [Java](https://github.com/algorithm-visualizer/tracers.java)) to visualize codes.
|
||||
|
||||
There are five tracers in the library to visualize different types of data:
|
||||
|
||||
- [Array1DTracer](https://github.com/algorithm-visualizer/algorithm-visualizer/wiki/Array1DTracer)
|
||||
- [Array2DTracer](https://github.com/algorithm-visualizer/algorithm-visualizer/wiki/Array2DTracer)
|
||||
- [ChartTracer](https://github.com/algorithm-visualizer/algorithm-visualizer/wiki/ChartTracer)
|
||||
- [GraphTracer](https://github.com/algorithm-visualizer/algorithm-visualizer/wiki/GraphTracer)
|
||||
- [LogTracer](https://github.com/algorithm-visualizer/algorithm-visualizer/wiki/LogTracer)
|
||||
|
||||
There are also randomizers to help you create random data.
|
||||
|
||||
Check out the [API reference](https://github.com/algorithm-visualizer/algorithm-visualizer/wiki) for more information.
|
||||
|
||||
## Making Your Visualization Public
|
||||
|
||||
If you think other people would find your visualization useful, you can add it to the side menu
|
||||
by [contributing to `algorithm-visualizer/algorithms`](https://github.com/algorithm-visualizer/algorithms/blob/master/CONTRIBUTING.md)
|
||||
.
|
||||
@ -0,0 +1,43 @@
|
||||
// import visualization libraries {
|
||||
#include "algorithm-visualizer.h"
|
||||
// }
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
// define tracer variables {
|
||||
Array2DTracer array2dTracer = Array2DTracer("Grid");
|
||||
LogTracer logTracer = LogTracer("Console");
|
||||
// }
|
||||
|
||||
// define input variables
|
||||
std::vector<std::string> messages{
|
||||
"Visualize",
|
||||
"your",
|
||||
"own",
|
||||
"code",
|
||||
"here!",
|
||||
};
|
||||
|
||||
// highlight each line of messages recursively
|
||||
void highlight(int line) {
|
||||
if (line >= messages.size()) return;
|
||||
std::string message = messages[line];
|
||||
// visualize {
|
||||
logTracer.println(message);
|
||||
array2dTracer.selectRow(line, 0, message.size() - 1);
|
||||
Tracer::delay();
|
||||
array2dTracer.deselectRow(line, 0, message.size() - 1);
|
||||
// }
|
||||
highlight(line + 1);
|
||||
}
|
||||
|
||||
int main() {
|
||||
// visualize {
|
||||
Layout::setRoot(VerticalLayout({array2dTracer, logTracer}));
|
||||
array2dTracer.set(messages);
|
||||
Tracer::delay();
|
||||
// }
|
||||
highlight(0);
|
||||
return 0;
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
// import visualization libraries {
|
||||
import org.algorithm_visualizer.*;
|
||||
// }
|
||||
|
||||
class Main {
|
||||
// define tracer variables {
|
||||
Array2DTracer array2dTracer = new Array2DTracer("Grid");
|
||||
LogTracer logTracer = new LogTracer("Console");
|
||||
// }
|
||||
|
||||
// define input variables
|
||||
String[] messages = {
|
||||
"Visualize",
|
||||
"your",
|
||||
"own",
|
||||
"code",
|
||||
"here!",
|
||||
};
|
||||
|
||||
// highlight each line of messages recursively
|
||||
void highlight(int line) {
|
||||
if (line >= messages.length) return;
|
||||
String message = messages[line];
|
||||
// visualize {
|
||||
logTracer.println(message);
|
||||
array2dTracer.selectRow(line, 0, message.length() - 1);
|
||||
Tracer.delay();
|
||||
array2dTracer.deselectRow(line, 0, message.length() - 1);
|
||||
// }
|
||||
highlight(line + 1);
|
||||
}
|
||||
|
||||
Main() {
|
||||
// visualize {
|
||||
Layout.setRoot(new VerticalLayout(new Commander[]{array2dTracer, logTracer}));
|
||||
array2dTracer.set(messages);
|
||||
Tracer.delay();
|
||||
// }
|
||||
highlight(0);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
new Main();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
// import visualization libraries {
|
||||
const { Array2DTracer, Layout, LogTracer, Tracer, VerticalLayout } = require('algorithm-visualizer');
|
||||
// }
|
||||
|
||||
// define tracer variables {
|
||||
const array2dTracer = new Array2DTracer('Grid');
|
||||
const logTracer = new LogTracer('Console');
|
||||
// }
|
||||
|
||||
// define input variables
|
||||
const messages = [
|
||||
'Visualize',
|
||||
'your',
|
||||
'own',
|
||||
'code',
|
||||
'here!',
|
||||
];
|
||||
|
||||
// highlight each line of messages recursively
|
||||
function highlight(line) {
|
||||
if (line >= messages.length) return;
|
||||
const message = messages[line];
|
||||
// visualize {
|
||||
logTracer.println(message);
|
||||
array2dTracer.selectRow(line, 0, message.length - 1);
|
||||
Tracer.delay();
|
||||
array2dTracer.deselectRow(line, 0, message.length - 1);
|
||||
// }
|
||||
highlight(line + 1);
|
||||
}
|
||||
|
||||
(function main() {
|
||||
// visualize {
|
||||
Layout.setRoot(new VerticalLayout([array2dTracer, logTracer]));
|
||||
array2dTracer.set(messages);
|
||||
Tracer.delay();
|
||||
// }
|
||||
highlight(0);
|
||||
})();
|
||||
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { combineReducers, createStore } from 'redux';
|
||||
import { BrowserRouter, Route, Switch } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { routerReducer } from 'react-router-redux';
|
||||
import App from 'components/App';
|
||||
import * as reducers from 'reducers';
|
||||
import './stylesheet.scss';
|
||||
|
||||
const store = createStore(combineReducers({ ...reducers, routing: routerReducer }));
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<Switch>
|
||||
<Route exact path="/scratch-paper/:gistId" component={App}/>
|
||||
<Route exact path="/:categoryKey/:algorithmKey" component={App}/>
|
||||
<Route path="/" component={App}/>
|
||||
</Switch>
|
||||
</BrowserRouter>
|
||||
</Provider>, document.getElementById('root'));
|
||||
@ -0,0 +1,144 @@
|
||||
import { combineActions, createAction, handleActions } from 'redux-actions';
|
||||
import { ROOT_README_MD } from 'files';
|
||||
import { extension, isSaved } from 'common/util';
|
||||
|
||||
const prefix = 'CURRENT';
|
||||
|
||||
const setHome = createAction(`${prefix}/SET_HOME`, () => defaultState);
|
||||
const setAlgorithm = createAction(`${prefix}/SET_ALGORITHM`, ({ categoryKey, categoryName, algorithmKey, algorithmName, files, description }) => ({
|
||||
algorithm: { categoryKey, algorithmKey },
|
||||
titles: [categoryName, algorithmName],
|
||||
files,
|
||||
description,
|
||||
}));
|
||||
const setScratchPaper = createAction(`${prefix}/SET_SCRATCH_PAPER`, ({ login, gistId, title, files }) => ({
|
||||
scratchPaper: { login, gistId },
|
||||
titles: ['Scratch Paper', title],
|
||||
files,
|
||||
description: homeDescription,
|
||||
}));
|
||||
const setEditingFile = createAction(`${prefix}/SET_EDITING_FILE`, file => ({ file }));
|
||||
const modifyTitle = createAction(`${prefix}/MODIFY_TITLE`, title => ({ title }));
|
||||
const addFile = createAction(`${prefix}/ADD_FILE`, file => ({ file }));
|
||||
const renameFile = createAction(`${prefix}/RENAME_FILE`, (file, name) => ({ file, name }));
|
||||
const modifyFile = createAction(`${prefix}/MODIFY_FILE`, (file, content) => ({ file, content }));
|
||||
const deleteFile = createAction(`${prefix}/DELETE_FILE`, file => ({ file }));
|
||||
|
||||
export const actions = {
|
||||
setHome,
|
||||
setAlgorithm,
|
||||
setScratchPaper,
|
||||
setEditingFile,
|
||||
modifyTitle,
|
||||
addFile,
|
||||
modifyFile,
|
||||
deleteFile,
|
||||
renameFile,
|
||||
};
|
||||
|
||||
const homeTitles = ['Algorithm Visualizer'];
|
||||
const homeFiles = [ROOT_README_MD];
|
||||
const homeDescription = 'Algorithm Visualizer is an interactive online platform that visualizes algorithms from code.';
|
||||
const defaultState = {
|
||||
algorithm: {
|
||||
categoryKey: 'algorithm-visualizer',
|
||||
algorithmKey: 'home',
|
||||
},
|
||||
scratchPaper: undefined,
|
||||
titles: homeTitles,
|
||||
files: homeFiles,
|
||||
lastTitles: homeTitles,
|
||||
lastFiles: homeFiles,
|
||||
description: homeDescription,
|
||||
editingFile: undefined,
|
||||
shouldBuild: true,
|
||||
saved: true,
|
||||
};
|
||||
|
||||
export default handleActions({
|
||||
[combineActions(
|
||||
setHome,
|
||||
setAlgorithm,
|
||||
setScratchPaper,
|
||||
)]: (state, { payload }) => {
|
||||
const { algorithm, scratchPaper, titles, files, description } = payload;
|
||||
return {
|
||||
...state,
|
||||
algorithm,
|
||||
scratchPaper,
|
||||
titles,
|
||||
files,
|
||||
lastTitles: titles,
|
||||
lastFiles: files,
|
||||
description,
|
||||
editingFile: undefined,
|
||||
shouldBuild: true,
|
||||
saved: true,
|
||||
};
|
||||
},
|
||||
[setEditingFile]: (state, { payload }) => {
|
||||
const { file } = payload;
|
||||
return {
|
||||
...state,
|
||||
editingFile: file,
|
||||
shouldBuild: true,
|
||||
};
|
||||
},
|
||||
[modifyTitle]: (state, { payload }) => {
|
||||
const { title } = payload;
|
||||
const newState = {
|
||||
...state,
|
||||
titles: [state.titles[0], title],
|
||||
};
|
||||
return {
|
||||
...newState,
|
||||
saved: isSaved(newState),
|
||||
};
|
||||
},
|
||||
[addFile]: (state, { payload }) => {
|
||||
const { file } = payload;
|
||||
const newState = {
|
||||
...state,
|
||||
files: [...state.files, file],
|
||||
editingFile: file,
|
||||
shouldBuild: true,
|
||||
};
|
||||
return {
|
||||
...newState,
|
||||
saved: isSaved(newState),
|
||||
};
|
||||
},
|
||||
[combineActions(
|
||||
renameFile,
|
||||
modifyFile,
|
||||
)]: (state, { payload }) => {
|
||||
const { file, ...update } = payload;
|
||||
const editingFile = { ...file, ...update };
|
||||
const newState = {
|
||||
...state,
|
||||
files: state.files.map(oldFile => oldFile === file ? editingFile : oldFile),
|
||||
editingFile,
|
||||
shouldBuild: extension(editingFile.name) === 'md',
|
||||
};
|
||||
return {
|
||||
...newState,
|
||||
saved: isSaved(newState),
|
||||
};
|
||||
},
|
||||
[deleteFile]: (state, { payload }) => {
|
||||
const { file } = payload;
|
||||
const index = state.files.indexOf(file);
|
||||
const files = state.files.filter(oldFile => oldFile !== file);
|
||||
const editingFile = files[Math.min(index, files.length - 1)];
|
||||
const newState = {
|
||||
...state,
|
||||
files,
|
||||
editingFile,
|
||||
shouldBuild: true,
|
||||
};
|
||||
return {
|
||||
...newState,
|
||||
saved: isSaved(newState),
|
||||
};
|
||||
},
|
||||
}, defaultState);
|
||||
@ -0,0 +1,26 @@
|
||||
import { combineActions, createAction, handleActions } from 'redux-actions';
|
||||
|
||||
const prefix = 'DIRECTORY';
|
||||
|
||||
const setCategories = createAction(`${prefix}/SET_CATEGORIES`, categories => ({ categories }));
|
||||
const setScratchPapers = createAction(`${prefix}/SET_SCRATCH_PAPERS`, scratchPapers => ({ scratchPapers }));
|
||||
|
||||
export const actions = {
|
||||
setCategories,
|
||||
setScratchPapers,
|
||||
};
|
||||
|
||||
const defaultState = {
|
||||
categories: [],
|
||||
scratchPapers: [],
|
||||
};
|
||||
|
||||
export default handleActions({
|
||||
[combineActions(
|
||||
setCategories,
|
||||
setScratchPapers,
|
||||
)]: (state, { payload }) => ({
|
||||
...state,
|
||||
...payload,
|
||||
}),
|
||||
}, defaultState);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue