├── .babelrc
├── .gitmodules
├── src
├── reducers
│ ├── index.js
│ ├── version.js
│ └── state.js
├── index.js
├── server.js
├── actions
│ ├── types.js
│ ├── manipulation.js
│ ├── version.js
│ └── state.js
├── components
│ ├── stateview.js
│ ├── editor.js
│ ├── liveview.js
│ ├── redux.js
│ ├── htmlview.js
│ ├── ace.js
│ └── app.js
├── examples.js
├── proxies.js
├── strategy.js
├── proxy.js
├── rewriting.js
├── builder.js
└── symstr.js
├── README.md
├── test
├── index.html
├── immutability.js
├── first-order.js
├── time-travel.js
├── suite.js
├── helpers.js
├── rewrite.js
└── symstr.js
├── .editorconfig
├── .eslintrc
├── .gitignore
├── LICENSE
├── index.html
├── webpack.tests.config.js
├── webpack.config.js
├── package.json
└── benchmarks
├── plots.py
└── benchmark.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "stage": 0,
3 | "blacklist": "regenerator"
4 | }
5 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "z3"]
2 | path = z3
3 | url = https://github.com/Z3Prover/z3
4 |
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | export { default as version } from './version';
2 | export { default as state } from './state';
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # rde
2 |
3 | Reactive Debugging Environment
4 |
5 | Online demo: [http://levjj.github.io/rde/](http://levjj.github.io/rde/)
6 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Redux from './components/redux';
3 |
4 | require('../node_modules/bootstrap/dist/css/bootstrap.css');
5 |
6 | React.render(
7 | ,
8 | document.getElementById('root')
9 | );
10 |
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Tests: Reactive Variables
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 |
8 | [*]
9 |
10 | # Change these settings to your own preference
11 | indent_style = space
12 | indent_size = 2
13 |
14 | # We recommend you to keep these unchanged
15 | end_of_line = lf
16 | charset = utf-8
17 | trim_trailing_whitespace = true
18 | insert_final_newline = true
19 |
20 | [*.md]
21 | trim_trailing_whitespace = false
22 |
--------------------------------------------------------------------------------
/src/server.js:
--------------------------------------------------------------------------------
1 | /* eslint no-var:0 */
2 | var webpack = require('webpack');
3 | var WebpackDevServer = require('webpack-dev-server');
4 | var config = require('../webpack.config');
5 |
6 | var app = new WebpackDevServer(webpack(config), {
7 | publicPath: config.output.publicPath,
8 | hot: true,
9 | historyApiFallback: true,
10 | stats: {
11 | colors: true
12 | }
13 | });
14 |
15 | app.listen(3000, 'localhost', function onstart(err) {
16 | /* eslint no-console:0 */
17 | if (err) {
18 | console.log(err);
19 | }
20 | console.log('Listening at localhost:3000');
21 | });
22 |
--------------------------------------------------------------------------------
/src/actions/types.js:
--------------------------------------------------------------------------------
1 | export const CHANGE_REQUEST = 'CHANGE_REQUEST';
2 | export const ADD_VERSION = 'ADD_VERSION';
3 | export const ADD_VERSION_FAILED = 'ADD_VERSION_FAILED';
4 | export const SWAP_VERSION = 'SWAP_VERSION';
5 |
6 | export const RESET_STATE = 'RESET_STATE';
7 | export const RESET_STATE_FAILED = 'RESET_STATE_FAILED';
8 | export const SWAP_STATE = 'SWAP_STATE';
9 | export const SWAP_STATE_FAILED = 'SWAP_STATE_FAILED';
10 | export const EVENT_HANDLED = 'EVENT_HANDLED';
11 | export const EVENT_FAILED = 'EVENT_FAILED';
12 | export const TOGGLE_ACTIVE = 'TOGGLE_ACTIVE';
13 |
14 | export const STRING_LIT_CURSOR = 'STRING_LIT_CURSOR';
15 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint-config-airbnb",
3 | "env": {
4 | "browser": true,
5 | "node": true
6 | },
7 | "parser": "babel-eslint",
8 | "rules": {
9 | "react/jsx-uses-react": 2,
10 | "react/jsx-uses-vars": 2,
11 | "react/react-in-jsx-scope": 2,
12 | // Temporarirly disabled due to a possible bug in babel-eslint (todomvc example)
13 | "block-scoped-var": 0,
14 | // Temporarily disabled for test/* until babel/babel-eslint#33 is resolved
15 | "padded-blocks": 0,
16 | "no-console": 0,
17 | "id-length": 0,
18 | "camelcase": [1, {"properties": "always"}],
19 | "comma-dangle": [2, "never"]
20 | },
21 | "plugins": [
22 | "react"
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # node-waf configuration
20 | .lock-wscript
21 |
22 | # Compiled binary addons (http://nodejs.org/api/addons.html)
23 | build/Release
24 |
25 | # Dependency directory
26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
27 | node_modules
28 | benchmarks/*.pdf
29 | benchmarks/*.csv
30 |
31 | static/**/*
32 |
--------------------------------------------------------------------------------
/test/immutability.js:
--------------------------------------------------------------------------------
1 | /* globals describe, before, it */
2 | import {expect} from 'chai';
3 |
4 | import {SWAP_STATE_FAILED} from '../src/actions/types';
5 | import {runRender} from './helpers';
6 |
7 |
8 | export default function tests() {
9 | describe('immutability', () => {
10 |
11 | it('allows render to read the state', () => {
12 | const {dom} = runRender({x: 23}, () => window.state.x);
13 | expect(dom).to.be.equal(23);
14 | });
15 |
16 | it('should not allow render to write to the state', () => {
17 | const res = runRender({x: 23}, () => window.state.x++);
18 | expect(res.type).to.be.equal(SWAP_STATE_FAILED);
19 | expect(res.error).to.be.an.instanceof(TypeError);
20 | });
21 | });
22 | }
23 |
--------------------------------------------------------------------------------
/test/first-order.js:
--------------------------------------------------------------------------------
1 | /* globals describe, before, it */
2 | import {expect} from 'chai';
3 |
4 | import {EVENT_FAILED} from '../src/actions/types';
5 | import {runHandler} from './helpers';
6 |
7 |
8 | export default function tests() {
9 | describe('first-order', () => {
10 | it('allows event handling to write to the state', () => {
11 | const {state} = runHandler({i: 22}, () => window.state.i++);
12 | expect(state).to.be.deep.equal({i: 23});
13 | });
14 |
15 | it('should not allow render to write functions to the state', () => {
16 | const {type} = runHandler({}, () => {
17 | window.state.i = () => 0;
18 | window.state.i();
19 | });
20 | expect(type).to.be.equal(EVENT_FAILED);
21 | });
22 | });
23 | }
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015, Christopher Schuster
2 |
3 | Permission to use, copy, modify, and/or distribute this software for any
4 | purpose with or without fee is hereby granted, provided that the above
5 | copyright notice and this permission notice appear in all copies.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14 |
15 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Reactive Development Environment
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/components/stateview.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import stringify from 'json-stringify-pretty-compact';
3 | import { connect } from 'redux/react';
4 |
5 | import Ace from './ace';
6 | import strategy from '../strategy';
7 |
8 | @connect(state => ({
9 | currState: strategy.current(state)}))
10 | export default class StateView extends Component {
11 | static propTypes = {
12 | currState: PropTypes.any,
13 | active: PropTypes.bool.isRequired
14 | }
15 |
16 | shouldComponentUpdate(nextProps) {
17 | return this.props.active || nextProps.active;
18 | }
19 |
20 | componentDidUpdate(prevProps) {
21 | if (!prevProps.active && this.props.active) {
22 | this.refs.stateace.repaint();
23 | }
24 | }
25 |
26 | render() {
27 | const {currState} = this.props;
28 | return (
29 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/editor.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'redux/react';
3 |
4 | import Ace from './ace';
5 | import { changeReqest } from '../actions/version';
6 |
7 | @connect(state => ({
8 | source: state.version.source,
9 | highlight: state.version.highlight
10 | }))
11 | export default class Editor extends Component {
12 | static propTypes = {
13 | source: PropTypes.string,
14 | highlight: PropTypes.any,
15 | showLineNumbers: PropTypes.bool.isRequired,
16 | dispatch: PropTypes.func.isRequired
17 | }
18 |
19 | onChange(newSource) {
20 | this.props.dispatch(changeReqest(newSource));
21 | }
22 |
23 | render() {
24 | return (
25 |
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/test/time-travel.js:
--------------------------------------------------------------------------------
1 | /* globals describe, before, it */
2 | import {expect} from 'chai';
3 |
4 | import {EVENT_HANDLED} from '../src/actions/types';
5 | import strategy from '../src/strategy';
6 |
7 | import {runHandlerInternal} from './helpers';
8 |
9 | export default function tests() {
10 | describe('time travel', () => {
11 |
12 | it('changes to the state should not affect previous states', () => {
13 | let internal = strategy.add({}, strategy.current({
14 | state: {current: -1}
15 | }));
16 |
17 | let result = runHandlerInternal(internal, 0, () => window.state.i = 23);
18 | expect(result.type).to.be.equal(EVENT_HANDLED);
19 | internal = strategy.add({internal, current: 0}, result.state);
20 |
21 | result = runHandlerInternal(internal, 1, () => window.state.i++);
22 | expect(result.type).to.be.equal(EVENT_HANDLED);
23 | internal = strategy.add({internal, current: 1}, result.state);
24 |
25 | const oldState = strategy.current({
26 | state: {internal, current: 1}
27 | });
28 | expect(oldState.i).to.be.equal(23);
29 | const newState = strategy.current({
30 | state: {internal, current: 2}
31 | });
32 | expect(newState.i).to.be.equal(24);
33 | });
34 | });
35 | }
36 |
--------------------------------------------------------------------------------
/test/suite.js:
--------------------------------------------------------------------------------
1 | /* globals describe, before */
2 |
3 | import current, {simple, proxies, proxy} from '../src/strategy';
4 |
5 | import immutability from './immutability';
6 | import firstOrder from './first-order';
7 | import timeTravel from './time-travel';
8 | import symstr from './symstr';
9 |
10 | describe('simple strategy', () => {
11 |
12 | before(() => {
13 | current.handle = simple.handle;
14 | current.render = simple.render;
15 | current.current = simple.current;
16 | current.add = simple.add;
17 | });
18 |
19 | immutability();
20 | firstOrder();
21 | timeTravel();
22 | });
23 |
24 | describe('proxy wrapping', () => {
25 |
26 | before(() => {
27 | current.handle = proxies.handle;
28 | current.render = proxies.render;
29 | current.current = proxies.current;
30 | current.add = proxies.add;
31 | });
32 |
33 | immutability();
34 | firstOrder();
35 | timeTravel();
36 | });
37 |
38 | describe('single membrane', () => {
39 |
40 | before(() => {
41 | current.handle = proxy.handle;
42 | current.render = proxy.render;
43 | current.current = proxy.current;
44 | current.add = proxy.add;
45 | });
46 |
47 | immutability();
48 | firstOrder();
49 | timeTravel();
50 | });
51 |
52 | describe('tracked strings', () => {
53 | symstr();
54 | });
55 |
56 | import './rewrite';
57 |
58 | export default function() {}
59 |
--------------------------------------------------------------------------------
/webpack.tests.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require('webpack');
3 | var build = !!!process.env.NODE_ENV;
4 | var ExtractTextPlugin = require('extract-text-webpack-plugin');
5 |
6 | module.exports = {
7 | devtool: 'eval',
8 | entry: ['mocha!./test/suite.js'],
9 | output: {
10 | path: path.join(__dirname, 'static'),
11 | filename: 'bundle.tests.js',
12 | publicPath: ''
13 | },
14 | plugins: build ? [new ExtractTextPlugin('style.css', {allChunks: true})] : [
15 | new ExtractTextPlugin('style.css', {allChunks: true}),
16 | new webpack.HotModuleReplacementPlugin(),
17 | new webpack.NoErrorsPlugin()
18 | ],
19 | resolve: {
20 | modulesDirectories: [
21 | 'node_modules'
22 | ],
23 | extensions: ['', '.json', '.js']
24 | },
25 | module: {
26 | preLoaders: [{
27 | test: /\.json$/,
28 | loader: 'json'
29 | }],
30 | loaders: [
31 | {test: /\.js$/, exclude: /node_modules/, loaders: ['react-hot', 'babel']},
32 | {test: /\.jsx$/, loader: 'babel'},
33 | {test: /\.css$/, loader: ExtractTextPlugin.extract('style', 'css')},
34 | {test: /\.woff$/, loader: 'url?limit=10000&mimetype=application/font-woff' },
35 | {test: /\.woff2$/, loader: 'url?limit=10000&mimetype=application/font-woff2' },
36 | {test: /\.ttf$/, loader: 'url?limit=10000&mimetype=application/octet-stream' },
37 | {test: /\.eot$/, loader: 'file' },
38 | {test: /\.svg$/, loader: 'url?limit=10000&mimetype=image/svg+xml' }
39 | ]
40 | }
41 | };
42 |
--------------------------------------------------------------------------------
/test/helpers.js:
--------------------------------------------------------------------------------
1 | import {expect} from 'chai';
2 |
3 | import {refresh} from '../src/actions/state';
4 | import {ADD_VERSION, ADD_VERSION_FAILED} from '../src/actions/types';
5 | import {addVersion} from '../src/actions/version';
6 | import {wrapHandler} from '../src/builder';
7 | import strategy from '../src/strategy';
8 |
9 | export function runRenderInternal(internal, current, func) {
10 | const action = refresh(func);
11 | return action(null, () => ({
12 | state: {
13 | internal,
14 | current
15 | }
16 | }));
17 | }
18 |
19 | export function runRender(state, func) {
20 | return runRenderInternal(strategy.add({}, state), 0, func);
21 | }
22 |
23 | export function runHandlerInternal(internal, current, func) {
24 | let result;
25 | const dispatch = (act) => result = act(null, () => ({
26 | state: {
27 | internal,
28 | current,
29 | isActive: true
30 | },
31 | version: {
32 | versions: [{source: '', init: () => '', render: () => ''}],
33 | current: 0
34 | }
35 | }));
36 | try {
37 | wrapHandler(dispatch, func)();
38 | } finally {
39 | return result;
40 | }
41 | }
42 |
43 | export function runHandler(state, func) {
44 | return runHandlerInternal(strategy.add({}, state), 0, func);
45 | }
46 |
47 | export function rewrite(src) {
48 | const action = addVersion(src)();
49 | if (action.error) console.error(action.error, action.error.stack);
50 | expect(action.type).to.be.equal(ADD_VERSION);
51 | return [action.init, action.render];
52 | }
53 |
54 | export function shouldFail(src) {
55 | const action = addVersion(src)();
56 | expect(action.type).to.be.equal(ADD_VERSION_FAILED);
57 | }
58 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require('webpack');
3 | var build = !!!process.env.NODE_ENV;
4 | var ExtractTextPlugin = require('extract-text-webpack-plugin');
5 |
6 | module.exports = {
7 | devtool: 'eval',
8 | entry: build ? ['./src/index'] : [
9 | 'webpack-dev-server/client?http://localhost:3000',
10 | 'webpack/hot/only-dev-server',
11 | './src/index'
12 | ],
13 | output: {
14 | path: path.join(__dirname, 'static'),
15 | filename: 'bundle.js',
16 | publicPath: build ? '' : '/static/'
17 | },
18 | plugins: build ? [new ExtractTextPlugin('style.css', {allChunks: true})] : [
19 | new ExtractTextPlugin('style.css', {allChunks: true}),
20 | new webpack.HotModuleReplacementPlugin(),
21 | new webpack.NoErrorsPlugin()
22 | ],
23 | resolve: {
24 | modulesDirectories: [
25 | 'node_modules'
26 | ],
27 | extensions: ['', '.json', '.js']
28 | },
29 | module: {
30 | preLoaders: [{
31 | test: /\.json$/,
32 | loader: 'json'
33 | }],
34 | loaders: [
35 | {test: /\.js$/, exclude: /node_modules/, loaders: ['react-hot', 'babel']},
36 | {test: /\.jsx$/, loader: 'babel'},
37 | {test: /\.css$/, loader: ExtractTextPlugin.extract('style', 'css')},
38 | {test: /\.woff$/, loader: 'url?limit=10000&mimetype=application/font-woff' },
39 | {test: /\.woff2$/, loader: 'url?limit=10000&mimetype=application/font-woff2' },
40 | {test: /\.ttf$/, loader: 'url?limit=10000&mimetype=application/octet-stream' },
41 | {test: /\.eot$/, loader: 'file' },
42 | {test: /\.svg$/, loader: 'url?limit=10000&mimetype=image/svg+xml' }
43 | ]
44 | }
45 | };
46 |
--------------------------------------------------------------------------------
/src/components/liveview.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import $ from 'jquery';
3 | import { connect } from 'redux/react';
4 |
5 | import {build} from '../builder';
6 | import {typeOf} from '../symstr';
7 |
8 | @connect(state => ({
9 | dom: state.state.dom,
10 | isActive: state.state.isActive
11 | }))
12 | export default class LiveView extends Component {
13 | static propTypes = {
14 | dom: PropTypes.any,
15 | isActive: PropTypes.bool.isRequired,
16 | dispatch: PropTypes.func.isRequired
17 | }
18 |
19 | componentDidMount() {
20 | const view = React.findDOMNode(this.refs.view);
21 | const {dom, isActive, dispatch} = this.props;
22 | $(view).empty();
23 | $(view).append(build(dom, dispatch, !isActive));
24 | }
25 |
26 | shouldComponentUpdate(nextProps) {
27 | function rec(left, right) {
28 | if (typeOf(left) !== typeOf(right)) return true;
29 | if (typeOf(left) === 'string') return `${left}` !== `${right}`;
30 | if (typeOf(left) !== 'object') return left !== right;
31 | const leftKeys = Object.keys(left);
32 | const rightKeys = Object.keys(right);
33 | if (leftKeys.length !== rightKeys.length) return true;
34 | for (let i = 0; i < leftKeys.length; i++) {
35 | const key = leftKeys[i];
36 | if (key !== rightKeys[i]) return true;
37 | if (rec(left[key], right[key])) return true;
38 | }
39 | return false;
40 | }
41 | return this.props.isActive !== nextProps.isActive ||
42 | rec(this.props.dom, nextProps.dom);
43 | }
44 |
45 | componentWillUpdate() {
46 | const view = React.findDOMNode(this.refs.view);
47 | $(view).empty();
48 | }
49 |
50 | componentDidUpdate() {
51 | const view = React.findDOMNode(this.refs.view);
52 | const {dom, isActive, dispatch} = this.props;
53 | $(view).append(build(dom, dispatch, !isActive));
54 | }
55 |
56 | render() {
57 | return (
58 |
59 | );
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/actions/manipulation.js:
--------------------------------------------------------------------------------
1 | import {
2 | STRING_LIT_CURSOR
3 | } from './types';
4 |
5 | import {changeReqest} from './version';
6 |
7 | import {currentVersion} from '../reducers/version';
8 |
9 | export function strIndexOf(str, row, column) {
10 | const parts = `${str}`.split('\n');
11 | let idx = 0;
12 | for (let i = 0; i < row; i++) {
13 | idx += parts[i].length + 1;
14 | }
15 | return idx + column;
16 | }
17 |
18 | export function firstDifference(strA, strB) {
19 | const lim = Math.min(strA.length, strB.length);
20 | for (let i = 0; i < lim; i++) {
21 | if (strA[i] !== strB[i]) return i;
22 | }
23 | return lim;
24 | }
25 |
26 | export function stringLitCursor(id) {
27 | return {
28 | type: STRING_LIT_CURSOR,
29 | id
30 | };
31 | }
32 |
33 | function findStartIdx(state, id, idx) {
34 | const {source, mapping} = currentVersion(state);
35 | const {line, column} = mapping[id].start;
36 | const sIdx = strIndexOf(source, line - 1, column + idx) + 1;
37 | if (mapping[id].extra) {
38 | const start = mapping[id].extra.start;
39 | const eIdx = strIndexOf(source, start.line - 1, start.column + idx) + 1;
40 | return [sIdx, eIdx];
41 | }
42 | return [sIdx];
43 | }
44 |
45 | export function stringLitInsert(id, idx, insertStr) {
46 | return (dispatch, getState) => {
47 | const startIdxs = findStartIdx(getState(), id, idx);
48 | const {source} = currentVersion(getState());
49 | let newSource = source;
50 | let adapt = 1;
51 | for (const startIdx of startIdxs) {
52 | newSource = newSource.substr(0, startIdx + adapt) + insertStr + newSource.substr(startIdx + adapt);
53 | adapt += insertStr.length;
54 | }
55 | return changeReqest(newSource);
56 | };
57 | }
58 |
59 | export function stringLitDelete(id, idx, delLength) {
60 | return (dispatch, getState) => {
61 | const startIdxs = findStartIdx(getState(), id, idx);
62 | const {source} = currentVersion(getState());
63 | let newSource = source;
64 | let adapt = 0;
65 | for (const startIdx of startIdxs) {
66 | newSource = newSource.substr(0, startIdx + adapt) + newSource.substr(startIdx + adapt + delLength);
67 | adapt -= delLength;
68 | }
69 | return changeReqest(newSource);
70 | };
71 | }
72 |
--------------------------------------------------------------------------------
/src/components/redux.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import App from './app';
3 | import { createRedux, createDispatcher, composeStores } from 'redux';
4 | import { Provider } from 'redux/react';
5 | import $ from 'jquery';
6 |
7 | import * as stores from '../reducers/index';
8 | import {addVersion} from '../actions/version';
9 | import {frame, reset} from '../actions/state';
10 | import {counter} from '../examples';
11 |
12 | const store = composeStores(stores);
13 |
14 | let redux;
15 | function middleware(getState) {
16 | return (next) => (act) => {
17 | const action = typeof act === 'function'
18 | ? act(redux.dispatch, getState) : act;
19 | const { promise, type, types, ...rest } = action;
20 | if (type) {
21 | if (type === 'noop') return null;
22 | }
23 | action.dispatch = redux.dispatch;
24 | if (!promise) {
25 | return next(action);
26 | }
27 |
28 | const [REQUEST, SUCCESS, FAILURE] = types;
29 | next({...rest, type: REQUEST});
30 | return promise.then(
31 | (result) => next({...rest, result, type: SUCCESS}),
32 | (error) => next({...rest, error, type: FAILURE})
33 | );
34 | };
35 | }
36 |
37 | const dispatcher = createDispatcher(store, (getState) => [middleware(getState)]);
38 |
39 | redux = createRedux(dispatcher);
40 |
41 | const params = (() => {
42 | const raw = window.location.href.match(/#(.*)$/);
43 | if (!raw) return {};
44 | const props = raw[1].split(/,/);
45 | const src = props.reduce((acc, p) => acc || p.startsWith('src=') && p, false);
46 | return {
47 | src: src && decodeURIComponent(src.split(/=/)[1]),
48 | isDemo: props.indexOf('demo') >= 0,
49 | hideTime: props.indexOf('notime') >= 0
50 | };
51 | })();
52 |
53 | export default class Redux extends Component {
54 |
55 | componentWillMount() {
56 | if (params.isDemo) {
57 | $('#banner').remove();
58 | }
59 | redux.dispatch(addVersion(params.src || counter));
60 | redux.dispatch(reset());
61 | }
62 |
63 | componentDidMount() {
64 | setInterval(() => redux.dispatch(frame()), 25);
65 | }
66 |
67 | render() {
68 | return (
69 |
70 | {() => }
72 |
73 | );
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rde",
3 | "version": "0.0.0",
4 | "description": "Reactive Debugger Environment",
5 | "main": "server.js",
6 | "scripts": {
7 | "start": "NODE_ENV=\"development\" node src/server.js",
8 | "lint": "eslint src test",
9 | "build": "node ./node_modules/webpack/bin/webpack.js --verbose --colors --display-error-details --config webpack.config.js",
10 | "test": "node ./node_modules/webpack/bin/webpack.js --verbose --colors --display-error-details --config webpack.tests.config.js",
11 | "perf": "mocha -t 0 --harmony --harmony_proxies --compilers js:babel-core/register benchmarks/benchmark.js",
12 | "gh-pages": "git checkout gh-pages ; git rebase master ; npm run build ; git commit -a -m build ; git push -f ; git checkout master"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "https://github.com/levjj/rde.git"
17 | },
18 | "license": "ISC",
19 | "homepage": "https://github.com/levjj/rde",
20 | "dependencies": {
21 | "bootstrap": "^3.3.6",
22 | "brace": "^0.5.1",
23 | "clone": "^1.0.2",
24 | "component-file-picker": "^0.2.1",
25 | "deep-freeze": "0.0.1",
26 | "escodegen": "^1.6.1",
27 | "escope": "^3.2.0",
28 | "esprima-fb": "^15001.1001.0-dev-harmony-fb",
29 | "estraverse": "^4.1.0",
30 | "estraverse-fb": "^1.3.1",
31 | "jquery": "^2.1.4",
32 | "json-stringify-pretty-compact": "^1.0.1",
33 | "lodash": "^3.10.1",
34 | "react": "^0.14.6",
35 | "react-ace": "^2.0.2",
36 | "react-bootstrap": "^0.28.3",
37 | "react-dom": "^0.14.6",
38 | "redux": "^0.12.0"
39 | },
40 | "devDependencies": {
41 | "babel-eslint": "^4.0.5",
42 | "babel-loader": "5.1.4",
43 | "chai": "^3.3.0",
44 | "css-loader": "^0.19.0",
45 | "eslint": "^1.9.0",
46 | "eslint-config-airbnb": "^1.0.0",
47 | "eslint-plugin-react": "^3.1.0",
48 | "extract-text-webpack-plugin": "^0.8.2",
49 | "file-loader": "^0.8.4",
50 | "json-loader": "^0.5.2",
51 | "mbench": "github:levjj/mbench",
52 | "mocha": "^2.4.5",
53 | "mocha-loader": "^0.7.1",
54 | "node-libs-browser": "^0.5.2",
55 | "raw-loader": "^0.5.1",
56 | "react-hot-loader": "^1.2.7",
57 | "regenerator": "^0.8.42",
58 | "style-loader": "^0.12.4",
59 | "url-loader": "^0.5.6",
60 | "webpack": "^1.10.5",
61 | "webpack-dev-server": "^1.10.1"
62 | },
63 | "volta": {
64 | "node": "8.6.0"
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/actions/version.js:
--------------------------------------------------------------------------------
1 | import {
2 | CHANGE_REQUEST,
3 | ADD_VERSION,
4 | ADD_VERSION_FAILED,
5 | SWAP_VERSION
6 | } from './types';
7 |
8 | import {analyze} from 'escope';
9 | import {parse} from 'esprima-fb';
10 | import {generate} from 'escodegen';
11 |
12 | import {rewriteJSX, rewriteState, rewriteOps, rewriteSymStrings} from '../rewriting';
13 | import {SymString, operators} from '../symstr.js';
14 | import {refresh} from './state';
15 |
16 | export function changeReqest(source) {
17 | return {
18 | type: CHANGE_REQUEST,
19 | source: source
20 | };
21 | }
22 |
23 | function check(ast) {
24 | const scopeManager = analyze(ast, {optimistic: true});
25 | const globalScope = scopeManager.globalScope;
26 | for (const {identifier: {name}, resolved} of globalScope.through) {
27 | if (resolved === null && name !== 'Math') {
28 | throw new ReferenceError(`${name} is not defined`);
29 | }
30 | }
31 | if (globalScope.thisFound) {
32 | throw new ReferenceError('this is not defined');
33 | }
34 | if (!globalScope.variables.some((v) => v.name === 'view' &&
35 | v.defs.length === 1 &&
36 | v.defs[0].type === 'FunctionName')) {
37 | throw new Error('Expected a "view" function');
38 | }
39 | }
40 |
41 | export function addVersion(source) {
42 | return () => {
43 | try {
44 | let ast = parse(source, {loc: true});
45 | check(ast);
46 | ast = rewriteState(rewriteJSX(ast));
47 | const astAndMapping = rewriteSymStrings(ast);
48 | ast = rewriteOps(astAndMapping.ast);
49 | const generated = generate(ast);
50 | const wrapped = `(function(){\n"use strict";\n${generated} })`;
51 | /* eslint no-eval:0 */
52 | global.operators = operators;
53 | global.sym = SymString.single;
54 | const [init, render] = eval(wrapped)();
55 | return {
56 | type: ADD_VERSION,
57 | source,
58 | init,
59 | render,
60 | mapping: astAndMapping.mapping
61 | };
62 | } catch (e) {
63 | return {
64 | type: ADD_VERSION_FAILED,
65 | error: e
66 | };
67 | }
68 | };
69 | }
70 |
71 | export function swapVersion(idx) {
72 | return (dispatch, getState) => {
73 | dispatch(refresh(getState().version.versions[idx].render));
74 | return {
75 | type: SWAP_VERSION,
76 | idx: idx
77 | };
78 | };
79 | }
80 |
--------------------------------------------------------------------------------
/benchmarks/plots.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 |
4 | import numpy as np
5 | import matplotlib.pyplot as plt
6 |
7 |
8 | data = np.genfromtxt('data.csv',
9 | delimiter=',',
10 | names=True,
11 | dtype="S9,S9,S9,i8,i8,f8,f8")
12 |
13 | strategies = np.unique(data['Strategy'])
14 | shapes = np.unique(data['StateShape'])
15 | benchmarks = np.unique(data['Benchmark'])
16 |
17 | for strat in strategies:
18 | ds = data[data['Strategy'] == strat]
19 | for benchmark in benchmarks:
20 | db = ds[ds['Benchmark'] == benchmark]
21 | size = np.array([d[3] for d in db])
22 | size_t = np.sort(np.unique(size))
23 | events = np.array([d[4] for d in db])
24 | events_t = np.sort(np.unique(events))
25 | time = np.array([d[5] for d in db])
26 | memory = np.array([d[6] for d in db])
27 | cr = ['r' for k in size]
28 | cg = ['g' for k in size]
29 |
30 | fig = plt.figure()
31 | fig.suptitle(benchmark + ' with ' + strat + ' Strategy', fontsize=14, fontweight='bold')
32 | atim = fig.add_subplot(211)
33 | atim.set_xlabel('Size of State')
34 | atim.set_ylabel(u'Time in μs', color='r')
35 | atim.tick_params(axis='y', color='r', labelcolor='r')
36 | atim.scatter(size, time, color=cr, marker='+')
37 |
38 | means = [np.mean([d[5] for d in db if d[3] == s]) for s in size_t]
39 | atim.plot(size_t, means, color='r')
40 |
41 | amem = atim.twinx();
42 | amem.set_ylabel(u'Memory in bytes', color='g')
43 | amem.tick_params(axis='y', color='g', labelcolor='g')
44 | amem.scatter(size, memory, color=cg, marker='x')
45 |
46 | means = [np.mean([d[6] for d in db if d[3] == s]) for s in size_t]
47 | amem.plot(size_t, means, color='g')
48 |
49 | atim = fig.add_subplot(212)
50 | atim.set_xlabel('Number of Events')
51 | atim.set_ylabel(u'Time in μs', color='r')
52 | atim.tick_params(axis='y', color='r', labelcolor='r')
53 | atim.scatter(events, time, color=cr, marker='+')
54 |
55 | means = [np.mean([d[5] for d in db if d[4] == e]) for e in events_t]
56 | atim.plot(size_t, means, color='r')
57 |
58 | amem = atim.twinx();
59 | amem.set_ylabel(u'Memory in bytes', color='g')
60 | amem.tick_params(axis='y', color='g', labelcolor='g')
61 | amem.scatter(events, memory, color=cg, marker='x')
62 |
63 | means = [np.mean([d[6] for d in db if d[4] == e]) for e in events_t]
64 | amem.plot(size_t, means, color='g')
65 |
66 | # plt.show()
67 | plt.savefig(benchmark + '_' + strat + '.pdf')
68 |
--------------------------------------------------------------------------------
/src/reducers/version.js:
--------------------------------------------------------------------------------
1 | import {
2 | CHANGE_REQUEST,
3 | ADD_VERSION,
4 | ADD_VERSION_FAILED,
5 | SWAP_VERSION,
6 | STRING_LIT_CURSOR
7 | } from '../actions/types';
8 |
9 | import { addVersion as doAddVersion } from '../actions/version';
10 | import { refresh } from '../actions/state';
11 |
12 | const initialState = {
13 | source: '',
14 | request: null,
15 | versions: [],
16 | current: -1,
17 | error: null
18 | };
19 |
20 | export function currentVersion(state) {
21 | const { versions, current } = state.version;
22 | if (current < 0 || current >= versions.length) {
23 | return {
24 | source: '',
25 | init: () => '',
26 | render: () => '',
27 | mapping: {}
28 | };
29 | }
30 | return versions[current];
31 | }
32 |
33 | function changeReqest(state, action) {
34 | if (state.request) clearTimeout(state.request);
35 | return {
36 | ...state,
37 | source: action.source,
38 | request: setTimeout(() => action.dispatch(doAddVersion(action.source)), 1000)
39 | };
40 | }
41 |
42 | function addVersion(state, action) {
43 | const pastVersions = state.versions.slice(0, state.current + 1);
44 | const {source, init, render, mapping} = action;
45 | setTimeout(() => action.dispatch(refresh(render)), 0);
46 | return {
47 | ...state,
48 | source,
49 | request: null,
50 | versions: [...pastVersions, {source, init, render, mapping}],
51 | current: pastVersions.length,
52 | error: null
53 | };
54 | }
55 |
56 | function addVersionFailed(state, action) {
57 | return {
58 | ...state,
59 | request: null,
60 | error: action.error
61 | };
62 | }
63 |
64 | function swapVersion(state, action) {
65 | if (state.request) clearTimeout(state.request);
66 | const { source } = state.versions[action.idx];
67 | return {
68 | ...state,
69 | source,
70 | request: null,
71 | current: action.idx
72 | };
73 | }
74 |
75 | function strLitCursor(state, action) {
76 | const {mapping} = currentVersion({version: state});
77 | return {
78 | ...state,
79 | highlight: mapping[action.id]
80 | };
81 | }
82 |
83 | export default function code(state = initialState, action = {}) {
84 | switch (action.type) {
85 | case CHANGE_REQUEST: return changeReqest(state, action);
86 | case ADD_VERSION: return addVersion(state, action);
87 | case ADD_VERSION_FAILED: return addVersionFailed(state, action);
88 | case SWAP_VERSION: return swapVersion(state, action);
89 | case STRING_LIT_CURSOR: return strLitCursor(state, action);
90 | default: return state;
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/components/htmlview.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import {connect} from 'redux/react';
3 |
4 | import Ace from './ace';
5 | import {isSymString} from '../symstr';
6 | import {stringLitCursor, stringLitInsert, stringLitDelete, firstDifference, strIndexOf} from '../actions/manipulation';
7 |
8 | @connect(state => ({
9 | htmlstr: state.state.htmlstr,
10 | isActive: state.state.isActive
11 | }))
12 | export default class HTMLView extends Component {
13 | static propTypes = {
14 | htmlstr: PropTypes.any,
15 | isActive: PropTypes.bool.isRequired,
16 | active: PropTypes.bool.isRequired,
17 | editable: PropTypes.bool,
18 | dispatch: PropTypes.func.isRequired
19 | }
20 |
21 | componentDidMount() {
22 | window.htmlview = this;
23 | }
24 |
25 | shouldComponentUpdate(nextProps) {
26 | return this.props.active || nextProps.active;
27 | }
28 |
29 | componentDidUpdate(prevProps) {
30 | if (!prevProps.active && this.props.active) {
31 | this.refs.htmlace.repaint();
32 | }
33 | }
34 |
35 | onChange(newSource) {
36 | const firstDiff = firstDifference('' + this.props.htmlstr, newSource);
37 | const diff = newSource.length - this.props.htmlstr.length;
38 | // if adding characters, look up lit of prev char, else current
39 | const litAt = diff > 0 ? firstDiff - 1 : firstDiff;
40 | const lit = this.getStrLitAtPos(litAt);
41 | if (lit && lit.id > 0) {
42 | const {id, idx} = lit;
43 | if (diff > 0) {
44 | const insertStr = newSource.substr(firstDiff, diff);
45 | this.props.dispatch(stringLitInsert(id, idx, insertStr));
46 | } else if (diff < 0) {
47 | this.props.dispatch(stringLitDelete(id, idx, -diff));
48 | }
49 | } else {
50 | this.refs.htmlace.dropEdit();
51 | }
52 | }
53 |
54 | onChangeSelection(selection) {
55 | const {row, column} = selection.start;
56 | const htmlidx = strIndexOf(this.props.htmlstr, row, Math.max(0, column - 1));
57 | const lit = this.getStrLitAtPos(htmlidx);
58 | if (lit) {
59 | this.props.dispatch(stringLitCursor(lit.id));
60 | }
61 | }
62 |
63 | getStrLitAtPos(htmlidx) {
64 | const c = this.props.htmlstr[htmlidx];
65 | if (isSymString(c)) {
66 | return c.strs[0];
67 | }
68 | return null;
69 | }
70 |
71 | render() {
72 | const {htmlstr, isActive} = this.props;
73 | return (
74 | );
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/examples.js:
--------------------------------------------------------------------------------
1 | export const counter = `var i = 23;
2 |
3 | function click() {
4 | i++;
5 | }
6 |
7 | function view() {
8 | return (
9 |
Demo
10 |
Count: {i}
11 |
12 |
);
13 | }`;
14 |
15 | export const flappy = `var bgStep = 0; // background scroll
16 | var y = 100; // bird altitude
17 | var vy = -10; // bird vertical speed
18 |
19 | function scroll() {
20 | bgStep++;
21 | if (bgStep > 288) {
22 | bgStep = 0;
23 | }
24 | }
25 |
26 | function update() {
27 | y += vy;
28 | vy += 1; // gravity
29 | if (y >= 358) {
30 | y = 358;
31 | vy = 0;
32 | }
33 | }
34 |
35 | function click() {
36 | vy = -10;
37 | }
38 |
39 | function background() {
40 | return (
41 | );
46 | }
47 |
48 | function bird() {
49 | return (
50 | );
58 | }
59 |
60 | function view() {
61 | return (
62 |
63 | {background()}
64 | {bird()}
65 |
);
66 | }`;
67 |
68 | export const spiral = `var i = 0;
69 |
70 | function step() {
71 | if (++i >= 100) i = 0;
72 | }
73 |
74 | function sin100(a, f, s) {
75 | if (s == null) s = 0;
76 | return Math.floor(a * Math.sin(f * (i + s) * Math.PI / 50));
77 | }
78 |
79 | function cos100(a, f, s) {
80 | if (s == null) s = 0;
81 | return Math.floor(a * Math.cos(f * (i + s) * Math.PI / 50));
82 | }
83 |
84 | function dot(base) {
85 | if (base < 4) return ;
86 | const r = 128 + sin100(base * 4, 1);
87 | const g = 128 + sin100(base * 4, 2);
88 | const b = 128 + sin100(base * 4, 3);
89 | const x = sin100(20, 1, 2 * base);
90 | const y = cos100(20, 1, 2 * base);
91 | return (
99 | {dot(base / 1.2)}
100 |
);
101 | }
102 |
103 | function view() {
104 | return (
105 |
106 | {dot(32)}
107 |
108 |
);
109 | }`;
110 |
--------------------------------------------------------------------------------
/src/reducers/state.js:
--------------------------------------------------------------------------------
1 | import {
2 | RESET_STATE,
3 | RESET_STATE_FAILED,
4 | SWAP_STATE,
5 | SWAP_STATE_FAILED,
6 | EVENT_HANDLED,
7 | EVENT_FAILED,
8 | TOGGLE_ACTIVE
9 | } from '../actions/types';
10 |
11 | import {refresh} from '../actions/state';
12 | import {typeOf} from '../symstr';
13 | import {formatHTML} from '../builder';
14 | import strategy from '../strategy';
15 |
16 | const initialState = {
17 | current: -1,
18 | dom: '',
19 | error: null,
20 | isActive: true
21 | };
22 |
23 | export function getFrameHandlers(state) {
24 | function rec(d) {
25 | if (typeOf(d) !== 'object') return [];
26 | const fh = d.attributes
27 | .filter(({key}) => `${key}` === 'onframe')
28 | .map(({value}) => value);
29 | return d.children.reduce((res, child) => [...res, ...rec(child)], fh);
30 | }
31 | return rec(state.state.dom);
32 | }
33 |
34 | function resetState(state, action) {
35 | const initial = action.state;
36 | const internal = strategy.add({current: -1}, initial);
37 | return {
38 | ...state,
39 | internal,
40 | current: 0,
41 | dom: action.dom,
42 | htmlstr: formatHTML(action.dom)
43 | };
44 | }
45 |
46 | function swapState(state, action) {
47 | return {
48 | ...state,
49 | current: action.idx,
50 | dom: action.dom,
51 | htmlstr: formatHTML(action.dom)
52 | };
53 | }
54 |
55 | function swapStateFailed(state, action) {
56 | return {
57 | ...state,
58 | error: action.error
59 | };
60 | }
61 |
62 | function resetStateFailed(state, action) {
63 | return {
64 | ...state,
65 | error: action.error
66 | };
67 | }
68 |
69 | function eventHandled(state, action) {
70 | setTimeout(() => action.dispatch(refresh()), 0);
71 | return {
72 | ...state,
73 | internal: strategy.add(state, action.state),
74 | current: state.current + 1,
75 | dom: action.dom,
76 | htmlstr: formatHTML(action.dom)
77 | };
78 | }
79 |
80 | function eventFailed(state, action) {
81 | return {
82 | ...state,
83 | isSwapping: false,
84 | error: action.error
85 | };
86 | }
87 |
88 | function toggleActive(state) {
89 | return {
90 | ...state,
91 | isActive: !!!state.isActive
92 | };
93 | }
94 |
95 | export default function stateReducer(state = initialState, action = {}) {
96 | switch (action.type) {
97 | case RESET_STATE: return resetState(state, action);
98 | case RESET_STATE_FAILED: return resetStateFailed(state, action);
99 | case SWAP_STATE: return swapState(state, action);
100 | case SWAP_STATE_FAILED: return swapStateFailed(state, action);
101 | case EVENT_HANDLED: return eventHandled(state, action);
102 | case EVENT_FAILED: return eventFailed(state, action);
103 | case TOGGLE_ACTIVE: return toggleActive(state, action);
104 | default: return state;
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/proxies.js:
--------------------------------------------------------------------------------
1 | import {typeOf} from './symstr';
2 |
3 | const immutableProxies = new WeakSet();
4 | const immutableObjects = new WeakMap();
5 |
6 | export function immutable(x) {
7 | if (x === null ||
8 | (typeOf(x) !== 'object' && typeOf(x) !== 'function') ||
9 | immutableProxies.has(x)) {
10 | return x;
11 | }
12 | if (immutableObjects.has(x)) {
13 | return immutableObjects.get(x);
14 | }
15 | const proxy = new Proxy(x, {
16 | get: (target, key) => immutable(target[key]),
17 | set: () => {
18 | throw new TypeError('Mutation to immutable object');
19 | }
20 | });
21 | immutableProxies.add(proxy);
22 | immutableObjects.set(x, proxy);
23 | return proxy;
24 | }
25 |
26 | const lazyFirstOrderProxies = new WeakSet();
27 | const lazyFirstOrderObjects = new WeakMap();
28 |
29 | export function lazyFirstOrder(x) {
30 | if (x === null ||
31 | (typeOf(x) !== 'object' && typeOf(x) !== 'function') ||
32 | lazyFirstOrderProxies.has(x)) {
33 | return x;
34 | }
35 | if (lazyFirstOrderObjects.has(x)) {
36 | return lazyFirstOrderObjects.get(x);
37 | }
38 | const proxy = new Proxy(x, {
39 | get: (target, key) => lazyFirstOrder(target[key]),
40 | set: (target, key, value) => {
41 | target[key] = value;
42 | return true;
43 | },
44 | apply: () => {
45 | throw new TypeError('Call of a first-order value');
46 | }
47 | });
48 | lazyFirstOrderProxies.add(proxy);
49 | lazyFirstOrderObjects.set(x, proxy);
50 | return proxy;
51 | }
52 |
53 | export function cow(r) {
54 | const cowProxies = new WeakSet();
55 | const cowObjects = new WeakMap();
56 | const changes = new WeakMap();
57 |
58 | const wrap = (x) => {
59 | if (x === null ||
60 | (typeOf(x) !== 'object' && typeOf(x) !== 'function') ||
61 | cowProxies.has(x)) {
62 | return x;
63 | }
64 | if (cowObjects.has(x)) {
65 | return cowObjects.get(x);
66 | }
67 | const proxy = new Proxy(x, {
68 | get: (target, key) => {
69 | const props = changes.get(target);
70 | if (props && Object.hasOwnProperty.call(props, key)) {
71 | return wrap(props[key]);
72 | }
73 | return wrap(target[key]);
74 | },
75 | has: (target, key) => {
76 | const props = changes.get(target);
77 | return (props && Object.hasOwnProperty.call(props, key)) || Object.hasOwnProperty.call(target, key);
78 | },
79 | set: (target, key, value) => {
80 | const props = changes.get(target);
81 | if (props) {
82 | props[key] = value;
83 | } else {
84 | changes.set(target, {[key]: value});
85 | }
86 | return true;
87 | },
88 | ownKeys: (target) => {
89 | const props = changes.get(target);
90 | const result = props ? Object.keys(props) : [];
91 | const targetKeys = Object.keys(target);
92 | for (const k in targetKeys) {
93 | if (!result.includes(k)) {
94 | result.push(k);
95 | }
96 | }
97 | return result;
98 | }
99 | });
100 | cowProxies.add(proxy);
101 | cowObjects.set(x, proxy);
102 | return proxy;
103 | };
104 | return wrap(r);
105 | }
106 |
--------------------------------------------------------------------------------
/src/components/ace.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import AceEditor from 'react-ace/src/ace.jsx';
3 |
4 | require('brace/mode/jsx');
5 | require('brace/mode/html');
6 | require('brace/mode/json');
7 | require('brace/theme/eclipse');
8 |
9 | export default class Ace extends Component {
10 | static propTypes = {
11 | name: PropTypes.string,
12 | mode: PropTypes.string,
13 | source: PropTypes.string,
14 | highlight: PropTypes.any,
15 | height: PropTypes.number,
16 | showLineNumbers: PropTypes.bool,
17 | readOnly: PropTypes.bool,
18 | onChange: PropTypes.func,
19 | onChangeSelection: PropTypes.func
20 | }
21 |
22 | componentDidMount() {
23 | this.hasMounted = true;
24 | }
25 |
26 | shouldComponentUpdate(nextProps) {
27 | return nextProps.readOnly !== this.props.readOnly ||
28 | `${nextProps.source}` !== `${this.props.source}` ||
29 | !this.locEqual(nextProps.highlight, this.props.highlight);
30 | }
31 |
32 | componentWillUpdate() {
33 | this.hasMounted = false;
34 | }
35 |
36 | componentDidUpdate() {
37 | this.hasMounted = true;
38 | const session = this.refs.ace.editor.getSession();
39 | for (const key of Object.keys(session.getMarkers(false))) {
40 | session.removeMarker(key);
41 | }
42 | if (this.props.highlight) {
43 | const {start, end} = this.props.highlight;
44 | const Range = this.refs.ace.editor.getSelectionRange().constructor;
45 | const range = new Range(start.line - 1, start.column,
46 | end.line - 1, end.column);
47 | // session.addMarker(range, 'ace_selected_word', 'text', true);
48 | session.addMarker(range, 'ace_selection', 'text');
49 | }
50 | }
51 |
52 | onChange(newSource) {
53 | if (this.props.onChange && this.hasMounted && newSource !== this.props.source) {
54 | this.props.onChange(newSource);
55 | }
56 | }
57 |
58 | onAceLoad(editor) {
59 | editor.getSession().setTabSize(2);
60 | editor.renderer.setShowGutter(this.props.showLineNumbers);
61 | editor.on('changeSelection', () => {
62 | if (this.props.onChangeSelection && this.hasMounted) {
63 | this.props.onChangeSelection(editor.getSelectionRange());
64 | }
65 | });
66 | }
67 |
68 | locEqual(locA, locB) {
69 | return locA === locB ||
70 | (locA && locB &&
71 | locA.start.line === locB.start.line &&
72 | locA.start.column === locB.start.column &&
73 | locA.end.line === locB.end.line &&
74 | locA.end.column === locB.end.column);
75 | }
76 |
77 | dropEdit() {
78 | const cursor = this.refs.ace.editor.getSelectionRange().start;
79 | this.forceUpdate(() => {
80 | this.refs.ace.editor.moveCursorTo(cursor.row, cursor.column);
81 | });
82 | }
83 |
84 | repaint() {
85 | this.refs.ace.editor.resize();
86 | }
87 |
88 | render() {
89 | const {name, source, mode, height, readOnly} = this.props;
90 | return (
91 |
102 | );
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/actions/state.js:
--------------------------------------------------------------------------------
1 | import {
2 | RESET_STATE,
3 | RESET_STATE_FAILED,
4 | SWAP_STATE,
5 | SWAP_STATE_FAILED,
6 | EVENT_FAILED,
7 | EVENT_HANDLED,
8 | TOGGLE_ACTIVE
9 | } from './types';
10 |
11 | import {getFrameHandlers} from '../reducers/state';
12 | import {currentVersion} from '../reducers/version';
13 | import strategy from '../strategy';
14 |
15 | function renderState(render, state) {
16 | if (!render || !state) return '';
17 | return strategy.render(render, state);
18 | }
19 |
20 | function renderCurrent(getState, state) {
21 | return renderState(currentVersion(getState()).render, state);
22 | }
23 |
24 | export function reset() {
25 | return (dispatch, getState) => {
26 | const {init} = currentVersion(getState());
27 | try {
28 | const state = strategy.handle(() => init(), {});
29 | return {
30 | type: RESET_STATE,
31 | state,
32 | dom: renderCurrent(getState, state)
33 | };
34 | } catch (e) {
35 | return {
36 | type: RESET_STATE_FAILED,
37 | error: e
38 | };
39 | }
40 | };
41 | }
42 |
43 | export function refresh(render) {
44 | return (dispatch, getState) => {
45 | const state = getState();
46 | const r = render || currentVersion(getState()).render;
47 | try {
48 | return {
49 | type: SWAP_STATE,
50 | idx: state.state.current,
51 | dom: renderState(r, strategy.current(state))
52 | };
53 | } catch (e) {
54 | return {
55 | type: SWAP_STATE_FAILED,
56 | error: e
57 | };
58 | }
59 | };
60 | }
61 |
62 | export function swapState(idx) {
63 | return (dispatch, getState) => {
64 | try {
65 | const state = strategy.current({ state: {
66 | current: idx,
67 | internal: getState().state.internal
68 | }});
69 | return {
70 | type: SWAP_STATE,
71 | idx: idx,
72 | dom: renderCurrent(getState, state)
73 | };
74 | } catch (e) {
75 | return {
76 | type: SWAP_STATE_FAILED,
77 | error: e
78 | };
79 | }
80 | };
81 | }
82 |
83 | export function event(handler) {
84 | return (dispatch, getState) => {
85 | if (!getState().state.isActive) {
86 | return { type: 'noop' };
87 | }
88 | try {
89 | const state = strategy.handle(handler, strategy.current(getState()));
90 | return {
91 | type: EVENT_HANDLED,
92 | state,
93 | dom: renderCurrent(getState, state)
94 | };
95 | } catch (e) {
96 | return {
97 | type: EVENT_FAILED,
98 | error: e
99 | };
100 | }
101 | };
102 | }
103 |
104 | export function frame() {
105 | return (dispatch, getState) => {
106 | const {isActive} = getState().state;
107 | const frameHandlers = getFrameHandlers(getState());
108 | if (!isActive || !frameHandlers || !frameHandlers.length) {
109 | return { type: 'noop' };
110 | }
111 | try {
112 | const handler = () => frameHandlers.forEach(h => h());
113 | const state = strategy.handle(handler, strategy.current(getState()));
114 | return {
115 | type: EVENT_HANDLED,
116 | state,
117 | dom: renderCurrent(getState, state)
118 | };
119 | } catch (e) {
120 | return {
121 | type: EVENT_FAILED,
122 | error: e
123 | };
124 | }
125 | };
126 | }
127 |
128 | export function toggleActive() {
129 | return (dispatch, getState) => {
130 | dispatch(refresh(currentVersion(getState()).render));
131 | return {
132 | type: TOGGLE_ACTIVE
133 | };
134 | };
135 | }
136 |
--------------------------------------------------------------------------------
/src/strategy.js:
--------------------------------------------------------------------------------
1 | import deepFreeze from 'deep-freeze';
2 | import clone from 'clone';
3 |
4 | import {immutable, lazyFirstOrder, cow} from './proxies';
5 | import stateMembrane from './proxy';
6 | import {typeOf} from './symstr';
7 |
8 | function checkState(state) {
9 | const ws = new WeakSet();
10 | function c(o) {
11 | if (typeOf(o) === 'function') {
12 | throw new Error('Functions not allowed in state');
13 | }
14 | if (typeOf(o) !== 'object') return;
15 | ws.add(o);
16 | Object.getOwnPropertyNames(o).forEach((prop) => {
17 | if (o.hasOwnProperty(prop) &&
18 | o[prop] !== null &&
19 | !ws.has(o[prop])) {
20 | c(o[prop]);
21 | }
22 | });
23 | }
24 | c(state);
25 | }
26 |
27 | export const simple = {
28 | handle: (handle, state) => {
29 | window.state = clone(state);
30 | handle();
31 | checkState(window.state);
32 | return window.state;
33 | },
34 | render: (render, state) => {
35 | window.state = deepFreeze(clone(state));
36 | try {
37 | return render();
38 | } catch (e) {
39 | if (e instanceof TypeError) {
40 | throw new TypeError('render() needs to be a pure function!');
41 | }
42 | throw e;
43 | }
44 | },
45 | current: ({state}) => {
46 | const states = state.internal || [];
47 | const current = state.current;
48 | if (current < 0 || current >= states.length) {
49 | return {};
50 | }
51 | return states[current];
52 | },
53 | add: (state, nextState) => {
54 | const pastStates = (state.internal || []).slice(0, state.current + 1);
55 | return [...pastStates, nextState];
56 | },
57 | maxState: ({state}) => {
58 | const states = state.internal || [];
59 | return states.length - 1;
60 | }
61 | };
62 |
63 | export const proxies = {
64 | handle: (handle, state) => {
65 | window.state = lazyFirstOrder(cow(state));
66 | handle();
67 | return window.state;
68 | },
69 | render: (render, state) => {
70 | window.state = immutable(state);
71 | return render();
72 | },
73 | current: simple.current,
74 | add: simple.add,
75 | maxState: simple.maxState
76 | };
77 |
78 | export const proxy = {
79 | handle: (handle, state) => {
80 | const membrane = stateMembrane(state);
81 | membrane.cow();
82 | window.state = membrane.getState();
83 | handle();
84 | return window.state;
85 | },
86 | render: (render, state) => {
87 | const membrane = stateMembrane(state);
88 | membrane.freeze();
89 | window.state = membrane.getState();
90 | try {
91 | return render();
92 | } finally {
93 | membrane.unfreeze();
94 | }
95 | },
96 | current: (state) => {
97 | const current = state.state.current;
98 | const membrane = stateMembrane(state.state.internal || {});
99 | membrane.timeTravel(current);
100 | return membrane.getState();
101 | },
102 | add: (state, nextState) => {
103 | const membrane = stateMembrane(state.internal || nextState);
104 | return membrane.getState();
105 | },
106 | maxState: ({state}) => {
107 | const membrane = stateMembrane(state.internal || {});
108 | return membrane.getMaxVersion();
109 | }
110 | };
111 |
112 | const defaultStrategy = (typeOf(window) === 'undefined' || typeOf(window.Proxy) !== 'undefined') ? proxy : simple;
113 |
114 | const current = {
115 | handle: defaultStrategy.handle,
116 | render: defaultStrategy.render,
117 | current: defaultStrategy.current,
118 | add: defaultStrategy.add,
119 | maxState: defaultStrategy.maxState
120 | };
121 |
122 | export default current;
123 |
--------------------------------------------------------------------------------
/benchmarks/benchmark.js:
--------------------------------------------------------------------------------
1 | import mbench from 'mbench';
2 | // import {expect} from 'chai';
3 |
4 | import current, {simple, proxies, proxy} from '../src/strategy';
5 | import {runHandlerInternal, runRenderInternal} from '../test/helpers';
6 |
7 | function createFlatState(m) {
8 | const result = [];
9 | for (let i = 1; i < m; i++) {
10 | result.push({z: 1});
11 | }
12 | return {a: result, y: 1, z: 1};
13 | }
14 |
15 | function createDeepState(m) {
16 | if (m == 0) return {};
17 | return {
18 | a: createDeepState(m - 1),
19 | y: 1,
20 | z: 1
21 | };
22 | }
23 |
24 | function createTreeState(m) {
25 | if (m === 0) return {};
26 | return {
27 | a: createTreeState(Math.floor(m / 2)),
28 | b: createTreeState(m - Math.floor(m / 2) - 1),
29 | y: 1,
30 | z: 1
31 | };
32 | }
33 |
34 | const M = 100;
35 | const N = 100;
36 | const STEPS = 5;
37 |
38 | function init(state) {
39 | let internal = current.add({}, current.current({
40 | state: {current: -1}
41 | }));
42 | let result = runHandlerInternal(internal, 0, () => window.state.s = state);
43 | return current.add({internal, current: 0}, result.state);
44 | }
45 |
46 | function sumUp(current) {
47 | let res = 0;
48 | if (current.length) {
49 | for (let i = 0; i < current.length; i++) {
50 | res += sumUp(current[i]);
51 | }
52 | }
53 | if (current.a) {
54 | res += sumUp(current.a);
55 | }
56 | if (current.b) {
57 | res += sumUp(current.b);
58 | }
59 | if (current.z) {
60 | res += current.z;
61 | }
62 | return res;
63 | }
64 |
65 | function render(internal, n) {
66 | return runRenderInternal(internal, n, () => {
67 | return sumUp(window.state.s);
68 | });
69 | }
70 |
71 | function handle(internal, n) {
72 | let result = runHandlerInternal(internal, n, () => {
73 | window.state.s.z += window.state.s.y;
74 | });
75 | return current.add({internal, current: n}, result.state);
76 | }
77 |
78 | function bench(kind, type, stateBuilder) {
79 | for (let n = 1; n < N; n += N / STEPS) {
80 | for (let m = 1; m < M; m += M / STEPS) {
81 | let internal;
82 | const t = mbench(() => {
83 | const {dom} = render(internal, n);
84 | // expect(dom).to.be.equal(n + m - 1);
85 | return dom;
86 | }, {setup: () => {
87 | global.window = {};
88 | internal = init(stateBuilder(m));
89 | for (let i = 1; i < n; i++) {
90 | internal = handle(internal, i);
91 | }
92 | }});
93 | console.log(`${kind},render,${type},${m},${n},${t[0]},${t[1]}`);
94 | }
95 | }
96 | for (let n = 1; n < N; n += N / STEPS) {
97 | for (let m = 1; m < M; m += M / STEPS) {
98 | let internal;
99 | const t = mbench(() => {
100 | internal = handle(internal, n);
101 | // const state = current.current({
102 | // state: {internal, current: n + 1}
103 | // });
104 | // expect(state.s.z).to.be.eql(n + 1);
105 | }, {setup: () => {
106 | global.window = {};
107 | internal = init(stateBuilder(m));
108 | for (let i = 1; i < n; i++) {
109 | internal = handle(internal, i);
110 | }
111 | }});
112 | console.log(`${kind},handle,${type},${m},${n},${t[0]},${t[1]}`);
113 | }
114 | }
115 | }
116 |
117 |
118 | it('bench', () => {
119 | console.log('Strategy,Benchmark,StateShape,StateSize,Events,Time,Memory');
120 | const strategies = {simple, proxies, proxy};
121 |
122 | for (let key in strategies) {
123 | const strategy = strategies[key];
124 | current.handle = strategy.handle;
125 | current.render = strategy.render;
126 | current.current = strategy.current;
127 | current.add = strategy.add;
128 |
129 | bench(key, 'flat', createFlatState);
130 | bench(key, 'deep', createDeepState);
131 | bench(key, 'tree', createTreeState);
132 | }
133 | });
134 |
--------------------------------------------------------------------------------
/src/proxy.js:
--------------------------------------------------------------------------------
1 | import {typeOf} from './symstr';
2 |
3 | const stateMembranes = new WeakMap();
4 |
5 | class StateMembrane {
6 | constructor(state) {
7 | this.proxies = new WeakSet();
8 | this.objects = new WeakMap();
9 | this.changes = new WeakMap();
10 | this.state = this.wrap(state);
11 | this.frozen = false;
12 | this.version = -1;
13 | this.maxVersion = -1;
14 | }
15 |
16 | /**
17 | * @param changes [{change: any, version: number]
18 | * non-empty list of changes for a particular property
19 | *
20 | * version=3 means it was changed at version 4 and the current
21 | * value up to including version 3 is stored in .change
22 | *
23 | * @return [number] index of the entry including the current
24 | * value or *length of versions* if no such entry exists
25 | */
26 | lookup(versions) {
27 | const lastIdx = versions.length - 1;
28 | // last write was for previous version -> use current value
29 | if (lastIdx < 0 || versions[lastIdx].version < this.version) return lastIdx + 1;
30 | // binary search
31 | let leftIdx = 0;
32 | let rightIdx = lastIdx;
33 | while (leftIdx + 1 < rightIdx) {
34 | const midIdx = 0 | (leftIdx + rightIdx) / 2;
35 | if (versions[midIdx].version < this.version) {
36 | leftIdx = midIdx;
37 | } else {
38 | rightIdx = midIdx;
39 | }
40 | }
41 | if (leftIdx < rightIdx && versions[leftIdx].version < this.version) {
42 | leftIdx = rightIdx;
43 | }
44 | // leftIdx points to last entry for that version
45 | return leftIdx;
46 | }
47 |
48 | wrap(x) {
49 | if (x === null ||
50 | (typeOf(x) !== 'object' && typeOf(x) !== 'function') ||
51 | this.proxies.has(x)) {
52 | return x;
53 | }
54 | if (this.objects.has(x)) {
55 | return this.objects.get(x);
56 | }
57 | const proxy = new Proxy(x, {
58 | get: (target, key) => {
59 | if (key === 'prototype') return target[key];
60 | const changes = this.changes.get(target);
61 | if (changes && Object.hasOwnProperty.call(changes, key)) {
62 | const idx = this.lookup(changes[key]);
63 | if (idx < changes[key].length) {
64 | return this.wrap(changes[key][idx].change);
65 | }
66 | }
67 | return this.wrap(target[key]);
68 | },
69 | set: (target, key, value) => {
70 | if (this.frozen) {
71 | throw new TypeError('Mutation to immutable object');
72 | }
73 | let objChanges = this.changes.get(target);
74 | if (!objChanges) {
75 | objChanges = {};
76 | this.changes.set(target, objChanges);
77 | }
78 | if (!Object.hasOwnProperty.call(objChanges, key)) {
79 | objChanges[key] = [];
80 | }
81 | const versions = objChanges[key];
82 | const idx = this.lookup(versions);
83 |
84 | let prevValue = target[key];
85 | if (versions.length > 0 && idx < versions.length) {
86 | prevValue = versions[idx];
87 | versions.splice(idx);
88 | }
89 | if (idx === 0 || versions[idx - 1].version !== this.version - 1) {
90 | versions.push({version: this.version - 1, change: prevValue});
91 | }
92 | target[key] = value;
93 | return true;
94 | },
95 | ownKeys: (target) => {
96 | const props = this.changes.get(target);
97 | const result = props ? Object.keys(props) : [];
98 | const targetKeys = Object.keys(target);
99 | for (const k in targetKeys) {
100 | if (!result.includes(k)) {
101 | result.push(k);
102 | }
103 | }
104 | return result;
105 | }
106 | });
107 | this.proxies.add(proxy);
108 | this.objects.set(x, proxy);
109 | return proxy;
110 | }
111 |
112 | freeze() {
113 | this.frozen = true;
114 | }
115 |
116 | unfreeze() {
117 | this.frozen = false;
118 | }
119 |
120 | cow() {
121 | this.version++;
122 | this.setMaxVersion(this.version);
123 | }
124 |
125 | timeTravel(version) {
126 | this.version = version;
127 | }
128 |
129 | getState() {
130 | return this.state;
131 | }
132 |
133 | getMaxVersion() {
134 | return this.maxVersion;
135 | }
136 |
137 | setMaxVersion(maxVersion) {
138 | this.maxVersion = maxVersion;
139 | }
140 | }
141 |
142 | export default function stateMembrane(state) {
143 | if (stateMembranes.has(state)) {
144 | return stateMembranes.get(state);
145 | }
146 | const membrane = new StateMembrane(state);
147 | stateMembranes.set(membrane.getState(), membrane);
148 | return membrane;
149 | }
150 |
151 | /**
152 | * The cow proxy is a little bit like an object capability system.
153 | * If you have a reference to a certain object (identify) then it
154 | * obviously existed, so you simply reach it and then do your property
155 | * lookup locally through all accumulated proxies to find the most
156 | * recent value of a property.
157 | *
158 | * Without proxies, object identities are eternal, and the desired version
159 | * is used as index into the list of property changes. Problem: you still
160 | * have to pay O(n) for looking up unchanged properties in a changing object.
161 | *
162 | * Ideally, we would keep a version history per property field. This would
163 | * work fine but what about properties that were never changed, so don't
164 | * have a version history? Proposed solution: use initial value!
165 | */
166 |
--------------------------------------------------------------------------------
/test/rewrite.js:
--------------------------------------------------------------------------------
1 | /* globals describe, it */
2 | /* eslint no-unused-expressions:0 */
3 | import {expect} from 'chai';
4 | import {parse} from 'esprima-fb';
5 | import {generate} from 'escodegen';
6 |
7 | import {shouldFail, rewrite} from './helpers';
8 | import {rewriteSymStrings, rewriteOps} from '../src/rewriting';
9 | import {SymString, operators} from '../src/symstr';
10 |
11 | describe('rewrite JSX', () => {
12 | it('rejects invalid programs', () => {
13 | shouldFail('var i = 23;');
14 | shouldFail('function rende() { return 23; }');
15 | shouldFail('function render() { return x; }');
16 | shouldFail('function render() { return 1 +++ 2; }');
17 | });
18 |
19 | it('should support a trivial render function', () => {
20 | const [, render] = rewrite('function render() { return 1; }');
21 | expect(render()).to.be.equal(1);
22 | });
23 |
24 | it('should rewrite state accesses', () => {
25 | const src = 'var i = 1; function render() { return i; }';
26 | window.state = {};
27 | const [init, render] = rewrite(src);
28 | expect(window.state).to.deep.equal({});
29 | init();
30 | expect(window.state).to.deep.equal({i: 1});
31 | expect(render()).to.be.equal(1);
32 | });
33 |
34 | it('should compile simple JSX', () => {
35 | const [, render] = rewrite('function render() { return ; }');
36 | const dom = render();
37 | expect(dom.name.toSourceString()).to.equal('a');
38 | expect(dom.attributes).to.deep.equal([]);
39 | expect(dom.children).to.deep.equal([]);
40 | /* eslint eqeqeq:0 */
41 | expect(dom.name == 'a').to.be.true;
42 | expect(`<${dom.name}>${dom.name}>`).to.be.equal('');
43 | });
44 |
45 | it('should supports attributes', () => {
46 | const [, render] = rewrite('function render() { return ; }');
47 | const dom = render();
48 | expect(dom.name.toSourceString()).to.equal('a');
49 | expect(dom.attributes).to.have.length(1);
50 | const [{key, value}] = dom.attributes;
51 | expect(key.toSourceString()).to.equal('x');
52 | expect(value.toSourceString()).to.equal('f');
53 | expect(dom.children).to.deep.equal([]);
54 | });
55 |
56 | it('should supports JavaScript as attributes', () => {
57 | const [, render] = rewrite('function render() { return ; }');
58 | const dom = render();
59 | expect(dom.name.toSourceString()).to.equal('a');
60 | expect(dom.attributes).to.have.length(1);
61 | const [{key, value}] = dom.attributes;
62 | expect(key.toSourceString()).to.equal('x');
63 | expect(value.toSourceString()).to.equal('f');
64 | expect(dom.children).to.deep.equal([]);
65 | });
66 |
67 | it('should support child elements', () => {
68 | const [, render] = rewrite('function render() { return ; }');
69 | const dom = render();
70 | expect(dom.name.toSourceString()).to.equal('a');
71 | expect(dom.attributes).to.deep.equal([]);
72 | expect(dom.children).to.have.length(1);
73 | expect(dom.children[0].name.toSourceString()).to.equal('b');
74 | expect(dom.children[0].attributes).to.deep.equal([]);
75 | expect(dom.children[0].children).to.deep.equal([]);
76 | });
77 |
78 | it('should support JavaScript as child element', () => {
79 | const [, render] = rewrite('function render() { return {"foo"}; }');
80 | const dom = render();
81 | expect(dom.name.toSourceString()).to.equal('a');
82 | expect(dom.attributes).to.deep.equal([]);
83 | expect(dom.children).to.have.length(1);
84 | expect(dom.children[0].toSourceString()).to.equal('foo');
85 | });
86 | });
87 |
88 | function rew(src) {
89 | const parsed = parse(src, {loc: true});
90 | const {ast, mapping} = rewriteSymStrings(parsed);
91 | const rewritten = rewriteOps(ast);
92 | const generated = generate(rewritten);
93 | /* eslint no-eval:0 */
94 | global.operators = operators;
95 | global.sym = SymString.single;
96 | const res = eval(generated);
97 | return {ast, res, length: res.length, mapping};
98 | }
99 |
100 | function inlineLoc(from, to) {
101 | return {
102 | start: {
103 | line: 1,
104 | column: from
105 | },
106 | end: {
107 | line: 1,
108 | column: to
109 | }
110 | };
111 | }
112 |
113 | describe('rewrite symbolic strings', () => {
114 |
115 | it('rewrite string literals', () => {
116 | const {res, mapping} = rew('"abc"');
117 | expect(mapping).to.be.an('object');
118 | expect(Object.keys(mapping)).to.have.length(1);
119 | const id = +Object.keys(mapping)[0];
120 | expect(res.strs).to.not.be.undefined;
121 | expect(res.strs).to.deep.equal([{
122 | str: 'abc',
123 | id,
124 | idx: 0,
125 | start: 0
126 | }]);
127 | expect(mapping[id]).to.deep.equal(inlineLoc(0, 5));
128 | });
129 |
130 | it('rewrite string concat literal', () => {
131 | const {res, mapping} = rew('"abc" + 42');
132 | expect(mapping).to.be.an('object');
133 | expect(Object.keys(mapping)).to.have.length(1);
134 | const id = +Object.keys(mapping)[0];
135 | expect(res.strs).to.not.be.undefined;
136 | expect(res.strs).to.deep.equal([{
137 | str: 'abc',
138 | id,
139 | idx: 0,
140 | start: 0
141 | }, {
142 | str: '42',
143 | id: 0,
144 | idx: 0,
145 | start: 3
146 | }]);
147 | expect(mapping[id]).to.deep.equal(inlineLoc(0, 5));
148 | });
149 |
150 | it('rewrite string concat two literals', () => {
151 | const {res, mapping} = rew('"abc" + "def"');
152 | expect(mapping).to.be.an('object');
153 | expect(Object.keys(mapping)).to.have.length(2);
154 | const id = +Object.keys(mapping)[0];
155 | const id2 = +Object.keys(mapping)[1];
156 | expect(res.strs).to.not.be.undefined;
157 | expect(res.strs).to.deep.equal([{
158 | str: 'abc',
159 | id,
160 | idx: 0,
161 | start: 0
162 | }, {
163 | str: 'def',
164 | id: id2,
165 | idx: 0,
166 | start: 3
167 | }]);
168 | expect(mapping[id]).to.deep.equal(inlineLoc(0, 5));
169 | expect(mapping[id2]).to.deep.equal(inlineLoc(8, 13));
170 | });
171 |
172 | it('rewrite string concat assignment', () => {
173 | const {res, mapping} = rew('var i = "abc"; i += "def"');
174 | expect(mapping).to.be.an('object');
175 | expect(Object.keys(mapping)).to.have.length(2);
176 | const id = +Object.keys(mapping)[0];
177 | const id2 = +Object.keys(mapping)[1];
178 | expect(res.strs).to.not.be.undefined;
179 | expect(res.strs).to.deep.equal([{
180 | str: 'abc',
181 | id,
182 | idx: 0,
183 | start: 0
184 | }, {
185 | str: 'def',
186 | id: id2,
187 | idx: 0,
188 | start: 3
189 | }]);
190 | expect(mapping[id]).to.deep.equal(inlineLoc(8, 13));
191 | expect(mapping[id2]).to.deep.equal(inlineLoc(20, 25));
192 | });
193 |
194 | it('rewrite string prefix', () => {
195 | const {res, mapping} = rew('var i = "12"; ++i');
196 | expect(mapping).to.be.an('object');
197 | expect(Object.keys(mapping)).to.have.length(1);
198 | expect(res).to.be.equal(13);
199 | });
200 |
201 | it('rewrite string postfix', () => {
202 | const {res, mapping} = rew('var i = "12"; [i++, i]');
203 | expect(mapping).to.be.an('object');
204 | expect(Object.keys(mapping)).to.have.length(1);
205 | expect(res).to.be.deep.equal([12, 13]);
206 | });
207 | });
208 |
209 | export default function() {}
210 |
--------------------------------------------------------------------------------
/src/rewriting.js:
--------------------------------------------------------------------------------
1 | import {analyze} from 'escope';
2 | import {replace} from 'estraverse-fb';
3 |
4 | import {compileJSX} from './builder';
5 | import {typeOf} from './symstr';
6 |
7 | function stateVar(identifier) {
8 | return {
9 | type: 'MemberExpression',
10 | computed: false,
11 | object: {
12 | type: 'MemberExpression',
13 | computed: false,
14 | object: {
15 | type: 'Identifier',
16 | name: 'window'
17 | },
18 | property: {
19 | type: 'Identifier',
20 | name: 'state'
21 | }
22 | },
23 | property: identifier
24 | };
25 | }
26 |
27 | function stateAssign(identifier, value) {
28 | return {
29 | type: 'ExpressionStatement',
30 | expression: {
31 | type: 'AssignmentExpression',
32 | operator: '=',
33 | left: stateVar(identifier),
34 | right: value || {type: 'Identifier', name: 'undefined'}
35 | }
36 | };
37 | }
38 |
39 | function initFun(init) {
40 | return {
41 | type: 'FunctionDeclaration',
42 | id: {type: 'Identifier', name: 'init'},
43 | params: [],
44 | defaults: [],
45 | body: {
46 | type: 'BlockStatement',
47 | body: init.map(decl => stateAssign(decl.id, decl.init))
48 | },
49 | generator: false,
50 | expression: false
51 | };
52 | }
53 |
54 | function returnRenderInit() {
55 | return {
56 | type: 'ReturnStatement',
57 | argument: {
58 | type: 'ArrayExpression',
59 | elements: [{
60 | type: 'Identifier',
61 | name: 'init'
62 | }, {
63 | type: 'Identifier',
64 | name: 'view'
65 | }]
66 | }
67 | };
68 | }
69 |
70 | export function rewriteJSX(ast) {
71 | return replace(ast, {
72 | enter: function enter(node) {
73 | if (node.type === 'JSXElement') {
74 | return compileJSX(node);
75 | }
76 | if (node.type === 'JSXExpressionContainer') {
77 | return node.expression;
78 | }
79 | return node;
80 | }
81 | });
82 | }
83 |
84 | export function rewriteState(ast) {
85 | const scopeManager = analyze(ast, {optimistic: true});
86 | const glob = scopeManager.globalScope;
87 | let scope = glob;
88 | let init = [];
89 | return replace(ast, {
90 | enter: function enter(node) {
91 | if (node.type === 'ReturnStatement' && scope === glob) {
92 | throw new Error('Unexpeced global return');
93 | }
94 | if (node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration') {
95 | scope = scopeManager.acquire(node);
96 | }
97 | if (node.type === 'VariableDeclaration' && scope === glob) {
98 | init = init.concat(node.declarations);
99 | return this.remove();
100 | }
101 | return node;
102 | },
103 | leave: function leave(node) {
104 | if (node.type === 'Identifier' && node.name !== '_') {
105 | const ref = scope.resolve(node);
106 | if (ref && ref.resolved && ref.resolved.scope === glob &&
107 | !(ref.resolved.defs[0].type === 'FunctionName')) {
108 | return stateVar(node);
109 | }
110 | }
111 | if (node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration') {
112 | scope = scope.upper;
113 | }
114 | if (node === glob.block) {
115 | node.body.push(initFun(init));
116 | node.body.push(returnRenderInit());
117 | }
118 | return node;
119 | }
120 | });
121 | }
122 |
123 | function globalOp(cat, name, ...args) {
124 | return {
125 | type: 'CallExpression',
126 | callee: {
127 | type: 'MemberExpression',
128 | computed: true,
129 | object: {
130 | type: 'MemberExpression',
131 | computed: false,
132 | object: {
133 | type: 'MemberExpression',
134 | computed: false,
135 | object: {
136 | type: 'Identifier',
137 | name: 'global'
138 | },
139 | property: {
140 | type: 'Identifier',
141 | name: 'operators'
142 | }
143 | },
144 | property: {
145 | type: 'Identifier',
146 | name: cat
147 | }
148 | },
149 | property: {
150 | type: 'Literal',
151 | value: name
152 | }
153 | },
154 | arguments: args
155 | };
156 | }
157 |
158 | function postfixUpdate(node, update) {
159 | return {
160 | type: 'CallExpression',
161 | callee: {
162 | type: 'FunctionExpression',
163 | id: null,
164 | params: [{
165 | type: 'Identifier',
166 | name: '__x'
167 | }],
168 | body: {
169 | type: 'BlockStatement',
170 | body: [{
171 | type: 'ExpressionStatement',
172 | expression: update
173 | }, {
174 | type: 'ReturnStatement',
175 | argument: {
176 | type: 'Identifier',
177 | name: '__x'
178 | }
179 | }]
180 | },
181 | rest: null,
182 | generator: false,
183 | expression: false
184 | },
185 | arguments: [{
186 | type: 'UnaryExpression',
187 | operator: '+',
188 | prefix: true,
189 | argument: node
190 | }]
191 | };
192 | }
193 |
194 | function desugarUpdate({argument, operator, prefix}) {
195 | const update = {
196 | type: 'AssignmentExpression',
197 | operator: '=',
198 | left: argument,
199 | right: {
200 | type: 'BinaryExpression',
201 | operator: operator[0],
202 | left: {
203 | type: 'UnaryExpression',
204 | operator: '+',
205 | prefix: true,
206 | argument
207 | },
208 | right: {type: 'Literal', value: 1}
209 | }
210 | };
211 | return prefix ? update : postfixUpdate(argument, update);
212 | }
213 |
214 | function desugarAssignment({operator, left, right}) {
215 | return {
216 | type: 'AssignmentExpression',
217 | operator: '=',
218 | left,
219 | right: {
220 | type: 'BinaryExpression',
221 | operator: operator.slice(0, -1),
222 | left,
223 | right
224 | }
225 | };
226 | }
227 |
228 | export function rewriteOps(ast) {
229 | return replace(ast, {
230 | enter: function enter(node) {
231 | if (node.type === 'AssignmentExpression' && node.operator !== '=') {
232 | return desugarAssignment(node);
233 | }
234 | if (node.type === 'UpdateExpression') {
235 | return desugarUpdate(node);
236 | }
237 | return node;
238 | },
239 | leave: function enter(node) {
240 | if (node.type === 'BinaryExpression') {
241 | return globalOp('binary', node.operator, node.left, node.right);
242 | }
243 | if (node.type === 'UnaryExpression') {
244 | return globalOp('unary', node.operator, node.argument);
245 | }
246 | return node;
247 | }
248 | });
249 | }
250 |
251 | let nextId = 1;
252 |
253 | export function rewriteSymStrings(ast) {
254 | const mapping = {};
255 | const rewritten = replace(ast, {
256 | leave: function enter(node) {
257 | if (node.type === 'Literal' && typeOf(node.value) === 'string') {
258 | const id = nextId++;
259 | mapping[id] = node.loc;
260 | return {
261 | type: 'CallExpression',
262 | callee: {
263 | type: 'MemberExpression',
264 | computed: false,
265 | object: {
266 | type: 'Identifier',
267 | name: 'global'
268 | },
269 | property: {
270 | type: 'Identifier',
271 | name: 'sym'
272 | }
273 | },
274 | arguments: [node, {type: 'Literal', value: id}]
275 | };
276 | }
277 | return node;
278 | }
279 | });
280 | return {ast: rewritten, mapping};
281 | }
282 |
--------------------------------------------------------------------------------
/src/builder.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | import _ from 'lodash';
3 |
4 | import {event} from './actions/state';
5 | import {stringLitCursor, stringLitInsert, stringLitDelete, firstDifference} from './actions/manipulation';
6 | import {operators, typeOf, isSymString, convertSymStr} from './symstr';
7 |
8 | export const eventKeys = [
9 | 'onabort',
10 | 'onautocomplete',
11 | 'onautocompleteerror',
12 | 'oncancel',
13 | 'oncanplay',
14 | 'oncanplaythrough',
15 | 'onchange',
16 | 'onclick',
17 | 'onclose',
18 | 'oncontextmenu',
19 | 'oncuechange',
20 | 'ondblclick',
21 | 'ondrag',
22 | 'ondragend',
23 | 'ondragenter',
24 | 'ondragexit',
25 | 'ondragleave',
26 | 'ondragover',
27 | 'ondragstart',
28 | 'ondrop',
29 | 'ondurationchange',
30 | 'onemptied',
31 | 'onended',
32 | 'oninput',
33 | 'oninvalid',
34 | 'onkeydown',
35 | 'onkeypress',
36 | 'onkeyup',
37 | 'onloadeddata',
38 | 'onloadedmetadata',
39 | 'onloadstart',
40 | 'onmousedown',
41 | 'onmouseenter',
42 | 'onmouseleave',
43 | 'onmousemove',
44 | 'onmouseout',
45 | 'onmouseover',
46 | 'onmouseup',
47 | 'onwheel',
48 | 'onpause',
49 | 'onplay',
50 | 'onplaying',
51 | 'onprogress',
52 | 'onratechange',
53 | 'onreset',
54 | 'onseeked',
55 | 'onseeking',
56 | 'onselect',
57 | 'onshow',
58 | 'onsort',
59 | 'onstalled',
60 | 'onsubmit',
61 | 'onsuspend',
62 | 'ontimeupdate',
63 | 'ontoggle',
64 | 'onvolumechange',
65 | 'onwaiting'];
66 |
67 | export const customEventKeys = ['onframe'];
68 |
69 | function jsxLocAsStringLitLoc(node) {
70 | // JSX elements are identifiers, so need to
71 | // expand loc to left and right for compatibility
72 | // with normal string literals
73 | const {loc: {start, end}} = node;
74 | return {
75 | start: {
76 | line: start.line,
77 | column: start.column - 1
78 | },
79 | end: {
80 | line: end.line,
81 | column: end.column + 1
82 | }
83 | };
84 | }
85 |
86 | export function compileJSX(node) {
87 | if (node.type === 'Literal' && typeOf(node.value) === 'string') {
88 | const loc = jsxLocAsStringLitLoc(node);
89 | return {...node, loc, value: node.value.replace(/\s+/g, ' ')};
90 | }
91 | if (node.type !== 'JSXElement') return node;
92 | const name = node.openingElement.name.name;
93 | const attributes = [];
94 | for (const attr of node.openingElement.attributes) {
95 | const attrKeyLoc = jsxLocAsStringLitLoc(attr.name);
96 | attributes.push({
97 | key: {type: 'Literal', value: attr.name.name, loc: attrKeyLoc},
98 | value: attr.value
99 | });
100 | }
101 | const children = node.children.map(compileJSX).filter(n => {
102 | // drop empty string literals
103 | return n.type !== 'Literal' ||
104 | typeOf(n.value) !== 'string' ||
105 | n.value.length > 0;
106 | });
107 |
108 | const openingLoc = jsxLocAsStringLitLoc(node.openingElement.name);
109 | const loc = node.closingElement
110 | ? {...openingLoc, extra: jsxLocAsStringLitLoc(node.closingElement.name)}
111 | : openingLoc;
112 | return {type: 'ObjectExpression', properties: [
113 | {
114 | type: 'Property',
115 | key: {type: 'Identifier', name: 'name'},
116 | value: {
117 | type: 'Literal',
118 | value: name,
119 | loc
120 | },
121 | kind: 'init',
122 | method: false,
123 | shorthand: false,
124 | computed: false
125 | }, {
126 | type: 'Property',
127 | key: {type: 'Identifier', name: 'attributes'},
128 | value: {type: 'ArrayExpression', elements: attributes.map(({key, value}) => ({type: 'ObjectExpression', properties: [
129 | {
130 | type: 'Property',
131 | key: {type: 'Identifier', name: 'key'},
132 | value: key,
133 | kind: 'init',
134 | method: false,
135 | shorthand: false,
136 | computed: false
137 | }, {
138 | type: 'Property',
139 | key: {type: 'Identifier', name: 'value'},
140 | value,
141 | kind: 'init',
142 | method: false,
143 | shorthand: false,
144 | computed: false
145 | }]}))},
146 | kind: 'init',
147 | method: false,
148 | shorthand: false,
149 | computed: false
150 | }, {
151 | type: 'Property',
152 | key: {type: 'Identifier', name: 'children'},
153 | value: {type: 'ArrayExpression', elements: children},
154 | kind: 'init',
155 | method: false,
156 | shorthand: false,
157 | computed: false
158 | }
159 | ]};
160 | }
161 |
162 | export function wrapHandler(dispatch, func) {
163 | return function handler() {
164 | dispatch(event(() => {
165 | const args = Array.from(arguments).map(convertSymStr);
166 | func.apply(this, args);
167 | }));
168 | };
169 | }
170 |
171 | function submitChange(oldStr, newStr) {
172 | const firstDiff = firstDifference('' + oldStr, newStr);
173 | const diff = newStr.length - oldStr.length;
174 | // if adding characters, look up lit of prev char, else current
175 | const litAt = diff > 0 ? firstDiff - 1 : firstDiff;
176 | const c = oldStr[litAt];
177 | if (!isSymString(c)) return false;
178 | const {id, idx} = c.strs[0];
179 | if (id > 0) {
180 | if (diff > 0) {
181 | const insertStr = newStr.substr(firstDiff, diff);
182 | return stringLitInsert(id, idx, insertStr);
183 | } else if (diff < 0) {
184 | return stringLitDelete(id, idx, -diff);
185 | }
186 | }
187 | return null;
188 | }
189 |
190 | function buildEditableSpan(str, dispatch) {
191 | const elem = $(`${str}`);
192 | elem.prop('contenteditable', true);
193 | elem.on('input', () => {
194 | const action = submitChange(str, elem.text());
195 | if (!action) {
196 | elem.text('' + str);
197 | } else {
198 | dispatch(action);
199 | }
200 | });
201 | elem.on('focus', () => {
202 | const selection = window.getSelection();
203 | const idx = selection && selection.focusOffset || 0;
204 | const c = str[idx];
205 | if (!isSymString(c)) return false;
206 | dispatch(stringLitCursor(c.strs[0].id));
207 | });
208 | elem.on('blur', () => {
209 | dispatch(stringLitCursor(0));
210 | });
211 | return elem;
212 | }
213 |
214 | export function build(dom, dispatch, editable) {
215 | if (typeOf(dom) !== 'object') {
216 | return editable && isSymString(dom)
217 | ? buildEditableSpan(dom, dispatch)
218 | : $(`${dom}`);
219 | }
220 | const el = $(`<${dom.name}>${dom.name}>`);
221 | dom.attributes.forEach(({key, value}) => {
222 | const sKey = `${key}`;
223 | if (sKey === 'style' && typeOf(value) === 'object') {
224 | el.css(value);
225 | } else if (eventKeys.indexOf(sKey) >= 0) {
226 | if (!editable) {
227 | el.on(sKey.substr(2), wrapHandler(dispatch, value));
228 | }
229 | } else if (customEventKeys.indexOf(sKey) < 0) {
230 | el.attr(sKey, value);
231 | }
232 | });
233 | for (const childDom of dom.children) {
234 | if (childDom instanceof Array) {
235 | for (const cd of childDom) {
236 | el.append(build(cd, dispatch, editable));
237 | }
238 | } else {
239 | el.append(build(childDom, dispatch, editable));
240 | }
241 | }
242 | return el;
243 | }
244 |
245 | function add(...strs) {
246 | return strs.reduce((res, str) => operators.binary['+'](res, str), '');
247 | }
248 |
249 | function formatCSS(obj) {
250 | return Object.keys(obj).reduce((str, key) => {
251 | const k = _.snakeCase(key).replace(/_/g, '-');
252 | return add(str, k, ':', obj[key], ';');
253 | }, '');
254 | }
255 |
256 | export function formatHTML(dom, indent = 0) {
257 | const pre = ' '.repeat(indent);
258 | if (typeOf(dom) !== 'object') {
259 | return add(pre, dom, '\n');
260 | }
261 | const attrString = dom.attributes.reduce((str, {key, value}) => {
262 | const sKey = `${key}`;
263 | if (sKey === 'style' && typeOf(value) === 'object') {
264 | return add(str, key, '="', formatCSS(value), '"');
265 | }
266 | if (eventKeys.includes(sKey) || customEventKeys.includes(sKey)) {
267 | return '';
268 | }
269 | return add(str, key, '="', value, '"');
270 | }, '');
271 | let res = add(pre, '<', dom.name);
272 | if (attrString.length > 0) {
273 | res = add(res, ' ', attrString);
274 | }
275 | res = add(res, '>\n');
276 | res = dom.children.reduce((str, childDom) => {
277 | if (childDom instanceof Array) {
278 | return childDom.reduce((cd, prev) => add(prev, formatHTML(cd, indent + 2)), str);
279 | }
280 | return add(str, formatHTML(childDom, indent + 2));
281 | }, res);
282 | return add(res, pre, '', dom.name, '>\n');
283 | }
284 |
--------------------------------------------------------------------------------
/src/components/app.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'redux/react';
3 | import { PageHeader, Row, Col, Panel, Button, Glyphicon, Input, Tabs, Tab } from 'react-bootstrap';
4 | import $ from 'jquery';
5 | import filePicker from 'component-file-picker';
6 |
7 | import { flappy, counter, spiral } from '../examples';
8 | import LiveView from './liveview';
9 | import HTMLView from './htmlview';
10 | import StateView from './stateview';
11 | import Editor from './editor';
12 | import { addVersion, swapVersion } from '../actions/version';
13 | import { reset, swapState, toggleActive } from '../actions/state';
14 | import strategy from '../strategy';
15 |
16 | @connect(state => ({
17 | request: state.version.request,
18 | versionError: state.version.error,
19 | stateError: state.state.error,
20 | currVersion: state.version.current,
21 | maxVersion: state.version.versions.length - 1,
22 | currStateIdx: state.state.current,
23 | maxState: strategy.maxState(state),
24 | isActive: state.state.isActive,
25 | source: state.version.source
26 | }))
27 | export default class App extends Component {
28 | static propTypes = {
29 | request: PropTypes.any,
30 | versionError: PropTypes.any,
31 | stateError: PropTypes.any,
32 | currVersion: PropTypes.number.isRequired,
33 | maxVersion: PropTypes.number.isRequired,
34 | currStateIdx: PropTypes.number.isRequired,
35 | maxState: PropTypes.number.isRequired,
36 | isActive: PropTypes.bool.isRequired,
37 | isDemo: PropTypes.bool,
38 | showTimeControl: PropTypes.bool.isRequired,
39 | source: PropTypes.string,
40 | dispatch: PropTypes.func.isRequired
41 | }
42 |
43 | state = {outkey: 1};
44 |
45 | componentWillMount() {
46 | }
47 |
48 | onChangeVersion(e) {
49 | this.props.dispatch(swapVersion(+e.target.value - 1));
50 | }
51 |
52 | onChangeState(e) {
53 | if (this.props.currStateIdx !== +e.target.value - 1) {
54 | this.props.dispatch(swapState(+e.target.value - 1));
55 | }
56 | }
57 |
58 | onToggle() {
59 | this.props.dispatch(toggleActive());
60 | }
61 |
62 | onOpen() {
63 | /* eslint no-alert:0 */
64 | filePicker({accept: ['.js']}, ([file]) => {
65 | if (file === undefined) return alert('No file selected');
66 | const reader = new FileReader();
67 | reader.onload = () => {
68 | if (reader.result === undefined) return alert('No file contents');
69 | this.props.dispatch(addVersion(reader.result));
70 | };
71 | reader.readAsText(file);
72 | });
73 | }
74 |
75 | onSave() {
76 | const src = encodeURIComponent(this.props.source);
77 | const a = $('')
78 | .attr({
79 | href: 'data:text/plain;charset=utf-8,' + src,
80 | download: 'code.js'
81 | })
82 | .appendTo($('body'));
83 | a[0].click();
84 | a.remove();
85 | }
86 |
87 | onReset() {
88 | this.props.dispatch(reset());
89 | }
90 |
91 | loadFlappy(evt) {
92 | evt.preventDefault();
93 | this.props.dispatch(addVersion(flappy));
94 | this.props.dispatch(reset());
95 | }
96 |
97 | loadCounter(evt) {
98 | evt.preventDefault();
99 | this.props.dispatch(addVersion(counter));
100 | this.props.dispatch(reset());
101 | }
102 |
103 | loadSpiral(evt) {
104 | evt.preventDefault();
105 | this.props.dispatch(addVersion(spiral));
106 | this.props.dispatch(reset());
107 | }
108 |
109 | handleOutSelect(key) {
110 | this.setState({outkey: key});
111 | }
112 |
113 | sourceHeader() {
114 | return (
115 | Source
116 | {!this.props.isDemo && (
117 |
118 |
122 | {' '}
123 |
127 | {' '}
128 |
132 | )}
133 | );
134 | }
135 |
136 | versionStyle() {
137 | const { request, versionError } = this.props;
138 | if (request) return 'warning';
139 | return versionError ? 'danger' : 'success';
140 | }
141 |
142 | versionFooter() {
143 | return this.props.versionError && this.props.versionError.toString();
144 | }
145 |
146 | stateStyle() {
147 | return this.props.stateError ? 'danger' : undefined;
148 | }
149 |
150 | stateFooter() {
151 | return this.props.stateError && this.props.stateError.toString();
152 | }
153 |
154 | render() {
155 | const { currVersion, maxVersion, currStateIdx, maxState, isActive, isDemo, showTimeControl } = this.props;
156 | return (
157 |
158 | {!isDemo && (
159 |
160 | Reactive Live Programming
161 | )}
162 |
163 |
164 |
165 |
166 |
167 |
168 | {showTimeControl && (
169 |
170 |
171 |
172 | Version
173 |
174 |
175 |
180 |
181 |
182 |
188 |
189 |
190 | {!isDemo &&
191 | }
194 | {!isDemo &&
195 | }
198 | {isDemo &&
199 | }
202 |
203 |
204 |
205 |
206 |
207 | State
208 |
209 |
210 |
215 |
216 |
217 |
223 |
224 |
225 |
228 | {!isDemo &&
229 | }
232 |
233 |
234 | )}
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 | );
253 | }
254 | }
255 |
--------------------------------------------------------------------------------
/test/symstr.js:
--------------------------------------------------------------------------------
1 | /* globals describe, before, it */
2 | /* eslint no-unused-expressions:0 */
3 | import {expect} from 'chai';
4 |
5 | import {SymString, operators} from '../src/symstr.js';
6 |
7 | export default function tests() {
8 |
9 | const str = SymString.single('abc', 23);
10 |
11 | it('get', () => {
12 | // Returns the character at the specified index.
13 | const res = str[1];
14 | expect(res.strs).to.deep.equal([{str: 'b', id: 23, idx: 1}]);
15 | expect(str[-1]).to.be.undefined;
16 | expect(str[3]).to.be.undefined;
17 | expect(str.a).to.be.undefined;
18 | });
19 |
20 | it('set', () => {
21 | const prev = str.strs;
22 | expect(str[1] = 'x').to.be.equal('x');
23 | expect(str.strs).to.deep.equal(prev);
24 | });
25 |
26 | it('delete', () => {
27 | const prev = str.strs;
28 | delete str[1];
29 | expect(str.strs).to.deep.equal(prev);
30 | });
31 |
32 | it('defineProperty', () => {
33 | expect(() => Object.defineProperty(str, 1, {})).to.throw(TypeError);
34 | });
35 |
36 | it.skip('has', () => {
37 | expect(() => 1 in str).to.throw(TypeError);
38 | });
39 |
40 | it('ownKeys', () => {
41 | expect(() => Object.ownKeys(str)).to.throw(TypeError);
42 | });
43 |
44 | it('enumerate', () => {
45 | const res = [];
46 | /* eslint guard-for-in:0 */
47 | for (const key in str) {
48 | res.push(key);
49 | }
50 | expect(res).to.deep.equal(['0', '1', '2']);
51 | });
52 |
53 | it('charAt', () => {
54 | // Returns the character at the specified index.
55 | const res = str.charAt(1);
56 | expect(res.strs).to.deep.equal([{str: 'b', id: 23, idx: 1}]);
57 | });
58 |
59 | it('charCodeAt', () => {
60 | // Returns a number indicating the Unicode value of the character at the given index.
61 | const res = str.charCodeAt(1);
62 | expect(res).to.be.equal(98);
63 | });
64 |
65 | it('codePointAt', () => {
66 | // Returns a non-negative integer that is the UTF-16 encoded code point value at the given position.
67 | const res = str.codePointAt(1);
68 | expect(res).to.be.equal(98);
69 | });
70 |
71 | it('concat', () => {
72 | // Combines the text of two strings and returns a new string.
73 | expect(str.concat().toSourceString()).to.be.equal('abc');
74 | expect(str.concat('d').toSourceString()).to.be.equal('abcd');
75 | expect(str.concat('de').toSourceString()).to.be.equal('abcde');
76 | });
77 |
78 | it('includes', () => {
79 | // Determines whether one string may be found within another string.
80 | expect(str.includes('d')).to.be.false;
81 | expect(str.includes('c')).to.be.true;
82 | });
83 |
84 | it('endsWith', () => {
85 | // Determines whether a string ends with the characters of another string.
86 | expect(str.endsWith('d')).to.be.false;
87 | expect(str.endsWith('bc')).to.be.true;
88 | });
89 |
90 | it('indexOf', () => {
91 | // Returns the index within the calling String object of the first occurrence of the specified value, or -1 if not found.
92 | expect(str.indexOf('d')).to.be.equal(-1);
93 | expect(str.indexOf('bc')).to.be.equal(1);
94 | });
95 |
96 | it('lastIndexOf', () => {
97 | // Returns the index within the calling String object of the last occurrence of the specified value, or -1 if not found.
98 | expect(str.lastIndexOf('d')).to.be.equal(-1);
99 | expect(str.lastIndexOf('bc')).to.be.equal(1);
100 | });
101 |
102 | it('localeCompare', () => {
103 | // Returns a number indicating whether a reference string comes before or after or is the same as the given string in sort order.
104 | expect(str.localeCompare('aa')).to.be.equal(1);
105 | expect(str.localeCompare('c')).to.be.equal(-1);
106 | });
107 |
108 | it('match', () => {
109 | // Used to match a regular expression against a string.
110 | expect(str.match(/bb/)).to.be.null;
111 | expect(str.match(/ab/)[0]).to.be.equal('ab');
112 | });
113 |
114 | it('normalize', () => {
115 | // Returns the Unicode Normalization Form of the calling string value.
116 | expect(str.normalize().strs).to.deep.equal([{str: 'abc', id: 23, idx: 0}]);
117 | });
118 |
119 | it('quote', () => {
120 | // Wraps the string in double quotes (""").
121 | expect(str.quote().toSourceString()).to.be.equal('"abc"');
122 | });
123 |
124 | it('repeat', () => {
125 | // Returns a string consisting of the elements of the object repeated the given times.
126 | expect(str.repeat(0)).to.be.equal('');
127 | expect(str.repeat(1).toSourceString()).to.be.equal('abc');
128 | expect(str.repeat(2).toSourceString()).to.be.equal('abcabc');
129 | });
130 |
131 | it('replace', () => {
132 | // Used to find a match between a regular expression and a string, and to replace the matched substring with a new substring.
133 | expect(str.replace('d', 'x').toSourceString()).to.be.equal('abc');
134 | expect(str.replace('b', 'x').toSourceString()).to.be.equal('axc');
135 | expect(str.replace(/[ac]/g, 'x').toSourceString()).to.be.equal('xbx');
136 | });
137 |
138 | it('search', () => {
139 | // Executes the search for a match between a regular expression and a specified string.
140 | expect(str.search('b')).to.be.equal(1);
141 | });
142 |
143 | it('slice', () => {
144 | // Extracts a section of a string and returns a new string.
145 | expect(str.slice(1, 2).toSourceString()).to.be.equal('b');
146 | expect(str.slice(1, 2).strs).to.deep.equal([{str: 'b', id: 23, idx: 1}]);
147 | expect(str.slice(1, 3).strs).to.deep.equal([{str: 'bc', id: 23, idx: 1}]);
148 | expect(str.slice(0, 2).strs).to.deep.equal([{str: 'ab', id: 23, idx: 0}]);
149 | });
150 |
151 | it('split', () => {
152 | // Splits a String object into an array of strings by separating the string into substrings.
153 | const res = str.split('b');
154 | expect(res[0].toSourceString()).to.be.equal('a');
155 | expect(res[1].toSourceString()).to.be.equal('c');
156 | const res2 = str.split(/b/);
157 | expect(res2[0].toSourceString()).to.be.equal('a');
158 | expect(res2[1].toSourceString()).to.be.equal('c');
159 | });
160 |
161 | it('startsWith', () => {
162 | // Determines whether a string begins with the characters of another string.
163 | expect(str.startsWith('b')).to.be.false;
164 | expect(str.startsWith('ab')).to.be.true;
165 | });
166 |
167 | it('substr', () => {
168 | // Returns the characters in a string beginning at the specified location through the specified number of characters.
169 | expect(str.substr(1, 2).toSourceString()).to.be.equal('bc');
170 | });
171 |
172 | it('substring', () => {
173 | // Returns the characters in a string between two indexes into the string.
174 | expect(str.substring(1, 2).toSourceString()).to.be.equal('b');
175 | });
176 |
177 | it('toLocaleLowerCase', () => {
178 | // The characters within a string are converted to lower case while respecting the current locale. For most languages, this will return the same as toLowerCase().
179 | expect(str.toLocaleLowerCase().toSourceString()).to.be.equal('abc');
180 | });
181 |
182 | it('toLocaleUpperCase', () => {
183 | // The characters within a string are converted to upper case while respecting the current locale. For most languages, this will return the same as toUpperCase().
184 | expect(str.toLocaleUpperCase().toSourceString()).to.be.equal('ABC');
185 | });
186 |
187 | it('toLowerCase', () => {
188 | // Returns the calling string value converted to lower case.
189 | expect(str.toLowerCase().toSourceString()).to.be.equal('abc');
190 | });
191 |
192 | it('toString', () => {
193 | // Returns a string representing the specified object. Overrides the Object.prototype.toString() method.
194 | expect(str.toString().strs).to.deep.equal(str.strs);
195 | });
196 |
197 | it('toUpperCase', () => {
198 | // Returns the calling string value converted to uppercase.
199 | expect(str.toUpperCase().toSourceString()).to.be.equal('ABC');
200 | });
201 |
202 | it('trim', () => {
203 | // Trims whitespace from the beginning and end of the string. Part of the ECMAScript 5 standard.
204 | const res = SymString.single(' abc ').trim();
205 | expect(res.toSourceString()).to.be.equal('abc');
206 | });
207 |
208 | it('trimLeft', () => {
209 | // Trims whitespace from the left side of the string.
210 | const res = SymString.single(' abc ').trimLeft();
211 | expect(res.toSourceString()).to.be.equal('abc ');
212 | });
213 |
214 | it('trimRight', () => {
215 | // Trims whitespace from the right side of the string.
216 | const res = SymString.single(' abc ').trimRight();
217 | expect(res.toSourceString()).to.be.equal(' abc');
218 | });
219 |
220 | it('valueOf', () => {
221 | // Returns the primitive value of the specified object. Overrides the Object.prototype.valueOf() method.
222 | expect(str.toString().strs).to.deep.equal(str.strs);
223 | });
224 |
225 | it('anchor', () => {
226 | // (hypertext target)
227 | const res = str.anchor('name').toSourceString();
228 | expect(res).to.be.equal('abc');
229 | });
230 |
231 | it('big', () => {
232 | //
233 | const res = str.big().toSourceString();
234 | expect(res).to.be.equal('abc');
235 | });
236 |
237 | it('blink', () => {
238 | //