├── .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}>`).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}>`); 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, '\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 | // 239 | const res = str.blink().toSourceString(); 240 | expect(res).to.be.equal('abc'); 241 | }); 242 | 243 | it('bold', () => { 244 | // 245 | const res = str.bold().toSourceString(); 246 | expect(res).to.be.equal('abc'); 247 | }); 248 | 249 | it('fixed', () => { 250 | // 251 | const res = str.fixed().toSourceString(); 252 | expect(res).to.be.equal('abc'); 253 | }); 254 | 255 | it('fontcolor', () => { 256 | // 257 | const res = str.fontcolor('#fff').toSourceString(); 258 | expect(res).to.be.equal('abc'); 259 | }); 260 | 261 | it('fontsize', () => { 262 | // 263 | const res = str.fontsize(20).toSourceString(); 264 | expect(res).to.be.equal('abc'); 265 | }); 266 | 267 | it('italics', () => { 268 | // 269 | const res = str.italics().toSourceString(); 270 | expect(res).to.be.equal('abc'); 271 | }); 272 | 273 | it('link', () => { 274 | // (link to URL) 275 | const res = str.link('http://tom').toSourceString(); 276 | expect(res).to.be.equal('abc'); 277 | }); 278 | 279 | it('small', () => { 280 | // 281 | const res = str.small().toSourceString(); 282 | expect(res).to.be.equal('abc'); 283 | }); 284 | 285 | it('strike', () => { 286 | // 287 | const res = str.strike().toSourceString(); 288 | expect(res).to.be.equal('abc'); 289 | }); 290 | 291 | it('sub', () => { 292 | // 293 | const res = str.sub().toSourceString(); 294 | expect(res).to.be.equal('abc'); 295 | }); 296 | 297 | it('sup', () => { 298 | // 299 | const res = str.sup().toSourceString(); 300 | expect(res).to.be.equal('abc'); 301 | }); 302 | 303 | const {unary, binary} = operators; 304 | 305 | it('instanceof', () => { 306 | expect(binary.instanceof(str, String)).to.be.true; 307 | }); 308 | 309 | it('typeof', () => { 310 | expect(unary.typeof(str)).to.be.equal('string'); 311 | }); 312 | 313 | } 314 | -------------------------------------------------------------------------------- /src/symstr.js: -------------------------------------------------------------------------------- 1 | let unary; 2 | let binary; 3 | 4 | export function isSymString(val) { 5 | if (val === undefined || val === null) { 6 | return false; 7 | } 8 | return val.isSymString; 9 | } 10 | 11 | export function typeOf(val) { 12 | if (isSymString(val)) { 13 | return 'string'; 14 | } 15 | return typeof val; 16 | } 17 | 18 | function wrap(x) { 19 | return new Proxy(x, { 20 | get: (target, key) => { 21 | if (key === Symbol.toPrimitive || key === Symbol.toStringTag) { 22 | return target.toPrimitive; 23 | } 24 | if (key === Symbol.iterator) { 25 | const chars = []; 26 | for (let i = 0; i < target.getLength(); i++) { 27 | chars.push(target.charAt(i)); 28 | } 29 | return chars[Symbol.iterator]; 30 | } 31 | if (typeof key === 'symbol') return target[key]; 32 | if (+key >= 0 && +key < target.getLength()) { 33 | return target.charAt(+key); 34 | } 35 | if (key === 'length') { 36 | return target.getLength(); 37 | } 38 | if (key === 'isSymString') { 39 | return true; 40 | } 41 | return target[key]; 42 | }, 43 | set: (target, key, value) => value, 44 | deleteProperty: () => true, 45 | getPrototypeOf: () => String.prototype, // FIXME: Needs to wrapped 46 | getOwnPropertyDescriptor: (target, prop) => { 47 | if (+prop >= 0 && +prop < target.getLength()) { 48 | return { 49 | value: target.charAt(+prop), 50 | writable: false, 51 | enumerable: true, 52 | configurable: true // FIXME: might need to be false 53 | }; 54 | } 55 | return undefined; 56 | }, 57 | defineProperty: () => { throw new TypeError('defineProperty invalid'); }, 58 | has: function has(target, key) { 59 | return this.ownKeys(target).includes(key); 60 | }, 61 | ownKeys: (target) => { 62 | const keys = []; 63 | for (let i = 0; i < target.getLength(); i++) { 64 | keys.push('' + i); 65 | } 66 | return keys; 67 | }, 68 | enumerate: function enumerate(target) { 69 | return this.ownKeys(target)[Symbol.iterator](); 70 | } 71 | }); 72 | } 73 | 74 | // String which track origin information ID 75 | export class SymString { 76 | 77 | constructor(parts) { 78 | this.strs = parts; 79 | } 80 | 81 | ensureLength() { 82 | if (this.length >= 0) return; 83 | if (this.strs.length === 0) { 84 | this.length = 0; 85 | return; 86 | } 87 | const parts = [{...this.strs[0], start: 0}]; 88 | for (let i = 1; i < this.strs.length; i++) { 89 | const {str, id} = this.strs[i]; 90 | const prev = parts[parts.length - 1]; 91 | if (id === 0 && prev.id === 0) { 92 | // coalesce basic strings 93 | prev.str += str; 94 | } else { 95 | // add new tracked strlit part 96 | parts.push({...this.strs[i], start: prev.start + prev.str.length}); 97 | } 98 | } 99 | const last = parts[parts.length - 1]; 100 | this.length = last.start + last.str.length; 101 | this.strs = parts; 102 | } 103 | 104 | getLength() { 105 | this.ensureLength(); 106 | return this.length; 107 | } 108 | 109 | add(other) { 110 | return SymString.create([...this.strs, ...other.strs]); 111 | } 112 | 113 | isSymString = true; 114 | 115 | toSourceString() { 116 | return this.strs.reduce((res, {str}) => res + str, ''); 117 | } 118 | 119 | toJSON() { 120 | return this.toSourceString(); 121 | } 122 | 123 | toPrimitive(hint) { 124 | if (hint === 'number') { 125 | return +this.toSourceString(); 126 | } 127 | if (hint === 'string' || hint === 'default') { 128 | return this.toSourceString(); 129 | } 130 | return this.toSourceString().length > 0; 131 | } 132 | 133 | mapParts(f) { 134 | return SymString.create(this.strs.map(({str, id, idx}) => { 135 | return {str: f(str), id, idx}; 136 | })); 137 | } 138 | 139 | // String methods 140 | 141 | charAt(i) { 142 | if (i < 0) return ''; 143 | this.ensureLength(); 144 | const group = this.strs.find(({str, start}) => start + str.length > i); 145 | if (group === undefined) return ''; 146 | const localIdx = i - group.start; 147 | return SymString.single(group.str[localIdx], group.id, group.idx + localIdx); 148 | } 149 | 150 | charCodeAt(index) { 151 | // Returns a number indicating the Unicode value of the character at the given index. 152 | return this.toSourceString().charCodeAt(index); 153 | } 154 | 155 | codePointAt(pos) { 156 | // Returns a non-negative integer that is the UTF-16 encoded code point value at the given position. 157 | return this.toSourceString().codePointAt(pos); 158 | } 159 | 160 | concat(...strs) { 161 | // Combines the text of two strings and returns a new string. 162 | const add = binary['+']; 163 | return strs.reduce((res, str) => add(res, str), this); 164 | } 165 | 166 | includes(searchStr, pos) { 167 | // Determines whether one string may be found within another string. 168 | return this.toSourceString().includes(searchStr, pos); 169 | } 170 | 171 | endsWith(searchStr, pos) { 172 | // Determines whether a string ends with the characters of another string. 173 | return this.toSourceString().endsWith(searchStr, pos); 174 | } 175 | 176 | indexOf(searchStr, pos) { 177 | // Returns the index within the calling String object of the first occurrence of the specified value, or -1 if not found. 178 | return this.toSourceString().indexOf(searchStr, pos); 179 | } 180 | 181 | lastIndexOf(searchStr, pos) { 182 | // Returns the index within the calling String object of the last occurrence of the specified value, or -1 if not found. 183 | return this.toSourceString().lastIndexOf(searchStr, pos); 184 | } 185 | 186 | localeCompare(compareString, locales, options) { 187 | // Returns a number indicating whether a reference string comes before or after or is the same as the given string in sort order. 188 | return this.toSourceString().localeCompare(compareString, locales, options); 189 | } 190 | 191 | match(regexp) { 192 | // Used to match a regular expression against a string. 193 | return this.toSourceString().match(regexp); 194 | } 195 | 196 | normalize(form) { 197 | // Returns the Unicode Normalization Form of the calling string value. 198 | return this.mapParts(p => p.normalize(form)); 199 | } 200 | 201 | quote() { 202 | // Wraps the string in double quotes ("""). 203 | const add = binary['+']; 204 | return add(add('"', this), '"'); 205 | } 206 | 207 | repeat(count) { 208 | // Returns a string consisting of the elements of the object repeated the given times. 209 | const add = binary['+']; 210 | let result = ''; 211 | for (let i = 0; i < count; i++) { 212 | result = add(result, this); 213 | } 214 | return result; 215 | } 216 | 217 | replace(substr, newSubStr) { 218 | const str = this.toSourceString(); 219 | const add = binary['+']; 220 | const typeo = unary.typeof; 221 | const replacement = typeof newSubStr === 'function' 222 | ? newSubStr() 223 | : newSubStr; 224 | let indices = []; 225 | if (typeo(substr) === 'string') { 226 | const idx = str.indexOf(substr); 227 | if (idx >= 0) indices = [{index: idx, length: substr.length}]; 228 | } else if (substr instanceof RegExp) { 229 | let res = substr.exec(str); 230 | while (res) { 231 | indices.push({index: res.index, length: res[0].length}); 232 | res = substr.global && substr.exec(str); 233 | } 234 | } 235 | let result = ''; 236 | let prevIdx = 0; 237 | for (const {index, length} of indices) { 238 | result = add(result, this.substring(prevIdx, index)); 239 | result = add(result, replacement); 240 | prevIdx = index + length; 241 | } 242 | const lastIdx = indices.length - 1; 243 | const last = lastIdx >= 0 244 | ? indices[lastIdx].index + indices[lastIdx].length 245 | : 0; 246 | return add(result, this.substr(last)); 247 | } 248 | 249 | search(regexp) { 250 | // Executes the search for a match between a regular expression and a specified string. 251 | return this.toSourceString().search(regexp); 252 | } 253 | 254 | slice(beginSlice, endSlice) { 255 | // Extracts a section of a string and returns a new string. 256 | return this.substring(beginSlice, endSlice); 257 | // FIXME: small differences 258 | } 259 | 260 | split(separator, limit) { 261 | // Splits a String object into an array of strings by separating the string into substrings. 262 | const str = this.toSourceString(); 263 | let indices = []; 264 | const typeo = unary.typeof; 265 | let re; 266 | if (typeo(separator) === 'string') { 267 | re = RegExp(separator, 'g'); 268 | } else if (separator instanceof RegExp) { 269 | let flags = 'g'; 270 | if (separator.ignoreCase) flags += 'i'; 271 | if (separator.multiline) flags += 'm'; 272 | re = RegExp(separator.source, flags); 273 | } 274 | indices = []; 275 | let res = re.exec(str); 276 | while (res) { 277 | indices.push({index: res.index, length: res[0].length}); 278 | res = re.global && re.exec(str); 279 | } 280 | const result = []; 281 | let prevIdx = 0; 282 | for (const {index, length} of indices) { 283 | result.push(this.substring(prevIdx, index)); 284 | prevIdx = index + length; 285 | } 286 | const lastIdx = indices.length - 1; 287 | const last = lastIdx >= 0 288 | ? indices[lastIdx].index + indices[lastIdx].length 289 | : 0; 290 | result.push(this.substr(last)); 291 | return result.slice(0, limit); 292 | } 293 | 294 | startsWith(searchStr, pos) { 295 | // Determines whether a string begins with the characters of another string. 296 | return this.toSourceString().startsWith(searchStr, pos); 297 | } 298 | 299 | substr(start, length = this.length - start) { 300 | // Returns the characters in a string beginning at the specified location through the specified number of characters. 301 | return this.substring(start, start + length); 302 | } 303 | 304 | substring(startIdx, endIdx = this.length) { 305 | // Returns the characters in a string between two indexes into the string. 306 | this.ensureLength(); 307 | const startGroup = this.strs.findIndex(({str, start}) => start + str.length > startIdx); 308 | if (startGroup === -1) return ''; 309 | const parts = []; 310 | for (let i = startGroup; i < this.strs.length; i++) { 311 | const {str, id, idx, start} = this.strs[i]; 312 | const from = Math.max(0, startIdx - start); 313 | const to = Math.max(0, endIdx - start); 314 | const sstr = str.substring(from, to); 315 | if (sstr.length > 0) { 316 | parts.push({ 317 | str: sstr, 318 | id, 319 | idx: idx + from 320 | }); 321 | } 322 | } 323 | return SymString.create(parts); 324 | } 325 | 326 | toLocaleLowerCase() { 327 | // 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(). 328 | return this.mapParts(p => p.toLocaleLowerCase()); 329 | } 330 | 331 | toLocaleUpperCase() { 332 | // 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(). 333 | return this.mapParts(p => p.toLocaleUpperCase()); 334 | } 335 | 336 | toLowerCase() { 337 | // Returns the calling string value converted to lower case. 338 | return this.mapParts(p => p.toLowerCase()); 339 | } 340 | 341 | toSource() { 342 | // Returns an object literal representing the specified object; you can use this value to create a new object. Overrides the Object.prototype.toSource() method. 343 | return this.toSourceString().toSource(); 344 | } 345 | 346 | toString() { 347 | // Returns a string representing the specified object. Overrides the Object.prototype.toString() method. 348 | return this; 349 | } 350 | 351 | toUpperCase() { 352 | // Returns the calling string value converted to uppercase. 353 | return this.mapParts(p => p.toUpperCase()); 354 | } 355 | 356 | trim() { 357 | // Trims whitespace from the beginning and end of the string. Part of the ECMAScript 5 standard. 358 | return this.trimRight().trimLeft(); 359 | } 360 | 361 | trimLeft() { 362 | // Trims whitespace from the left side of the string. 363 | const ws = this.match(/^\s+/).length; 364 | return this.substr(ws); 365 | } 366 | 367 | trimRight() { 368 | // Trims whitespace from the right side of the string. 369 | const ws = this.match(/\s+$/).length; 370 | return this.substr(0, this.getLength() - ws); 371 | } 372 | 373 | valueOf() { 374 | // Returns the primitive value of the specified object. Overrides the Object.prototype.valueOf() method. 375 | return this; 376 | } 377 | 378 | anchor(name) { 379 | // (hypertext target) 380 | const add = binary['+']; 381 | return add(add(add(add(''), this), ''); 382 | } 383 | 384 | big() { 385 | // 386 | const add = binary['+']; 387 | return add(add('', this), ''); 388 | } 389 | 390 | blink() { 391 | // 392 | const add = binary['+']; 393 | return add(add('', this), ''); 394 | } 395 | 396 | bold() { 397 | // 398 | const add = binary['+']; 399 | return add(add('', this), ''); 400 | } 401 | 402 | fixed() { 403 | // 404 | const add = binary['+']; 405 | return add(add('', this), ''); 406 | } 407 | 408 | fontcolor(color) { 409 | // 410 | const add = binary['+']; 411 | return add(add(``, this), ''); 412 | } 413 | 414 | fontsize(size) { 415 | // 416 | const add = binary['+']; 417 | return add(add(``, this), ''); 418 | } 419 | 420 | italics() { 421 | // 422 | const add = binary['+']; 423 | return add(add('', this), ''); 424 | } 425 | 426 | link(url) { 427 | // (link to URL) 428 | const add = binary['+']; 429 | return add(add(add(add(''), this), ''); 430 | } 431 | 432 | small() { 433 | // 434 | const add = binary['+']; 435 | return add(add('', this), ''); 436 | } 437 | 438 | strike() { 439 | // 440 | const add = binary['+']; 441 | return add(add('', this), ''); 442 | } 443 | 444 | sub() { 445 | // 446 | const add = binary['+']; 447 | return add(add('', this), ''); 448 | } 449 | 450 | sup() { 451 | // 452 | const add = binary['+']; 453 | return add(add('', this), ''); 454 | } 455 | 456 | static single(s, id = 0, idx = 0) { 457 | const str = String(s); 458 | if (str.length === 0) return ''; 459 | return wrap(new SymString([{str, id, idx}])); 460 | } 461 | 462 | static create(parts) { 463 | if (parts.length === 0) return ''; 464 | return wrap(new SymString(parts)); 465 | } 466 | } 467 | 468 | function sym(val) { 469 | if (isSymString(val)) { 470 | return val; 471 | } 472 | return SymString.single(val); 473 | } 474 | 475 | 476 | unary = { 477 | '-': (op) => { 478 | if (isSymString(op)) { 479 | return +op.toSourceString(); 480 | } 481 | return -op; 482 | }, 483 | '+': (op) => { 484 | if (isSymString(op)) { 485 | return +op.toSourceString(); 486 | } 487 | return +op; 488 | }, 489 | '!': (op) => { 490 | if (isSymString(op)) { 491 | return false; 492 | } 493 | return !op; 494 | }, 495 | '~': (op) => { 496 | if (isSymString(op)) { 497 | return -1; 498 | } 499 | return ~op; 500 | }, 501 | 'typeof': typeOf, 502 | 'void': (o) => void(o) 503 | // do not rewrite: delete 504 | }; 505 | 506 | binary = { 507 | '==': (l, r) => { 508 | /* eslint eqeqeq:0 */ 509 | const left = isSymString(l) ? l.toSourceString() : l; 510 | const right = isSymString(r) ? r.toSourceString() : r; 511 | return left == right; 512 | }, 513 | '!=': (l, r) => { 514 | const left = isSymString(l) ? l.toSourceString() : l; 515 | const right = isSymString(r) ? r.toSourceString() : r; 516 | return left != right; 517 | }, 518 | '===': (l, r) => { 519 | const left = isSymString(l) ? l.toSourceString() : l; 520 | const right = isSymString(r) ? r.toSourceString() : r; 521 | return left === right; 522 | }, 523 | '!==': (l, r) => { 524 | const left = isSymString(l) ? l.toSourceString() : l; 525 | const right = isSymString(r) ? r.toSourceString() : r; 526 | return left !== right; 527 | }, 528 | '<': (l, r) => { 529 | const left = isSymString(l) ? l.toSourceString() : l; 530 | const right = isSymString(r) ? r.toSourceString() : r; 531 | return left < right; 532 | }, 533 | '<=': (l, r) => { 534 | const left = isSymString(l) ? l.toSourceString() : l; 535 | const right = isSymString(r) ? r.toSourceString() : r; 536 | return left <= right; 537 | }, 538 | '>': (l, r) => { 539 | const left = isSymString(l) ? l.toSourceString() : l; 540 | const right = isSymString(r) ? r.toSourceString() : r; 541 | return left > right; 542 | }, 543 | '>=': (l, r) => { 544 | const left = isSymString(l) ? l.toSourceString() : l; 545 | const right = isSymString(r) ? r.toSourceString() : r; 546 | return left >= right; 547 | }, 548 | '<<': (l, r) => { 549 | const left = isSymString(l) ? l.toSourceString() : l; 550 | const right = isSymString(r) ? r.toSourceString() : r; 551 | return left << right; 552 | }, 553 | '>>': (l, r) => { 554 | const left = isSymString(l) ? l.toSourceString() : l; 555 | const right = isSymString(r) ? r.toSourceString() : r; 556 | return left >> right; 557 | }, 558 | '>>>': (l, r) => { 559 | const left = isSymString(l) ? l.toSourceString() : l; 560 | const right = isSymString(r) ? r.toSourceString() : r; 561 | return left >>> right; 562 | }, 563 | '+': (left, right) => { 564 | if (isSymString(left) || 565 | isSymString(right)) { 566 | const leftSym = sym(left); 567 | const rightSym = sym(right); 568 | if (!(isSymString(leftSym))) return rightSym; 569 | if (!(isSymString(rightSym))) return leftSym; 570 | return leftSym.add(rightSym); 571 | } 572 | return left + right; 573 | }, 574 | '-': (l, r) => { 575 | const left = isSymString(l) ? l.toSourceString() : l; 576 | const right = isSymString(r) ? r.toSourceString() : r; 577 | return left - right; 578 | }, 579 | '*': (l, r) => { 580 | const left = isSymString(l) ? l.toSourceString() : l; 581 | const right = isSymString(r) ? r.toSourceString() : r; 582 | return left * right; 583 | }, 584 | '/': (l, r) => { 585 | const left = isSymString(l) ? l.toSourceString() : l; 586 | const right = isSymString(r) ? r.toSourceString() : r; 587 | return left / right; 588 | }, 589 | '%': (l, r) => { 590 | const left = isSymString(l) ? l.toSourceString() : l; 591 | const right = isSymString(r) ? r.toSourceString() : r; 592 | return left % right; 593 | }, 594 | '|': (l, r) => { 595 | const left = isSymString(l) ? l.toSourceString() : l; 596 | const right = isSymString(r) ? r.toSourceString() : r; 597 | return left | right; 598 | }, 599 | '^': (l, r) => { 600 | const left = isSymString(l) ? l.toSourceString() : l; 601 | const right = isSymString(r) ? r.toSourceString() : r; 602 | return left ^ right; 603 | }, 604 | '&': (l, r) => { 605 | const left = isSymString(l) ? l.toSourceString() : l; 606 | const right = isSymString(r) ? r.toSourceString() : r; 607 | return left & right; 608 | }, 609 | 'in': (l, r) => { 610 | const left = isSymString(l) ? l.toSourceString() : l; 611 | const right = isSymString(r) ? r.toSourceString() : r; 612 | return left in right; 613 | }, 614 | 'instanceof': (obj, clz) => { 615 | if (isSymString(obj) && clz === String || clz === SymString) { 616 | return true; 617 | } 618 | return obj instanceof clz; 619 | } 620 | }; 621 | 622 | export function convertSymStr(x) { 623 | if (isSymString(x)) return x; 624 | if (typeOf(x) === 'string') return sym(x); 625 | return new Proxy(x, { 626 | get: (target, key) => convertSymStr(target[key]) 627 | }); 628 | } 629 | 630 | export const operators = { unary, binary }; 631 | --------------------------------------------------------------------------------