├── Makefile ├── app.css ├── components ├── ace-editor.jsx ├── app.jsx ├── call-stack-item.jsx ├── call-stack.jsx ├── callback-queue.jsx ├── callback.jsx ├── editor.jsx ├── event-loop-spinner.jsx ├── html-editor.jsx ├── render-queue.jsx ├── settings-panel.jsx ├── web-api-query.jsx ├── web-api-timer.jsx └── web-apis.jsx ├── demo.html ├── index.html ├── lib ├── delay.js ├── instrument-code.js ├── plugins │ ├── console.js │ └── query.js ├── tag.js ├── text-cursor.js └── wrap-insertion-points.js ├── loupe.bundle.css ├── loupe.bundle.js ├── loupe.css ├── loupe.js ├── loupe.jsx ├── models ├── apis.js ├── base-collection.js ├── callback-queue.js ├── callback.js ├── callstack.js ├── code.js ├── render-queue.js ├── stack-frame.js ├── stack-frames.js ├── timeout.js └── timeouts.js ├── npm-debug.log ├── package.json ├── router.js ├── templates.js └── tests └── text-cursor-test.js /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | PATH := node_modules/.bin:$(PATH) 3 | 4 | JS_FILES := $(shell glob-cli "lib/**/*.js" "models/**/*.js" "*.js") 5 | 6 | JSX_FILES := $(shell glob-cli "components/**/*.jsx") 7 | 8 | build: loupe.bundle.js loupe.bundle.css 9 | 10 | loupe.bundle.js: $(JS_FILES) $(JSX_FILES) 11 | browserify -t reactify loupe.js > loupe.bundle.js 12 | 13 | loupe.bundle.css: loupe.css 14 | autoprefixer loupe.css -o loupe.bundle.css 15 | -------------------------------------------------------------------------------- /app.css: -------------------------------------------------------------------------------- 1 | *, *:before, *:after { 2 | -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; 3 | } 4 | 5 | html, body { 6 | min-height: 100%; 7 | font-family: courier; 8 | font-size: 18px; 9 | } 10 | 11 | #timer-queue { 12 | width: 100px; 13 | position: absolute; 14 | top: 20px; 15 | right: 20px; 16 | } 17 | #timer-queue li { 18 | background: gray; 19 | padding: 20px 20 | } 21 | 22 | .editor { 23 | height: 100%; 24 | width: 100%; 25 | border: 1px gray solid; 26 | font-family: monospace; 27 | font-size: 16px; 28 | line-height: 1.5; 29 | padding: 10px; 30 | margin: 10px; 31 | display: inline-block; 32 | white-space: pre; 33 | } 34 | 35 | .editor span.running { 36 | background: rgba(236, 117, 74, 0.25); 37 | } 38 | 39 | .code-node { 40 | } 41 | 42 | .code-node.running { 43 | background: rgba(255,0,0,0.5); 44 | } 45 | 46 | .code-node:not(.running) { 47 | background: none 48 | transition: background 0.25s linear; 49 | -webkit-transition: background 0.25s linear; 50 | } 51 | 52 | [data-hook=code] { 53 | width: 50%; 54 | position: absolute; 55 | left: 50%; 56 | top: 0; 57 | height: 75%; 58 | } 59 | 60 | [data-hook=stack] { 61 | width: 25%; 62 | position: absolute; 63 | top: 0; 64 | left: 0%; 65 | height: 75%; 66 | border: 1px gray solid; 67 | } 68 | 69 | [data-hook=stack] ul { 70 | position: absolute; 71 | bottom: 0; 72 | list-style-type: none; 73 | padding: 0; 74 | width: 100%; 75 | margin: 0; 76 | } 77 | 78 | [data-hook=stack] ul li { 79 | padding: 10px; 80 | background: #F0E03F; 81 | margin: 10px; 82 | width: calc(100% - 20px); 83 | } 84 | 85 | [data-hook=timeouts] { 86 | width: 100%; 87 | position: absolute; 88 | top: 75%; 89 | left: 0; 90 | height: 12.5%; 91 | } 92 | 93 | [data-hook=timeouts]:before { 94 | content: "Timeouts:"; 95 | opacity: 0.5; 96 | padding: 5px; 97 | margin: 5px; 98 | background: gray; 99 | color: white; 100 | } 101 | 102 | [data-hook=stack]:before { 103 | content: "Stack"; 104 | opacity: 0.5; 105 | padding: 5px; 106 | margin: 5px; 107 | background: gray; 108 | color: white; 109 | } 110 | 111 | [data-hook=timeouts] li { 112 | background: coral; 113 | padding: 20px; 114 | margin: 5px; 115 | list-style-type: none; 116 | display: inline-block; 117 | } 118 | 119 | [data-hook=timeouts] li.started { 120 | background: lime; 121 | } 122 | 123 | [data-hook=timeouts] li.finished { 124 | background: lime; 125 | opacity: 0.25; 126 | } 127 | 128 | 129 | [data-hook=callbacks] { 130 | width: 100%; 131 | position: absolute; 132 | top: 87.5%; 133 | left: 0; 134 | height: 12.5%; 135 | } 136 | -------------------------------------------------------------------------------- /components/ace-editor.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var ace = require('brace'); 4 | require('brace/mode/javascript'); 5 | require('brace/mode/html'); 6 | require('brace/theme/solarized_light'); 7 | 8 | module.exports = React.createClass({ 9 | getDefaultProps: function () { 10 | return { 11 | mode: 'javascript', 12 | initialValue: '', 13 | onBlur: function () { }, 14 | onCodeChange: function (newCode) { 15 | console.log('Code changed to', newCode); 16 | } 17 | }; 18 | }, 19 | componentDidMount: function () { 20 | this.editor = ace.edit(this.getDOMNode()); 21 | this.editSession = this.editor.getSession(); 22 | 23 | this.editor.getSession().setMode('ace/mode/' + this.props.mode); 24 | this.editor.setTheme('ace/theme/solarized_light'); 25 | 26 | this.editor.focus(); 27 | this.editor.setValue(this.props.initialValue, -1); 28 | 29 | this.editor.on('blur', function () { 30 | this.props.onCodeChange(this.editor.getValue().split('\n')); 31 | this.props.onBlur(); 32 | }.bind(this)); 33 | }, 34 | 35 | componentWillUnmount: function () { 36 | this.editor.destroy(); 37 | }, 38 | 39 | render: function () { 40 | return ( 41 |
42 | ); 43 | } 44 | }); 45 | -------------------------------------------------------------------------------- /components/app.jsx: -------------------------------------------------------------------------------- 1 | /* JSX: React.DOM */ 2 | 3 | var React = require('react'); 4 | var CallStack = require('./call-stack.jsx'); 5 | var EventLoopSpinner = require('./event-loop-spinner.jsx'); 6 | var WebApis = require('./web-apis.jsx'); 7 | var Editor = require('./editor.jsx'); 8 | var CallbackQueue = require('./callback-queue.jsx'); 9 | var RenderQueue = require('./render-queue.jsx'); 10 | var HTMLEditor = require('./html-editor.jsx'); 11 | var SettingsPanel = require('./settings-panel.jsx'); 12 | var EventMixin = require('react-backbone-events-mixin'); 13 | var Modal = require('react-modal'); 14 | 15 | module.exports = React.createClass({ 16 | mixins: [EventMixin], 17 | 18 | getInitialState: function () { 19 | var showRenderQueue = window.location.search.match(/show-renders/); 20 | 21 | return { 22 | settingsOpen: false, 23 | code: app.store.code, 24 | modalOpen: true 25 | }; 26 | }, 27 | 28 | openModal: function () { 29 | this.setState({ modalOpen: true }); 30 | }, 31 | 32 | closeModal: function () { 33 | this.setState({ modalOpen: false }); 34 | }, 35 | 36 | registerListeners: function (props, state) { 37 | this.listenTo(state.code, 'change:simulateRenders', function () { 38 | this.forceUpdate(); 39 | }.bind(this)); 40 | }, 41 | 42 | toggleSettings: function () { 43 | this.setState({ 44 | settingsOpen: !this.state.settingsOpen 45 | }); 46 | }, 47 | render: function () { 48 | return ( 49 |
50 |
51 | 56 |
57 | 58 | 59 |
60 |
61 | 62 |
63 | 64 |
65 | 66 |
67 |
68 | 69 |
70 |
71 |
72 | 73 | 74 | 75 | 76 |
77 | 78 |
79 | 80 |
81 |
82 | 83 |
84 | { this.state.code.simulateRenders ? : null } 85 | 86 |
87 |
88 |
89 |
90 | 91 | 94 | 95 | close 96 | 97 |

Loupe

98 |

Intro

99 |

Loupe is a little visualisation to help you understand how JavaScript's call stack/event loop/callback queue interact with each other.

100 |

The best thing to do to understand how this works is watch this video, then when you are ready, go play!

101 | 102 |

Instructions

103 | 111 | 112 |

How does this work?

113 | 123 | 124 |

Built by Philip Roberts from &yet. Code is on github.

125 | 126 |

Got it? Close this dialog

127 |
128 |
129 | ) 130 | } 131 | }); 132 | -------------------------------------------------------------------------------- /components/call-stack-item.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | module.exports = React.createClass({ 4 | render: function () { 5 | var classes = "stack-item"; 6 | if (this.props.isCallback) { 7 | classes += " stack-item-callback"; 8 | } 9 | 10 | return ( 11 |
12 | {this.props.children} 13 |
14 | ); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /components/call-stack.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react/addons'); 2 | var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup; 3 | var CallStackItem = require('./call-stack-item.jsx'); 4 | var EventsMixin = require('react-backbone-events-mixin'); 5 | 6 | module.exports = React.createClass({ 7 | mixins: [ 8 | EventsMixin 9 | ], 10 | 11 | registerListeners: function (props, state) { 12 | var self = this; 13 | 14 | this.listenTo(state.stack, 'all', function () { 15 | self.forceUpdate(); 16 | }); 17 | 18 | }, 19 | 20 | getInitialState: function () { 21 | return { 22 | stack: window.app.store.callstack 23 | }; 24 | }, 25 | 26 | render: function () { 27 | var calls = []; 28 | 29 | this.state.stack.each(function (call) { 30 | calls.unshift({call.code}); 31 | }); 32 | 33 | return ( 34 |
35 |
36 | 37 | {calls} 38 | 39 |
40 |
41 | ); 42 | } 43 | }); 44 | -------------------------------------------------------------------------------- /components/callback-queue.jsx: -------------------------------------------------------------------------------- 1 | /* JSX: React.DOM */ 2 | 3 | var React = require('react/addons'); 4 | var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup; 5 | var Callback = require('./callback.jsx'); 6 | var EventsMixin = require('react-backbone-events-mixin'); 7 | 8 | module.exports = React.createClass({ 9 | mixins: [ 10 | EventsMixin 11 | ], 12 | getInitialState: function () { 13 | return { 14 | queue: app.store.queue 15 | }; 16 | }, 17 | registerListeners: function (props, state) { 18 | this.listenTo(state.queue, 'all', function () { 19 | this.forceUpdate(); 20 | }.bind(this)); 21 | }, 22 | render: function () { 23 | var queue = this.state.queue.map(function (callback) { 24 | return ( 25 | 26 | {callback.code} 27 | 28 | ); 29 | }); 30 | 31 | return ( 32 |
33 | 34 | {queue} 35 | 36 |
37 | ) 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /components/callback.jsx: -------------------------------------------------------------------------------- 1 | /* JSX: React.DOM */ 2 | 3 | var React = require('react'); 4 | 5 | module.exports = React.createClass({ 6 | render: function () { 7 | var classes = ["callback", "callback-" + this.props.state].join(' '); 8 | return ( 9 |
10 | {this.props.children} 11 |
12 | ); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /components/editor.jsx: -------------------------------------------------------------------------------- 1 | /* JSX: React.DOM */ 2 | 3 | var React = require('react'); 4 | var EventMixin = require('react-backbone-events-mixin'); 5 | var AceEditor = require('./ace-editor.jsx'); 6 | 7 | module.exports = React.createClass({ 8 | mixins: [ 9 | EventMixin 10 | ], 11 | 12 | registerListeners: function (props, state) { 13 | var self = this; 14 | 15 | this.listenTo(state.code, 'change', function () { 16 | this.forceUpdate(); 17 | }.bind(this)); 18 | 19 | this.listenTo(state.code, 'node:will-run', function (id) { 20 | var node = self.refs.code.getDOMNode().querySelector('#node-' + id); 21 | node.classList.add('running'); 22 | }); 23 | 24 | this.listenTo(state.code, 'node:did-run', function (id) { 25 | var node = self.refs.code.getDOMNode().querySelector('#node-' + id); 26 | node.classList.remove('running'); 27 | }); 28 | }, 29 | 30 | getInitialState: function () { 31 | return { 32 | code: app.store.code, 33 | editing: true 34 | }; 35 | }, 36 | 37 | onCodeChange: function (newCode) { 38 | this.state.code.codeLines = newCode; 39 | }, 40 | 41 | onEditBlur: function () { 42 | this.setState({ editing: false }); 43 | //var newCode = this.refs.code.getDOMNode().innerText; 44 | //this.state.code.html = newCode; 45 | //this.setState({ editing: false }); 46 | }, 47 | 48 | saveAndRunCode: function () { 49 | this.setState({ editing: false }); 50 | this.runCode(); 51 | }, 52 | 53 | runCode: function () { 54 | this.state.code.run(); 55 | }, 56 | 57 | pauseCode: function () { 58 | this.state.code.pause(); 59 | }, 60 | 61 | resumeCode: function () { 62 | this.state.code.resume(); 63 | }, 64 | 65 | onEditFocus: function () { 66 | this.state.code.resetEverything(); 67 | this.setState({ editing: true }); 68 | }, 69 | 70 | render: function () { 71 | if (this.state.editing) { 72 | return ( 73 |
74 |
75 | 76 |
77 | 83 |
84 | ); 85 | } else { 86 | var i = 0; 87 | var lines = this.state.code.codeLines.map(function () { i++; return i; }).join(String.fromCharCode(10)); 88 | 89 | return ( 90 |
91 |
92 | 93 | 94 | 95 | 96 |
97 |
104 |
105 | ); 106 | 107 | } 108 | 109 | //var innerHTML = this.state.editing ? this.state.code.html : this.state.code.wrappedHtml; 110 | 111 | //return ( 112 | //
120 | //); 121 | } 122 | }); 123 | -------------------------------------------------------------------------------- /components/event-loop-spinner.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var EventsMixin = require('react-backbone-events-mixin'); 3 | 4 | module.exports = React.createClass({ 5 | mixins: [ 6 | EventsMixin 7 | ], 8 | 9 | getInitialState: function () { 10 | return { 11 | code: app.store.code 12 | }; 13 | }, 14 | 15 | registerListeners: function (props, state) { 16 | this.listenTo(state.code, 'callback:shifted', function () { 17 | var domnode = this.refs.spinner.getDOMNode(); 18 | domnode.classList.add('spinner-wrapper-transition'); 19 | var onTransitionEnd = function () { 20 | domnode.classList.remove('spinner-wrapper-transition'); 21 | domnode.removeEventListener('transitionend', onTransitionEnd, false); 22 | }; 23 | domnode.addEventListener('transitionend', onTransitionEnd, false); 24 | }.bind(this)); 25 | }, 26 | 27 | render: function () { 28 | return ( 29 |
30 |
31 |
32 |
33 |
34 | ); 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /components/html-editor.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var AceEditor = require('./ace-editor.jsx'); 3 | var EventMixin = require('react-backbone-events-mixin'); 4 | 5 | module.exports = React.createClass({ 6 | mixins: [EventMixin], 7 | 8 | registerListeners: function (props, state) { 9 | this.listenTo(state.code, 'ready-to-run', function () { 10 | this.setState({ editing: false }); 11 | }); 12 | 13 | //this.listenTo(state.code, 'change:code', function () { 14 | // this.forceUpdate(); 15 | //}); 16 | }, 17 | 18 | getInitialState: function () { 19 | return { 20 | editing: false, 21 | code: app.store.code, 22 | }; 23 | }, 24 | 25 | switchMode: function () { 26 | var newValue = !this.state.editing; 27 | this.setState({ editing: newValue }); 28 | }, 29 | 30 | onCodeChange: function (newCode) { 31 | this.state.code.htmlScratchpad = newCode; 32 | }, 33 | 34 | render: function () { 35 | if (this.state.editing) { 36 | return ( 37 |
38 |
39 | 45 |
46 | ); 47 | }; 48 | 49 | var innerHTML = { __html: this.state.code.rawHtmlScratchpad }; 50 | 51 | return ( 52 |
53 |
54 |
55 |
56 | ); 57 | } 58 | }); 59 | -------------------------------------------------------------------------------- /components/render-queue.jsx: -------------------------------------------------------------------------------- 1 | /* JSX: React.DOM */ 2 | 3 | var React = require('react/addons'); 4 | var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup; 5 | var Callback = require('./callback.jsx'); 6 | var EventsMixin = require('react-backbone-events-mixin'); 7 | 8 | module.exports = React.createClass({ 9 | mixins: [ 10 | EventsMixin 11 | ], 12 | 13 | getInitialState: function () { 14 | return { 15 | queue: app.store.renderQueue 16 | }; 17 | }, 18 | 19 | registerListeners: function (props, state) { 20 | this.listenTo(state.queue, 'all', function () { 21 | this.forceUpdate(); 22 | }.bind(this)); 23 | }, 24 | 25 | render: function () { 26 | var queue = this.state.queue.map(function (callback) { 27 | return ( 28 | 29 | {callback.id} 30 | 31 | ); 32 | }); 33 | 34 | return ( 35 |
36 | {queue} 37 |
38 | ) 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /components/settings-panel.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var EventMixin = require('react-backbone-events-mixin'); 3 | 4 | module.exports = React.createClass({ 5 | mixins: [EventMixin], 6 | 7 | registerListeners: function (props, state) { 8 | this.listenTo(state.code, 'change:delay', function () { 9 | this.forceUpdate(); 10 | }.bind(this)); 11 | }, 12 | 13 | getInitialState: function () { 14 | return { 15 | code: app.store.code 16 | }; 17 | }, 18 | 19 | changeDelay: function () { 20 | this.state.code.delay = parseInt(this.refs.delay.getDOMNode().value); 21 | }, 22 | 23 | changeRenders: function () { 24 | app.store.code.simulateRenders = this.refs.renders.getDOMNode().checked; 25 | }, 26 | 27 | render: function () { 28 | var classes = "flexChild columnParent settingsColumn"; 29 | if (!this.props.open) { classes += " hidden"; } 30 | 31 | return ( 32 |
33 |
34 | 38 | 42 |
43 |
44 | ); 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /components/web-api-query.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | module.exports = React.createClass({ 4 | flash: function () { 5 | var el = this.getDOMNode(); 6 | el.classList.add('tr-webapi-spawn'); 7 | setTimeout(function () { 8 | el.classList.add('tr-webapi-spawn-active'); 9 | }, 16.6); 10 | 11 | var fallbackTimeout = setTimeout(function () { 12 | try { 13 | onTransitionOutEnd(); 14 | onTransitionInEnd(); 15 | } catch (e) { 16 | } 17 | }, 1000); 18 | 19 | var onTransitionOutEnd = function () { 20 | el.classList.remove('tr-webapi-spawn'); 21 | el.removeEventListener('transitionend', onTransitionOutEnd, false); 22 | }; 23 | 24 | var onTransitionInEnd = function () { 25 | el.classList.remove('tr-webapi-spawn-active'); 26 | el.removeEventListener('transitionend', onTransitionInEnd, false); 27 | el.addEventListener('transitionend', onTransitionOutEnd, false); 28 | }; 29 | 30 | el.addEventListener('transitionend', onTransitionInEnd, false); 31 | }, 32 | 33 | render: function () { 34 | return ( 35 |
36 |
37 | {this.props.children} 38 |
39 |
40 | ); 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /components/web-api-timer.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | module.exports = React.createClass({ 4 | render: function () { 5 | var animStyle = { 6 | animationDuration: this.props.timeout, 7 | WebkitAnimationDuration: this.props.timeout, 8 | animationPlayState: this.props.playState, 9 | WebkitAnimationPlayState: this.props.playState 10 | }; 11 | 12 | return ( 13 |
14 |
15 | {this.props.children} 16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | ); 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /components/web-apis.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react/addons'); 2 | var WebApiTimer = require('./web-api-timer.jsx'); 3 | var WebApiQuery = require('./web-api-query.jsx'); 4 | var EventMixin = require('react-backbone-events-mixin'); 5 | var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup; 6 | 7 | module.exports = React.createClass({ 8 | mixins: [ 9 | EventMixin 10 | ], 11 | 12 | registerListeners: function (props, state) { 13 | this.listenTo(state.apis, 'all', function () { 14 | this.forceUpdate(); 15 | }.bind(this)); 16 | 17 | this.listenTo(state.apis, 'callback:spawned', function (model) { 18 | this.refs[model.id].flash(); 19 | }.bind(this)); 20 | }, 21 | 22 | getInitialState: function () { 23 | return { 24 | apis: app.store.apis 25 | }; 26 | }, 27 | 28 | render: function () { 29 | var apis = this.state.apis.map(function (api) { 30 | if (api.type === 'timeout') { 31 | return {api.code}; 32 | } 33 | if (api.type === 'query') { 34 | return ( 35 | 36 | {api.code} 37 | 38 | ); 39 | } 40 | }); 41 | 42 | return ( 43 |
44 | 45 | {apis} 46 | 47 |
48 | ) 49 | } 50 | }); 51 | -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/delay.js: -------------------------------------------------------------------------------- 1 | module.exports = function (delay, fromId) { 2 | var microDelay = 15; 3 | var id = 0; 4 | if (fromId) { loupe.skipDelays = true; } 5 | var nMicroDelays = delay / microDelay; 6 | 7 | return function () { 8 | id++; 9 | var microId = 0; 10 | var start, target; 11 | var countdown = Math.floor(nMicroDelays); 12 | 13 | //var wasSkipping = loupe.skipDelays; 14 | //var startedAt = new Date().getTime(); 15 | while (countdown--) { 16 | weevil.send('delay', id + microId); 17 | loupe.triggerDelay(id + microId); 18 | 19 | if (!fromId || (id + microId) > fromId) { 20 | loupe.skipDelays = false; 21 | 22 | start = new Date().getTime(); 23 | target = start + microDelay; 24 | while (new Date().getTime() < target) { 25 | //no-op 26 | } 27 | } 28 | 29 | microId += 0.000001; 30 | } 31 | //var actual = new Date().getTime() - startedAt; 32 | //if (loupe.skipDelays) { 33 | // console.log(['Skipped', id, 'in', actual]); 34 | //} else { 35 | // if (wasSkipping) { 36 | // console.log([id, 'Partial!', delay, 'actual', actual, 'error', actual - delay]); 37 | // } else { 38 | // console.log([id, 'Target', delay, 'actual', actual, 'error', actual - delay]); 39 | // } 40 | //} 41 | }; 42 | }; 43 | -------------------------------------------------------------------------------- /lib/instrument-code.js: -------------------------------------------------------------------------------- 1 | var falafel = require('falafel'); 2 | 3 | var call = function (name) { 4 | return function (arg) { 5 | return arg[name](); 6 | }; 7 | }; 8 | 9 | var instruments = { 10 | ExpressionStatement: function (id, node, before, after) { 11 | node.update(node.source() + ';'); 12 | }, 13 | CallExpression: function (id, node, before, after) { 14 | var source = node.source(); 15 | 16 | if (node.callee.source() === 'console.log') { 17 | source = "_console.log(" + node.loc.start.line + ", " + node.arguments.map(call('source')).join(', ') + ")"; 18 | } 19 | 20 | var newString = '(' + before(id, node) + ', ' + source + ', ' + after(id, node) + ')\n'; 21 | node.update(newString); 22 | }, 23 | BinaryExpression: function (id, node, before, after) { 24 | var newString = '(' + before(id, node) + ', ' + after(id, node) + ', ' + node.source() + ')'; 25 | node.update(newString); 26 | }, 27 | //MemberExpression: function (id, node, before, after) { 28 | // var source = node.source(); 29 | // if (source === 'console.log') { 30 | // source = '_console.log'; 31 | // } 32 | 33 | // node.update(source); 34 | //} 35 | }; 36 | 37 | var isInstrumentable = function (node) { 38 | return !!instruments[node.type]; 39 | }; 40 | 41 | var instrumentNode = function (id, node, before, after) { 42 | instruments[node.type](id, node, before, after); 43 | }; 44 | 45 | 46 | module.exports = function (code, options) { 47 | var before = options.before || function () { return ''; }; 48 | var after = options.after || function () { return ''; }; 49 | 50 | var insertionPoints = []; 51 | var id = 1; 52 | var instrumented = falafel(code, { loc: true, range: true }, function (node) { 53 | if (!isInstrumentable(node)) return; 54 | insertionPoints.push({ 55 | id: id, 56 | type: 'start', 57 | loc: node.loc.start 58 | }); 59 | insertionPoints.push({ 60 | id: id, 61 | type: 'end', 62 | loc: node.loc.end 63 | }); 64 | instrumentNode(id, node, before, after); 65 | id++; 66 | }); 67 | 68 | insertionPoints.sort(function (a, b) { 69 | if (a.loc.line === b.loc.line) return a.loc.column - b.loc.column; 70 | return a.loc.line - b.loc.line; 71 | }); 72 | 73 | return { 74 | code: instrumented, 75 | insertionPoints: insertionPoints 76 | }; 77 | }; 78 | 79 | -------------------------------------------------------------------------------- /lib/plugins/console.js: -------------------------------------------------------------------------------- 1 | var deval = require('deval'); 2 | 3 | module.exports.prependWorkerCode = function (code) { 4 | return this.server + ';\n' + code; 5 | }; 6 | 7 | module.exports.server = deval(function () { 8 | var _console = {}; 9 | 10 | _console.log = function () { 11 | if (loupe.paused) return; 12 | 13 | weevil.send('console:log', [].slice.call(arguments)); 14 | }; 15 | }); 16 | 17 | module.exports.createClient = function (codeModel, emitter) { 18 | emitter.on('console:log', function (args) { 19 | args.unshift('color: coral; font-weight: bold;'); 20 | args.unshift('%c Loupe @ %d >'); 21 | console.log.apply(console, args); 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /lib/plugins/query.js: -------------------------------------------------------------------------------- 1 | var deval = require('deval'); 2 | 3 | module.exports.prependWorkerCode = function(code) { 4 | return this.server + ';\n' + code; 5 | }; 6 | 7 | module.exports.server = deval(function () { 8 | var $ = { 9 | _callbacks: {}, 10 | replayCallbacks: function (cbs) { 11 | var self = this; 12 | 13 | Object.keys(cbs).forEach(function (delayId) { 14 | var args = cbs[delayId]; 15 | loupe.onDelay(delayId, function () { 16 | self._callbacks[args[0]](args[1]); 17 | }); 18 | }); 19 | }, 20 | register: function (emitter) { 21 | this.emitter = emitter; 22 | this.emitter.on('query:event', function (id, callbackId) { 23 | this._callbacks[id](callbackId); 24 | }.bind(this)); 25 | }, 26 | 27 | on: function (selector, event, cb) { 28 | var self = this; 29 | 30 | var id = 'query:' + this.nextId(); 31 | var callbackName = (cb.name || "anonymous") + "()"; 32 | this.emitter.send('query:addEventListener', { selector: selector, event: event, id: id, source: callbackName }); 33 | 34 | this._callbacks[id] = function (callbackId) { 35 | delay(); 36 | self.emitter.send('callback:shifted', callbackId); 37 | delay(); 38 | cb(); 39 | self.emitter.send('callback:completed', callbackId); 40 | }; 41 | }, 42 | 43 | nextId: function () { 44 | if (!this._id) this._id = 0; 45 | this._id++; 46 | return this._id; 47 | } 48 | }; 49 | $.register(weevil); 50 | $.replayCallbacks(loupe.appState.query); 51 | }); 52 | 53 | module.exports.createClient = function (codeModel, emitter, document) { 54 | var listeners = [ 55 | ]; 56 | 57 | var historyLog = { 58 | }; 59 | 60 | window.historyLog = historyLog; 61 | 62 | codeModel.on('ready-to-run', function () { 63 | //historyLog = {}; 64 | }); 65 | 66 | emitter.on('query:addEventListener', function (data) { 67 | var els; 68 | 69 | codeModel.trigger('webapi:started', { 70 | id: data.id, 71 | type: 'query', 72 | selector: data.selector, 73 | event: data.event 74 | }); 75 | 76 | //var els = document.querySelectorAll(data.selector); 77 | if (data.selector === 'document') { 78 | els = [document]; 79 | } else { 80 | els = document.querySelectorAll(data.selector); 81 | } 82 | 83 | [].forEach.call(els, function (el) { 84 | var cb = function () { 85 | 86 | var callbackId = data.id + ":" + Date.now(); 87 | codeModel.trigger('callback:spawn', { 88 | id: callbackId, 89 | apiId: data.id, 90 | code: "[" + data.event + "] " + data.source 91 | }); 92 | 93 | historyLog[codeModel.currentExecution] = [data.id, callbackId]; 94 | 95 | emitter.send('query:event', data.id, callbackId); 96 | }; 97 | 98 | listeners.push([el, data.event, cb, false]); 99 | el.addEventListener(data.event, cb, false); 100 | }); 101 | }); 102 | 103 | codeModel.on('reset-everything', function () { 104 | listeners.forEach(function (listener) { 105 | var el = listener.shift(); 106 | el.removeEventListener.apply(el, listener); 107 | }); 108 | listeners = []; 109 | }); 110 | 111 | return { 112 | historyLog: historyLog 113 | }; 114 | }; 115 | -------------------------------------------------------------------------------- /lib/tag.js: -------------------------------------------------------------------------------- 1 | module.exports.o = function open (tagName, attrs) { 2 | attrs = attrs || {}; 3 | 4 | attrs = Object.keys(attrs).map(function (attr) { 5 | return attr + '="' + attrs[attr] + '"'; 6 | }).join(' '); 7 | return "<" + tagName + " " + attrs + ">"; 8 | }; 9 | 10 | module.exports.c = function close (tagName) { 11 | return ""; 12 | }; 13 | 14 | module.exports.w = function wrap (tagName, str, attrs) { 15 | return open(tagName, attrs) + str + close(tagName); 16 | }; 17 | -------------------------------------------------------------------------------- /lib/text-cursor.js: -------------------------------------------------------------------------------- 1 | module.exports = TextCursor; 2 | 3 | function TextCursor (code) { 4 | this.lines = [null].concat(code.split('\n')); 5 | 6 | this.cursor = { 7 | line: 1, 8 | column: 0 9 | }; 10 | } 11 | 12 | TextCursor.prototype.stepTo = function (line, column) { 13 | if (line === 'end') { 14 | line = this.lines.length - 1; 15 | } 16 | if (!column && typeof line === 'object') { 17 | column = line.column; 18 | line = line.line; 19 | } 20 | var output = ''; 21 | var currentSlice; 22 | 23 | //If we're going to a different line, grab the remainder of all 24 | //preceeding lines 25 | while (this.cursor.line < line) { 26 | currentSlice = this.lines[this.cursor.line].slice(this.cursor.column); 27 | //if (currentSlice.length) output += currentSlice + '\n'; 28 | output += currentSlice + '\n'; 29 | this.cursor.line++; 30 | this.cursor.column = 0; 31 | } 32 | 33 | //Grab from where we are on the line we're jumping to, to the desired 34 | //column 35 | currentSlice = this.lines[this.cursor.line].slice(this.cursor.column, column); 36 | if (currentSlice.length) output += currentSlice; 37 | 38 | // slice = lines[cursor.line].slice(cursor.column, pos.column); 39 | // if (slice.length) output += slice; 40 | if (this.lines[this.cursor.line].length <= column) output += '\n'; 41 | this.cursor.column = column; 42 | return output; 43 | }; 44 | 45 | //var cursor = { 46 | // line: 1, 47 | // column: 0 48 | //}; 49 | // 50 | //var piecesToPosition = function (pos) { 51 | // var output = ''; 52 | // var slice; 53 | // while(cursor.line < pos.line) { 54 | // slice = lines[cursor.line].slice(cursor.column); 55 | // if (slice.length) output += slice + '\n'; 56 | // cursor.line++; 57 | // cursor.column = 0; 58 | // } 59 | // slice = lines[cursor.line].slice(cursor.column, pos.column); 60 | // if (slice.length) output += slice; 61 | // if (lines[cursor.line].length === pos.column) output += '\n'; 62 | // cursor.column = pos.column; 63 | // return output; 64 | //}; 65 | -------------------------------------------------------------------------------- /lib/wrap-insertion-points.js: -------------------------------------------------------------------------------- 1 | var TextCursor = require('./text-cursor'); 2 | 3 | module.exports = function (code, insertionPoints, options) { 4 | var before = options.before || function () { return ''; }; 5 | var after = options.after || function () { return ''; }; 6 | var withWrappedCode = options.withWrappedCode || function () { return ''; }; 7 | 8 | var cursor = new TextCursor(code); 9 | var output = ''; 10 | 11 | var currentNodeContents = { 12 | }; 13 | 14 | insertionPoints.forEach(function (point) { 15 | var wrappedCode = cursor.stepTo(point.loc); 16 | output += wrappedCode; 17 | 18 | Object.keys(currentNodeContents).forEach(function (id) { 19 | currentNodeContents[id] += wrappedCode; 20 | }); 21 | 22 | if (point.type === 'start') { 23 | output += before(point.id); 24 | currentNodeContents[point.id] = ''; 25 | } 26 | 27 | if (point.type === 'end') { 28 | withWrappedCode(point.id, currentNodeContents[point.id]); 29 | delete currentNodeContents[point.id]; 30 | 31 | output += after(point.id); 32 | } 33 | }); 34 | output += cursor.stepTo('end'); 35 | return output; 36 | }; 37 | 38 | -------------------------------------------------------------------------------- /loupe.bundle.css: -------------------------------------------------------------------------------- 1 | *, *:before, *:after { 2 | box-sizing: border-box; 3 | } 4 | 5 | /*#flexcanvas{ 6 | width: 100%; 7 | height: 600px !important; 8 | }*/ 9 | html, body, .flexContainer { 10 | min-height: 100vh; 11 | font-family: Helvetica Neue, Helvetica, Arial, sans-serif; 12 | } 13 | body { 14 | padding: 0; 15 | margin: 0; 16 | } 17 | h1,h2,h3,h4 { 18 | font-family: Din Alternate, Helvetica Neue, Helvetica, Arial, sans-serif; 19 | color: #666; 20 | } 21 | 22 | .flexContainer { 23 | width: 100%; 24 | height: 100vh; 25 | } 26 | 27 | .flexContainer > * { 28 | height: 100%; 29 | padding-top: 32px; 30 | } 31 | 32 | .rowParent, .columnParent{ 33 | display: -webkit-box; 34 | display: -ms-flexbox; 35 | display: -webkit-flex; 36 | display: flex; 37 | -webkit-box-direction: normal; 38 | -webkit-box-orient: horizontal; 39 | -webkit-flex-direction: row; 40 | -ms-flex-direction: row; 41 | flex-direction: row; 42 | -webkit-flex-wrap: nowrap; 43 | -ms-flex-wrap: nowrap; 44 | flex-wrap: nowrap; 45 | -webkit-box-pack: start; 46 | -webkit-justify-content: flex-start; 47 | -ms-flex-pack: start; 48 | justify-content: flex-start; 49 | -webkit-align-content: stretch; 50 | -ms-flex-line-pack: stretch; 51 | align-content: stretch; 52 | -webkit-box-align: stretch; 53 | -webkit-align-items: stretch; 54 | -ms-flex-align: stretch; 55 | align-items: stretch; 56 | } 57 | 58 | .columnParent{ 59 | -webkit-box-orient: vertical; 60 | -webkit-flex-direction: column; 61 | -ms-flex-direction: column; 62 | flex-direction: column; 63 | } 64 | 65 | .flexChild{ 66 | -webkit-box-flex: 1; 67 | -webkit-flex: 1; 68 | -ms-flex: 1; 69 | flex: 1; 70 | -webkit-align-self: auto; 71 | -ms-flex-item-align: auto; 72 | align-self: auto; 73 | } 74 | 75 | .editorBox, .stackRow { 76 | -webkit-box-flex: 3; 77 | -webkit-flex: 3; 78 | -ms-flex: 3; 79 | flex: 3; 80 | position: relative; 81 | } 82 | 83 | .editorBox { 84 | background: #FDF6E3; 85 | } 86 | 87 | .codeColumn { 88 | -webkit-box-flex: 0.5; 89 | -webkit-flex: 0.5; 90 | -ms-flex: 0.5; 91 | flex: 0.5; 92 | margin-right: 20px; 93 | border-right: 2px #ddd solid; 94 | } 95 | 96 | .editor { 97 | min-height: 100%; 98 | width: 100%; 99 | font-family: monospace; 100 | font-size: 13px; 101 | line-height: 1.75; 102 | display: inline-block; 103 | white-space: pre; 104 | padding-left: 45px; 105 | color: #333; 106 | font-family: Monaco, Menlo, 'Ubuntu Mono', Consolas, source-code-pro, monospace; 107 | font-size: 12px; 108 | overflow-y: hidden; 109 | position: relative; 110 | } 111 | 112 | .editor:before { 113 | content: attr(data-lines); 114 | /*content: "1\a 2\A 3\A 4\A 5\A 6\A 7\A 8\A 9\A 10\A 11\A 12\A 13\A 14\A 15\A 16\A 17\A";*/ 115 | position: absolute; 116 | top: 0; 117 | left: 0; 118 | width: 41px; 119 | padding-left: 7.5px; 120 | text-align: center; 121 | background: #FBF1D3; 122 | height: 100%; 123 | white-space: pre; 124 | } 125 | 126 | .ace-editor-wrapper { 127 | -webkit-box-flex: 1; 128 | -webkit-flex: 1; 129 | -ms-flex: 1; 130 | flex: 1; 131 | } 132 | 133 | .ace_editor { 134 | line-height: 1.75!important; 135 | } 136 | 137 | .html-scratchpad { 138 | padding: 10px; 139 | background: #eee; 140 | overflow: scroll; 141 | font-family: Helvetica Neue, Helvetica, Arial, sans-serif; 142 | } 143 | 144 | .webapi { 145 | padding: 5px; 146 | border: 2px #ddd solid; 147 | margin: 5px; 148 | } 149 | 150 | .webapi-code { 151 | font-family: monospace; 152 | height: 100%; 153 | vertical-align: middle; 154 | margin-right: 5px; 155 | } 156 | 157 | 158 | .webapi-code:before { 159 | content: ''; 160 | display: inline-block; 161 | vertical-align: middle; 162 | height: 100%; 163 | } 164 | 165 | .webapi-timer > * { 166 | display: inline-block; 167 | vertical-align: middle; 168 | height: 44px; 169 | } 170 | 171 | 172 | @-webkit-keyframes rota { 173 | 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } 174 | 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } 175 | } 176 | 177 | 178 | @keyframes rota { 179 | 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } 180 | 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } 181 | } 182 | 183 | @-webkit-keyframes fill { 184 | 0% { opacity: 0; } 185 | 50%, 100% { opacity: 1; } 186 | } 187 | 188 | @keyframes fill { 189 | 0% { opacity: 0; } 190 | 50%, 100% { opacity: 1; } 191 | } 192 | 193 | @-webkit-keyframes mask { 194 | 0% { opacity: 1; } 195 | 50%, 100% { opacity: 0; } 196 | } 197 | 198 | @keyframes mask { 199 | 0% { opacity: 1; } 200 | 50%, 100% { opacity: 0; } 201 | } 202 | 203 | @-webkit-keyframes check { 204 | 0% { opacity: 0; } 205 | 98% { opacity: 0; } 206 | 100% { opacity: 1; } 207 | } 208 | 209 | @keyframes check { 210 | 0% { opacity: 0; } 211 | 98% { opacity: 0; } 212 | 100% { opacity: 1; } 213 | } 214 | 215 | @-webkit-keyframes background { 216 | 0% { background: rgba(148,255,148,0); } 217 | 95% { background: rgba(148,255,148,0); } 218 | 100% { background: rgba(148,255,148,1); } 219 | } 220 | 221 | @keyframes background { 222 | 0% { background: rgba(148,255,148,0); } 223 | 95% { background: rgba(148,255,148,0); } 224 | 100% { background: rgba(148,255,148,1); } 225 | } 226 | 227 | .stopwatch-wrapper { 228 | width: 44px; 229 | height: 44px; 230 | position: relative; 231 | background: rgba(148,255,148,1); 232 | border-radius: 250px; 233 | -webkit-animation: background 10s linear; 234 | animation: background 10s linear; 235 | overflow: hidden; 236 | } 237 | 238 | .stopwatch-wrapper:before { 239 | position: absolute; 240 | content: ""; 241 | width: 50%; 242 | height: 28%; 243 | top: 35%; 244 | left: 25.5%; 245 | border-left: 7px white solid; 246 | border-bottom: 7px white solid; 247 | -webkit-transform: rotate(-45deg); 248 | -ms-transform: rotate(-45deg); 249 | transform: rotate(-45deg); 250 | z-index: 500; 251 | } 252 | 253 | .stopwatch-pie { 254 | border-radius: 22px; 255 | width: 50%; 256 | height: 100%; 257 | position: absolute; 258 | border: 8px solid rgb(148,255,148); 259 | } 260 | 261 | .stopwatch-spinner { 262 | border-top-right-radius: 0; 263 | border-bottom-right-radius: 0; 264 | z-index: 200; 265 | border-right: none; 266 | -webkit-animation: rota 10s linear; 267 | animation: rota 10s linear; 268 | -webkit-transform-origin: 100% 50%; 269 | -ms-transform-origin: 100% 50%; 270 | transform-origin: 100% 50%; 271 | } 272 | 273 | .stopwatch-filler { 274 | border-top-left-radius: 0; 275 | border-bottom-left-radius: 0; 276 | z-index: 100; 277 | border-left: none; 278 | -webkit-animation: fill 10s steps(1, end); 279 | animation: fill 10s steps(1, end); 280 | left: 50%; 281 | opacity: 1; 282 | } 283 | 284 | .stopwatch-mask { 285 | width: 50%; 286 | height: 100%; 287 | position: absolute; 288 | z-index: 300; 289 | opacity: 0; 290 | background: white; 291 | -webkit-animation: mask 10s steps(1, end); 292 | animation: mask 10s steps(1, end); 293 | border-radius: 250px 0 0 250px; 294 | } 295 | 296 | .stopwatch-spinner, .stopwatch-filler, .stopwatch-mask, .stopwatch-wrapper { 297 | -webkit-animation-duration: 3s; 298 | animation-duration: 3s; 299 | } 300 | 301 | 302 | .callback-queue { 303 | margin-bottom: 20px; 304 | margin-right: 20px; 305 | overflow-y: scroll; 306 | } 307 | .callback-queue.render-queue { 308 | margin-bottom: 5px; 309 | } 310 | 311 | .callback { 312 | font-family: monospace; 313 | padding: 10px; 314 | display: inline-block; 315 | margin-right: 10px; 316 | border: 2px #ddd solid; 317 | width: 200px; 318 | overflow: scroll; 319 | } 320 | 321 | .callback-active { 322 | background: rgb(220,224,255); 323 | } 324 | 325 | .stackBox { 326 | position: relative; 327 | margin-right: 20px; 328 | } 329 | 330 | .stack-wrapper { 331 | width: 244px; 332 | padding: 20px; 333 | margin-bottom: 86px; 334 | margin-top: 20px; 335 | } 336 | 337 | .stack-wrapper, .callback-queue, .webapis { 338 | border: 2px #ddd dashed; 339 | position: relative; 340 | padding: 25px 10px 10px 10px; 341 | } 342 | 343 | .stack-wrapper:before, 344 | .callback-queue:before, 345 | .webapis:before { 346 | font-family: Helvetica Neue, Arial, sans-serif; 347 | position: absolute; 348 | top: 0; 349 | left: 0; 350 | padding: 5px; 351 | color: #999; 352 | } 353 | 354 | .webapis { 355 | margin-top: 20px; 356 | margin-bottom: 86px; 357 | margin-right: 20px; 358 | } 359 | 360 | .webapis:before { 361 | content: "Web Apis"; 362 | } 363 | 364 | .stack-wrapper:before { 365 | content: "Call Stack"; 366 | } 367 | 368 | .callback-queue:before { 369 | content: "Callback Queue"; 370 | } 371 | 372 | .callback-queue.render-queue:before { 373 | content: "Render Queue"; 374 | } 375 | 376 | .stack { 377 | position: absolute; 378 | bottom: 0px; 379 | left: 0; 380 | padding-left: 10px; 381 | } 382 | .stackBox > .spinner-wrapper { 383 | position: absolute; 384 | bottom: 0; 385 | left: 0; 386 | margin-bottom: 5px; 387 | } 388 | 389 | .stack-item { 390 | font-family: monospace; 391 | padding: 10px; 392 | margin: 5px; 393 | border: 2px #ddd solid; 394 | width: 220px; 395 | margin: 10px auto; 396 | } 397 | 398 | 399 | .spinner-wrapper { 400 | height: 76px; 401 | width: 76px; 402 | position: relative; 403 | padding: 9px; 404 | -webkit-transform: rotate(0deg); 405 | -ms-transform: rotate(0deg); 406 | transform: rotate(0deg); 407 | margin-left: 82px; 408 | } 409 | 410 | .spinner-wrapper-transition { 411 | -webkit-transform: rotate(180deg); 412 | -ms-transform: rotate(180deg); 413 | transform: rotate(180deg); 414 | -webkit-transition: -webkit-transform 0.5s linear; 415 | transition: transform 0.5s linear; 416 | } 417 | 418 | .spinner-circle { 419 | height: 100%; 420 | width: 100%; 421 | border-radius: 30px; 422 | border: 6px coral solid; 423 | } 424 | 425 | .spinner-arrow { 426 | width: 0px; 427 | height: 0px; 428 | border-style: solid; 429 | border-width: 0 16px 16px 16px; 430 | border-color: transparent transparent white transparent; 431 | position: absolute; 432 | } 433 | 434 | .spinner-arrow:after { 435 | content: ""; 436 | width: 0px; 437 | height: 0px; 438 | border-style: solid; 439 | border-width: 0 12px 12px 12px; 440 | border-color: transparent transparent coral transparent; 441 | position: absolute; 442 | } 443 | 444 | .spinner-arrow-left:after { 445 | left: -12px; 446 | top: 4px; 447 | } 448 | 449 | .spinner-arrow-left { 450 | left: -2.5px; 451 | top: calc(50% - 9px); 452 | } 453 | 454 | .spinner-arrow-right { 455 | right: -2px; 456 | top: calc(50% - 6px); 457 | border-width: 16px 16px 0px 16px; 458 | border-color: white transparent transparent transparent; 459 | } 460 | 461 | .spinner-arrow-right:after { 462 | border-width: 12px 12px 0px 12px; 463 | border-color: coral transparent transparent transparent; 464 | 465 | left: -12px; 466 | bottom: 4px; 467 | } 468 | 469 | .code-node { 470 | } 471 | 472 | .code-node:empty { 473 | display: none; 474 | } 475 | 476 | .code-node.running { 477 | background: rgba(236, 117, 74, 0.25); 478 | } 479 | 480 | 481 | /*********************************/ 482 | .tr-webapis-enter { 483 | background: rgb(255,255,194); 484 | -webkit-transition: background 0.2s linear; 485 | transition: background 0.2s linear; 486 | } 487 | 488 | .tr-webapis-enter.tr-webapis-enter-active { 489 | background: white; 490 | } 491 | 492 | .tr-webapis-leave { 493 | background: white; 494 | -webkit-transition: background 0.01s linear; 495 | transition: background 0.01s linear; 496 | } 497 | 498 | .tr-webapis-leave.tr-webapis-leave-active { 499 | background: white; 500 | } 501 | 502 | .tr-webapi-spawn { 503 | background: white; 504 | -webkit-transition: background 0.1s linear; 505 | transition: background 0.1s linear; 506 | } 507 | 508 | .tr-webapi-spawn.tr-webapi-spawn-active { 509 | background: rgb(255, 175, 146); 510 | } 511 | 512 | /*********************************/ 513 | .tr-queue-enter { 514 | background: rgb(255,255,194); 515 | -webkit-transition: background 0.2s linear; 516 | transition: background 0.2s linear; 517 | } 518 | 519 | .tr-queue-enter.tr-queue-enter-active { 520 | background: white; 521 | } 522 | 523 | .tr-queue-leave { 524 | -webkit-transform: translate(0, 0); 525 | -ms-transform: translate(0, 0); 526 | transform: translate(0, 0); 527 | opacity: 1; 528 | -webkit-transition: all 0.25s linear; 529 | transition: all 0.25s linear; 530 | } 531 | 532 | .tr-queue-leave.tr-queue-leave-active { 533 | -webkit-transform: translate(0, -100px); 534 | -ms-transform: translate(0, -100px); 535 | transform: translate(0, -100px); 536 | opacity: 0.01; 537 | } 538 | 539 | 540 | /*********************************/ 541 | .tr-stack-enter { 542 | -webkit-transform: translate(0, 0); 543 | -ms-transform: translate(0, 0); 544 | transform: translate(0, 0); 545 | opacity: 0.01; 546 | -webkit-transition: all 0.1s linear; 547 | transition: all 0.1s linear; 548 | } 549 | 550 | .tr-stack-enter.stack-item-callback { 551 | -webkit-transform: translate(0, 100px); 552 | -ms-transform: translate(0, 100px); 553 | transform: translate(0, 100px); 554 | -webkit-transition: all 0.25s linear; 555 | transition: all 0.25s linear; 556 | } 557 | 558 | .tr-stack-enter.tr-stack-enter-active { 559 | -webkit-transform: translate(0, 0); 560 | -ms-transform: translate(0, 0); 561 | transform: translate(0, 0); 562 | opacity: 1; 563 | } 564 | 565 | .tr-stack-leave { 566 | opacity: 1; 567 | -webkit-transition: all 0.1s linear; 568 | transition: all 0.1s linear; 569 | } 570 | 571 | .tr-stack-leave.tr-stack-leave-active { 572 | opacity: 0.01; 573 | -webkit-transition: all 0.1s linear; 574 | transition: all 0.1s linear; 575 | } 576 | 577 | .htmlEditorBox { 578 | position: relative; 579 | } 580 | 581 | .editor-switch { 582 | position: absolute; 583 | top: 5px; 584 | right: 5px; 585 | z-index: 9999; 586 | } 587 | 588 | .render-queue .callback-queued { 589 | background: white; 590 | } 591 | 592 | .render-queue .callback-delayed { 593 | background: rgba(255,0,0,0.5); 594 | -webkit-transition: background 0.2s linear; 595 | transition: background 0.2s linear; 596 | } 597 | 598 | .render-queue .callback-rendered { 599 | background: rgba(0,255,0,0.5); 600 | -webkit-transition: background 0.4s linear; 601 | transition: background 0.4s linear; 602 | } 603 | 604 | 605 | /* 606 | .tr-render-queue-enter { 607 | background: white; 608 | transition: background 0.5s linear; 609 | } 610 | 611 | .tr-render-queue-enter.tr-render-queue-enter-active { 612 | background: red; 613 | } 614 | 615 | .tr-render-queue-leave { 616 | background: green; 617 | opacity: 1; 618 | transition: all 0.2s linear; 619 | } 620 | 621 | .tr-render-queue-leave.tr-render-queue-leave-active { 622 | background: white; 623 | opacity: 0.01; 624 | transition: all 0.2s linear; 625 | }*/ 626 | /************************/ 627 | 628 | .top-nav { 629 | background: #D80; 630 | padding: 5px; 631 | height: 32px; 632 | position: absolute; 633 | width: 100%; 634 | color: #eee; 635 | padding-left: 10px; 636 | } 637 | 638 | .top-nav h1 { 639 | font-size: 20px; 640 | display: inline; 641 | position: relative; 642 | top: -2px; 643 | color: #eee; 644 | } 645 | 646 | .top-nav .settings-button { 647 | border: none; 648 | background: none; 649 | color: #eee; 650 | font-size: 30px; 651 | line-height: 30px; 652 | margin: 0; 653 | padding: 0; 654 | margin-right: 10px; 655 | margin-top: -7px; 656 | } 657 | 658 | .settingsColumn { 659 | box-sizing: border-box; 660 | -webkit-box-flex: 0; 661 | -webkit-flex: none; 662 | -ms-flex: none; 663 | flex: none; 664 | width: 200px; 665 | -webkit-transition: width 0.15s linear; 666 | transition: width 0.15s linear; 667 | font-family: Helvetica Neue, Helvetica, Arial, sans-serif; 668 | background: #ddd; 669 | color: #666; 670 | } 671 | 672 | .settingsColumn .setting { 673 | margin: 20px; 674 | } 675 | 676 | .settingsColumn.hidden { 677 | width: 0; 678 | overflow: hidden; 679 | } 680 | 681 | .ReactModal__Overlay { 682 | z-index: 20000; 683 | background: rgba(0,0,0,0.4); 684 | } 685 | 686 | .ReactModal__Content { 687 | left: 20%; 688 | right: 20%; 689 | border: none; 690 | font-size: 14px; 691 | } 692 | 693 | .ReactModal__Content * { 694 | color: #888; 695 | } 696 | 697 | .ReactModal__Content h1 { 698 | margin-top: 0; 699 | } 700 | 701 | .ReactModal__Content h1 + h2 { 702 | margin-top: 20px; 703 | } 704 | 705 | .ReactModal__Content h2 { 706 | margin-top: 40px; 707 | margin-bottom: 10px; 708 | } 709 | 710 | .ReactModal__Content a { 711 | color: #DF8900; 712 | } 713 | 714 | .ReactModal__Content li { 715 | margin-bottom: 5px; 716 | } 717 | 718 | .ReactModal__Content iframe { 719 | margin: 40px auto; 720 | max-width: 100%; 721 | display: block; 722 | border: none; 723 | } 724 | 725 | 726 | 727 | .modal-button { 728 | float: right; 729 | } 730 | 731 | .modal-button:hover { 732 | cursor: pointer; 733 | text-decoration: underline; 734 | } 735 | 736 | p.info { 737 | text-align: center; 738 | margin-top: 40px; 739 | } 740 | 741 | a.modalClose { 742 | position: absolute; 743 | top: 15px; 744 | right: 15px; 745 | } 746 | a:hover { 747 | cursor: pointer; 748 | } 749 | -------------------------------------------------------------------------------- /loupe.css: -------------------------------------------------------------------------------- 1 | *, *:before, *:after { 2 | box-sizing: border-box; 3 | } 4 | 5 | /*#flexcanvas{ 6 | width: 100%; 7 | height: 600px !important; 8 | }*/ 9 | html, body, .flexContainer { 10 | min-height: 100vh; 11 | font-family: Helvetica Neue, Helvetica, Arial, sans-serif; 12 | } 13 | body { 14 | padding: 0; 15 | margin: 0; 16 | } 17 | h1,h2,h3,h4 { 18 | font-family: Din Alternate, Helvetica Neue, Helvetica, Arial, sans-serif; 19 | color: #666; 20 | } 21 | 22 | .flexContainer { 23 | width: 100%; 24 | height: 100vh; 25 | } 26 | 27 | .flexContainer > * { 28 | height: 100%; 29 | padding-top: 32px; 30 | } 31 | 32 | .rowParent, .columnParent{ 33 | display: -webkit-box; 34 | display: -ms-flexbox; 35 | display: -webkit-flex; 36 | display: flex; 37 | -webkit-box-direction: normal; 38 | -webkit-box-orient: horizontal; 39 | -webkit-flex-direction: row; 40 | -ms-flex-direction: row; 41 | flex-direction: row; 42 | -webkit-flex-wrap: nowrap; 43 | -ms-flex-wrap: nowrap; 44 | flex-wrap: nowrap; 45 | -webkit-box-pack: start; 46 | -webkit-justify-content: flex-start; 47 | -ms-flex-pack: start; 48 | justify-content: flex-start; 49 | -webkit-align-content: stretch; 50 | -ms-flex-line-pack: stretch; 51 | align-content: stretch; 52 | -webkit-box-align: stretch; 53 | -webkit-align-items: stretch; 54 | -ms-flex-align: stretch; 55 | align-items: stretch; 56 | } 57 | 58 | .columnParent{ 59 | -webkit-box-orient: vertical; 60 | -webkit-flex-direction: column; 61 | -ms-flex-direction: column; 62 | flex-direction: column; 63 | } 64 | 65 | .flexChild{ 66 | -webkit-box-flex: 1; 67 | -webkit-flex: 1; 68 | -ms-flex: 1; 69 | flex: 1; 70 | -webkit-align-self: auto; 71 | -ms-flex-item-align: auto; 72 | align-self: auto; 73 | } 74 | 75 | .editorBox, .stackRow { 76 | flex: 3; 77 | position: relative; 78 | } 79 | 80 | .editorBox { 81 | background: #FDF6E3; 82 | } 83 | 84 | .codeColumn { 85 | flex: 0.5; 86 | margin-right: 20px; 87 | border-right: 2px #ddd solid; 88 | } 89 | 90 | .editor { 91 | min-height: 100%; 92 | width: 100%; 93 | font-family: monospace; 94 | font-size: 13px; 95 | line-height: 1.75; 96 | display: inline-block; 97 | white-space: pre; 98 | padding-left: 45px; 99 | color: #333; 100 | font-family: Monaco, Menlo, 'Ubuntu Mono', Consolas, source-code-pro, monospace; 101 | font-size: 12px; 102 | overflow-y: hidden; 103 | position: relative; 104 | } 105 | 106 | .editor:before { 107 | content: attr(data-lines); 108 | /*content: "1\a 2\A 3\A 4\A 5\A 6\A 7\A 8\A 9\A 10\A 11\A 12\A 13\A 14\A 15\A 16\A 17\A";*/ 109 | position: absolute; 110 | top: 0; 111 | left: 0; 112 | width: 41px; 113 | padding-left: 7.5px; 114 | text-align: center; 115 | background: #FBF1D3; 116 | height: 100%; 117 | white-space: pre; 118 | } 119 | 120 | .ace-editor-wrapper { 121 | flex: 1; 122 | } 123 | 124 | .ace_editor { 125 | line-height: 1.75!important; 126 | } 127 | 128 | .html-scratchpad { 129 | padding: 10px; 130 | background: #eee; 131 | overflow: scroll; 132 | font-family: Helvetica Neue, Helvetica, Arial, sans-serif; 133 | } 134 | 135 | .webapi { 136 | padding: 5px; 137 | border: 2px #ddd solid; 138 | margin: 5px; 139 | } 140 | 141 | .webapi-code { 142 | font-family: monospace; 143 | height: 100%; 144 | vertical-align: middle; 145 | margin-right: 5px; 146 | } 147 | 148 | 149 | .webapi-code:before { 150 | content: ''; 151 | display: inline-block; 152 | vertical-align: middle; 153 | height: 100%; 154 | } 155 | 156 | .webapi-timer > * { 157 | display: inline-block; 158 | vertical-align: middle; 159 | height: 44px; 160 | } 161 | 162 | 163 | @keyframes rota { 164 | 0% { transform: rotate(0deg); } 165 | 100% { transform: rotate(360deg); } 166 | } 167 | 168 | @keyframes fill { 169 | 0% { opacity: 0; } 170 | 50%, 100% { opacity: 1; } 171 | } 172 | 173 | @keyframes mask { 174 | 0% { opacity: 1; } 175 | 50%, 100% { opacity: 0; } 176 | } 177 | 178 | @keyframes check { 179 | 0% { opacity: 0; } 180 | 98% { opacity: 0; } 181 | 100% { opacity: 1; } 182 | } 183 | 184 | @keyframes background { 185 | 0% { background: rgba(148,255,148,0); } 186 | 95% { background: rgba(148,255,148,0); } 187 | 100% { background: rgba(148,255,148,1); } 188 | } 189 | 190 | .stopwatch-wrapper { 191 | width: 44px; 192 | height: 44px; 193 | position: relative; 194 | background: rgba(148,255,148,1); 195 | border-radius: 250px; 196 | animation: background 10s linear; 197 | overflow: hidden; 198 | } 199 | 200 | .stopwatch-wrapper:before { 201 | position: absolute; 202 | content: ""; 203 | width: 50%; 204 | height: 28%; 205 | top: 35%; 206 | left: 25.5%; 207 | border-left: 7px white solid; 208 | border-bottom: 7px white solid; 209 | transform: rotate(-45deg); 210 | z-index: 500; 211 | } 212 | 213 | .stopwatch-pie { 214 | border-radius: 22px; 215 | width: 50%; 216 | height: 100%; 217 | position: absolute; 218 | border: 8px solid rgb(148,255,148); 219 | } 220 | 221 | .stopwatch-spinner { 222 | border-top-right-radius: 0; 223 | border-bottom-right-radius: 0; 224 | z-index: 200; 225 | border-right: none; 226 | animation: rota 10s linear; 227 | transform-origin: 100% 50%; 228 | } 229 | 230 | .stopwatch-filler { 231 | border-top-left-radius: 0; 232 | border-bottom-left-radius: 0; 233 | z-index: 100; 234 | border-left: none; 235 | animation: fill 10s steps(1, end); 236 | left: 50%; 237 | opacity: 1; 238 | } 239 | 240 | .stopwatch-mask { 241 | width: 50%; 242 | height: 100%; 243 | position: absolute; 244 | z-index: 300; 245 | opacity: 0; 246 | background: white; 247 | animation: mask 10s steps(1, end); 248 | border-radius: 250px 0 0 250px; 249 | } 250 | 251 | .stopwatch-spinner, .stopwatch-filler, .stopwatch-mask, .stopwatch-wrapper { 252 | animation-duration: 3s; 253 | } 254 | 255 | 256 | .callback-queue { 257 | margin-bottom: 20px; 258 | margin-right: 20px; 259 | overflow-y: scroll; 260 | } 261 | .callback-queue.render-queue { 262 | margin-bottom: 5px; 263 | } 264 | 265 | .callback { 266 | font-family: monospace; 267 | padding: 10px; 268 | display: inline-block; 269 | margin-right: 10px; 270 | border: 2px #ddd solid; 271 | width: 200px; 272 | overflow: scroll; 273 | } 274 | 275 | .callback-active { 276 | background: rgb(220,224,255); 277 | } 278 | 279 | .stackBox { 280 | position: relative; 281 | margin-right: 20px; 282 | } 283 | 284 | .stack-wrapper { 285 | width: 244px; 286 | padding: 20px; 287 | margin-bottom: 86px; 288 | margin-top: 20px; 289 | } 290 | 291 | .stack-wrapper, .callback-queue, .webapis { 292 | border: 2px #ddd dashed; 293 | position: relative; 294 | padding: 25px 10px 10px 10px; 295 | } 296 | 297 | .stack-wrapper:before, 298 | .callback-queue:before, 299 | .webapis:before { 300 | font-family: Helvetica Neue, Arial, sans-serif; 301 | position: absolute; 302 | top: 0; 303 | left: 0; 304 | padding: 5px; 305 | color: #999; 306 | } 307 | 308 | .webapis { 309 | margin-top: 20px; 310 | margin-bottom: 86px; 311 | margin-right: 20px; 312 | } 313 | 314 | .webapis:before { 315 | content: "Web Apis"; 316 | } 317 | 318 | .stack-wrapper:before { 319 | content: "Call Stack"; 320 | } 321 | 322 | .callback-queue:before { 323 | content: "Callback Queue"; 324 | } 325 | 326 | .callback-queue.render-queue:before { 327 | content: "Render Queue"; 328 | } 329 | 330 | .stack { 331 | position: absolute; 332 | bottom: 0px; 333 | left: 0; 334 | padding-left: 10px; 335 | } 336 | .stackBox > .spinner-wrapper { 337 | position: absolute; 338 | bottom: 0; 339 | left: 0; 340 | margin-bottom: 5px; 341 | } 342 | 343 | .stack-item { 344 | font-family: monospace; 345 | padding: 10px; 346 | margin: 5px; 347 | border: 2px #ddd solid; 348 | width: 220px; 349 | margin: 10px auto; 350 | } 351 | 352 | 353 | .spinner-wrapper { 354 | height: 76px; 355 | width: 76px; 356 | position: relative; 357 | padding: 9px; 358 | transform: rotate(0deg); 359 | margin-left: 82px; 360 | } 361 | 362 | .spinner-wrapper-transition { 363 | transform: rotate(180deg); 364 | transition: transform 0.5s linear; 365 | } 366 | 367 | .spinner-circle { 368 | height: 100%; 369 | width: 100%; 370 | border-radius: 30px; 371 | border: 6px coral solid; 372 | } 373 | 374 | .spinner-arrow { 375 | width: 0px; 376 | height: 0px; 377 | border-style: solid; 378 | border-width: 0 16px 16px 16px; 379 | border-color: transparent transparent white transparent; 380 | position: absolute; 381 | } 382 | 383 | .spinner-arrow:after { 384 | content: ""; 385 | width: 0px; 386 | height: 0px; 387 | border-style: solid; 388 | border-width: 0 12px 12px 12px; 389 | border-color: transparent transparent coral transparent; 390 | position: absolute; 391 | } 392 | 393 | .spinner-arrow-left:after { 394 | left: -12px; 395 | top: 4px; 396 | } 397 | 398 | .spinner-arrow-left { 399 | left: -2.5px; 400 | top: calc(50% - 9px); 401 | } 402 | 403 | .spinner-arrow-right { 404 | right: -2px; 405 | top: calc(50% - 6px); 406 | border-width: 16px 16px 0px 16px; 407 | border-color: white transparent transparent transparent; 408 | } 409 | 410 | .spinner-arrow-right:after { 411 | border-width: 12px 12px 0px 12px; 412 | border-color: coral transparent transparent transparent; 413 | 414 | left: -12px; 415 | bottom: 4px; 416 | } 417 | 418 | .code-node { 419 | } 420 | 421 | .code-node:empty { 422 | display: none; 423 | } 424 | 425 | .code-node.running { 426 | background: rgba(236, 117, 74, 0.25); 427 | } 428 | 429 | 430 | /*********************************/ 431 | .tr-webapis-enter { 432 | background: rgb(255,255,194); 433 | transition: background 0.2s linear; 434 | } 435 | 436 | .tr-webapis-enter.tr-webapis-enter-active { 437 | background: white; 438 | } 439 | 440 | .tr-webapis-leave { 441 | background: white; 442 | transition: background 0.01s linear; 443 | } 444 | 445 | .tr-webapis-leave.tr-webapis-leave-active { 446 | background: white; 447 | } 448 | 449 | .tr-webapi-spawn { 450 | background: white; 451 | transition: background 0.1s linear; 452 | } 453 | 454 | .tr-webapi-spawn.tr-webapi-spawn-active { 455 | background: rgb(255, 175, 146); 456 | } 457 | 458 | /*********************************/ 459 | .tr-queue-enter { 460 | background: rgb(255,255,194); 461 | transition: background 0.2s linear; 462 | } 463 | 464 | .tr-queue-enter.tr-queue-enter-active { 465 | background: white; 466 | } 467 | 468 | .tr-queue-leave { 469 | transform: translate(0, 0); 470 | opacity: 1; 471 | transition: all 0.25s linear; 472 | } 473 | 474 | .tr-queue-leave.tr-queue-leave-active { 475 | transform: translate(0, -100px); 476 | opacity: 0.01; 477 | } 478 | 479 | 480 | /*********************************/ 481 | .tr-stack-enter { 482 | transform: translate(0, 0); 483 | opacity: 0.01; 484 | transition: all 0.1s linear; 485 | } 486 | 487 | .tr-stack-enter.stack-item-callback { 488 | transform: translate(0, 100px); 489 | transition: all 0.25s linear; 490 | } 491 | 492 | .tr-stack-enter.tr-stack-enter-active { 493 | transform: translate(0, 0); 494 | opacity: 1; 495 | } 496 | 497 | .tr-stack-leave { 498 | opacity: 1; 499 | transition: all 0.1s linear; 500 | } 501 | 502 | .tr-stack-leave.tr-stack-leave-active { 503 | opacity: 0.01; 504 | transition: all 0.1s linear; 505 | } 506 | 507 | .htmlEditorBox { 508 | position: relative; 509 | } 510 | 511 | .editor-switch { 512 | position: absolute; 513 | top: 5px; 514 | right: 5px; 515 | z-index: 9999; 516 | } 517 | 518 | .render-queue .callback-queued { 519 | background: white; 520 | } 521 | 522 | .render-queue .callback-delayed { 523 | background: rgba(255,0,0,0.5); 524 | transition: background 0.2s linear; 525 | } 526 | 527 | .render-queue .callback-rendered { 528 | background: rgba(0,255,0,0.5); 529 | transition: background 0.4s linear; 530 | } 531 | 532 | 533 | /* 534 | .tr-render-queue-enter { 535 | background: white; 536 | transition: background 0.5s linear; 537 | } 538 | 539 | .tr-render-queue-enter.tr-render-queue-enter-active { 540 | background: red; 541 | } 542 | 543 | .tr-render-queue-leave { 544 | background: green; 545 | opacity: 1; 546 | transition: all 0.2s linear; 547 | } 548 | 549 | .tr-render-queue-leave.tr-render-queue-leave-active { 550 | background: white; 551 | opacity: 0.01; 552 | transition: all 0.2s linear; 553 | }*/ 554 | /************************/ 555 | 556 | .top-nav { 557 | background: #D80; 558 | padding: 5px; 559 | height: 32px; 560 | position: absolute; 561 | width: 100%; 562 | color: #eee; 563 | padding-left: 10px; 564 | } 565 | 566 | .top-nav h1 { 567 | font-size: 20px; 568 | display: inline; 569 | position: relative; 570 | top: -2px; 571 | color: #eee; 572 | } 573 | 574 | .top-nav .settings-button { 575 | border: none; 576 | background: none; 577 | color: #eee; 578 | font-size: 30px; 579 | line-height: 30px; 580 | margin: 0; 581 | padding: 0; 582 | margin-right: 10px; 583 | margin-top: -7px; 584 | } 585 | 586 | .settingsColumn { 587 | box-sizing: border-box; 588 | flex: none; 589 | width: 200px; 590 | transition: width 0.15s linear; 591 | font-family: Helvetica Neue, Helvetica, Arial, sans-serif; 592 | background: #ddd; 593 | color: #666; 594 | } 595 | 596 | .settingsColumn .setting { 597 | margin: 20px; 598 | } 599 | 600 | .settingsColumn.hidden { 601 | width: 0; 602 | overflow: hidden; 603 | } 604 | 605 | .ReactModal__Overlay { 606 | z-index: 20000; 607 | background: rgba(0,0,0,0.4); 608 | } 609 | 610 | .ReactModal__Content { 611 | left: 20%; 612 | right: 20%; 613 | border: none; 614 | font-size: 14px; 615 | } 616 | 617 | .ReactModal__Content * { 618 | color: #888; 619 | } 620 | 621 | .ReactModal__Content h1 { 622 | margin-top: 0; 623 | } 624 | 625 | .ReactModal__Content h1 + h2 { 626 | margin-top: 20px; 627 | } 628 | 629 | .ReactModal__Content h2 { 630 | margin-top: 40px; 631 | margin-bottom: 10px; 632 | } 633 | 634 | .ReactModal__Content a { 635 | color: #DF8900; 636 | } 637 | 638 | .ReactModal__Content li { 639 | margin-bottom: 5px; 640 | } 641 | 642 | .ReactModal__Content iframe { 643 | margin: 40px auto; 644 | max-width: 100%; 645 | display: block; 646 | border: none; 647 | } 648 | 649 | 650 | 651 | .modal-button { 652 | float: right; 653 | } 654 | 655 | .modal-button:hover { 656 | cursor: pointer; 657 | text-decoration: underline; 658 | } 659 | 660 | p.info { 661 | text-align: center; 662 | margin-top: 40px; 663 | } 664 | 665 | a.modalClose { 666 | position: absolute; 667 | top: 15px; 668 | right: 15px; 669 | } 670 | a:hover { 671 | cursor: pointer; 672 | } 673 | -------------------------------------------------------------------------------- /loupe.js: -------------------------------------------------------------------------------- 1 | var React = require('react/addons'); 2 | window.React = React; 3 | var App = require('./components/app.jsx'); 4 | var AmpersandCollection = require('ampersand-collection'); 5 | var AmpersandState = require('ampersand-state'); 6 | var deval = require('deval'); 7 | 8 | var CallStack = require('./models/callstack'); 9 | 10 | var Code = require('./models/code'); 11 | var Apis = require('./models/apis'); 12 | var CallbackQueue = require('./models/callback-queue'); 13 | var RenderQueue = require('./models/render-queue'); 14 | 15 | var Router = require('./router'); 16 | var Modal = require('react-modal'); 17 | 18 | window.app = {}; 19 | 20 | window.app.router = new Router(); 21 | 22 | window.app.store = { 23 | callstack: new CallStack(), 24 | code: new Code(), 25 | apis: new Apis(), 26 | queue: new CallbackQueue(), 27 | renderQueue: new RenderQueue() 28 | }; 29 | 30 | app.store.code.on('change:codeLines', function () { 31 | }); 32 | 33 | app.store.code.on('change:encodedSource', function () { 34 | app.router.navigate('?code=' + app.store.code.encodedSource); 35 | }); 36 | 37 | //app.store.code.on('all', function () { 38 | // console.log('Code event', arguments); 39 | //}); 40 | 41 | app.store.code.on('node:will-run', function (id, source, invocation) { 42 | app.store.callstack.add({ 43 | _id: id, 44 | code: source 45 | }); 46 | }); 47 | 48 | app.store.code.on('node:did-run', function (id, invocation) { 49 | app.store.callstack.pop(); 50 | //app.store.callstack.remove(app.store.callstack.at(app.store.call 51 | //app.store.callstack.remove(id + ':' + invocation); 52 | }); 53 | 54 | app.store.code.on('webapi:started', function (data) { 55 | app.store.apis.add(data, { merge: true }); 56 | }); 57 | 58 | app.store.code.on('callback:shifted', function (id) { 59 | var callback = app.store.queue.get(id); 60 | if (!callback) { 61 | callback = app.store.apis.get(id); 62 | } 63 | 64 | app.store.callstack.add({ 65 | id: callback.id.toString(), 66 | code: callback.code, 67 | isCallback: true 68 | }); 69 | app.store.queue.remove(callback); 70 | }); 71 | 72 | app.store.code.on('callback:completed', function (id) { 73 | //app.store.callstack.remove(id.toString()); 74 | app.store.callstack.pop(); 75 | }); 76 | 77 | app.store.code.on('callback:spawn', function (data) { 78 | var webapi = app.store.apis.get(data.apiId); 79 | 80 | if (webapi) { 81 | webapi.trigger('callback:spawned', webapi); 82 | } 83 | app.store.queue.add(data); 84 | }); 85 | 86 | app.store.apis.on('callback:spawn', function (data) { 87 | app.store.queue.add(data); 88 | }); 89 | 90 | app.store.code.on('reset-everything', function () { 91 | app.store.renderQueue.reset(); 92 | app.store.queue.reset(); 93 | app.store.callstack.reset(); 94 | app.store.apis.reset(); 95 | }); 96 | 97 | app.store.code.on('paused', function () { 98 | app.store.apis.pause(); 99 | }); 100 | 101 | app.store.code.on('resumed', function () { 102 | app.store.apis.resume(); 103 | }); 104 | 105 | app.store.callstack.on('all', function () { 106 | if (app.store.callstack.length === 0) { 107 | app.store.renderQueue.shift(); 108 | } 109 | }); 110 | 111 | app.store.renderQueue.on('add', function () { 112 | if (app.store.callstack.length === 0) { 113 | app.store.renderQueue.shift(); 114 | } 115 | }); 116 | 117 | if (window.location.origin.match('latentflip.com')) { 118 | window.app.router.history.start({ pushState: true, root: '/loupe/' }); 119 | } else { 120 | window.app.router.history.start({ pushState: true }); 121 | } 122 | 123 | Modal.setAppElement(document.body); 124 | Modal.injectCSS(); 125 | React.renderComponent(App(), document.body); 126 | -------------------------------------------------------------------------------- /loupe.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react/addons'); 2 | window.React = React; 3 | var App = require('./components/app.jsx'); 4 | var AmpersandCollection = require('ampersand-collection'); 5 | var AmpersandState = require('ampersand-state'); 6 | var deval = require('deval'); 7 | 8 | var CallStack = require('./models/callstack'); 9 | 10 | var Code = require('./models/code'); 11 | var Apis = require('./models/apis'); 12 | var CallbackQueue = require('./models/callback-queue'); 13 | var RenderQueue = require('./models/render-queue'); 14 | 15 | var Router = require('./router'); 16 | var Modal = require('react-modal'); 17 | 18 | window.app = {}; 19 | 20 | window.app.router = new Router(); 21 | 22 | window.app.store = { 23 | callstack: new CallStack(), 24 | code: new Code(), 25 | apis: new Apis(), 26 | queue: new CallbackQueue(), 27 | renderQueue: new RenderQueue() 28 | }; 29 | 30 | app.store.code.on('change:codeLines', function () { 31 | }); 32 | 33 | app.store.code.on('change:encodedSource', function () { 34 | app.router.navigate('?code=' + app.store.code.encodedSource); 35 | }); 36 | 37 | //app.store.code.on('all', function () { 38 | // console.log('Code event', arguments); 39 | //}); 40 | 41 | app.store.code.on('node:will-run', function (id, source, invocation) { 42 | app.store.callstack.add({ 43 | _id: id, 44 | code: source 45 | }); 46 | }); 47 | 48 | app.store.code.on('node:did-run', function (id, invocation) { 49 | app.store.callstack.pop(); 50 | //app.store.callstack.remove(app.store.callstack.at(app.store.call 51 | //app.store.callstack.remove(id + ':' + invocation); 52 | }); 53 | 54 | app.store.code.on('webapi:started', function (data) { 55 | app.store.apis.add(data, { merge: true }); 56 | }); 57 | 58 | app.store.code.on('callback:shifted', function (id) { 59 | var callback = app.store.queue.get(id); 60 | if (!callback) { 61 | callback = app.store.apis.get(id); 62 | } 63 | 64 | app.store.callstack.add({ 65 | id: callback.id.toString(), 66 | code: callback.code, 67 | isCallback: true 68 | }); 69 | app.store.queue.remove(callback); 70 | }); 71 | 72 | app.store.code.on('callback:completed', function (id) { 73 | //app.store.callstack.remove(id.toString()); 74 | app.store.callstack.pop(); 75 | }); 76 | 77 | app.store.code.on('callback:spawn', function (data) { 78 | var webapi = app.store.apis.get(data.apiId); 79 | 80 | if (webapi) { 81 | webapi.trigger('callback:spawned', webapi); 82 | } 83 | app.store.queue.add(data); 84 | }); 85 | 86 | app.store.apis.on('callback:spawn', function (data) { 87 | app.store.queue.add(data); 88 | }); 89 | 90 | app.store.code.on('reset-everything', function () { 91 | app.store.renderQueue.reset(); 92 | app.store.queue.reset(); 93 | app.store.callstack.reset(); 94 | app.store.apis.reset(); 95 | }); 96 | 97 | app.store.code.on('paused', function () { 98 | app.store.apis.pause(); 99 | }); 100 | 101 | app.store.code.on('resumed', function () { 102 | app.store.apis.resume(); 103 | }); 104 | 105 | app.store.callstack.on('all', function () { 106 | if (app.store.callstack.length === 0) { 107 | app.store.renderQueue.shift(); 108 | } 109 | }); 110 | 111 | app.store.renderQueue.on('add', function () { 112 | if (app.store.callstack.length === 0) { 113 | app.store.renderQueue.shift(); 114 | } 115 | }); 116 | 117 | if (window.location.origin.match('latentflip.com')) { 118 | window.app.router.history.start({ pushState: true, root: '/loupe/' }); 119 | } else { 120 | window.app.router.history.start({ pushState: true }); 121 | } 122 | 123 | Modal.setAppElement(document.body); 124 | Modal.injectCSS(); 125 | React.renderComponent(App(), document.body); 126 | -------------------------------------------------------------------------------- /models/apis.js: -------------------------------------------------------------------------------- 1 | var AmpersandCollection = require('ampersand-collection'); 2 | var AmpersandState = require('ampersand-state'); 3 | 4 | var Timeout = AmpersandState.extend({ 5 | props: { 6 | id: ['string'], 7 | type: ['string', true, 'timeout'], 8 | timeout: ['number', true, 0], 9 | code: 'string', 10 | playState: ['string', true, 'running'] 11 | }, 12 | session: { 13 | timeoutId: 'number', 14 | startedAt: 'number', 15 | pausedAt: 'number', 16 | remainingTime: 'number' 17 | }, 18 | derived: { 19 | timeoutString: { 20 | deps: ['timeout'], 21 | fn: function () { 22 | return this.timeout/1000 + 's'; 23 | } 24 | } 25 | }, 26 | 27 | pause: function () { 28 | this.pausedAt = Date.now(); 29 | this.remainingTime = this.remainingTime - (this.pausedAt - this.startedAt); 30 | this.playState = 'paused'; 31 | clearTimeout(this.timeoutId); 32 | }, 33 | 34 | resume: function () { 35 | this.startedAt = Date.now(); 36 | this.playState = 'running'; 37 | 38 | this.timeoutId = setTimeout(function () { 39 | this.trigger('callback:spawn', { 40 | id: this.id, 41 | code: this.code 42 | }); 43 | this.collection.remove(this); 44 | }.bind(this), this.remainingTime); 45 | }, 46 | 47 | initialize: function () { 48 | this.startedAt = Date.now(); 49 | this.remainingTime = this.timeout; 50 | 51 | this.timeoutId = setTimeout(function () { 52 | this.trigger('callback:spawn', { 53 | id: this.id, 54 | code: this.code 55 | }); 56 | this.collection.remove(this); 57 | }.bind(this), this.remainingTime); 58 | 59 | this.on('remove', function () { 60 | clearTimeout(this.timeoutId); 61 | }.bind(this)); 62 | }, 63 | 64 | getPausedState: function () { 65 | return { remainingTime: this.remainingTime }; 66 | } 67 | }); 68 | 69 | var Query = AmpersandState.extend({ 70 | props: { 71 | id: 'string', 72 | type: ['string', true, 'query'], 73 | selector: 'string', 74 | event: 'string' 75 | }, 76 | derived: { 77 | code: { 78 | deps: ['selector', 'event'], 79 | fn: function () { 80 | return "$.on('" + this.selector + "', '" + this.event + "', ...)"; 81 | } 82 | } 83 | }, 84 | pause: function () { 85 | }, 86 | resume: function () { 87 | }, 88 | getPausedState: function () { 89 | return { }; 90 | } 91 | }); 92 | 93 | module.exports = AmpersandCollection.extend({ 94 | model: function (props, opts) { 95 | if (props.type === 'timeout') { 96 | return new Timeout(props, opts); 97 | } 98 | if (props.type === 'query') { 99 | return new Query(props, opts); 100 | } 101 | throw 'Unknown prop type: ' + props.type; 102 | }, 103 | pause: function () { 104 | this.each(function (model) { model.pause(); }); 105 | }, 106 | resume: function () { 107 | this.each(function (model) { model.resume(); }); 108 | }, 109 | getPausedState: function () { 110 | var data = {}; 111 | this.each(function (model) { 112 | data[model.id] = model.getPausedState(); 113 | }); 114 | return data; 115 | } 116 | }); 117 | -------------------------------------------------------------------------------- /models/base-collection.js: -------------------------------------------------------------------------------- 1 | var Collection = require('ampersand-collection'); 2 | 3 | module.exports = Collection.extend(require('ampersand-collection-underscore-mixin')); 4 | -------------------------------------------------------------------------------- /models/callback-queue.js: -------------------------------------------------------------------------------- 1 | var AmpersandCollection = require('ampersand-collection'); 2 | var Callback = require('./callback'); 3 | 4 | module.exports = AmpersandCollection.extend({ 5 | model: Callback 6 | }); 7 | -------------------------------------------------------------------------------- /models/callback.js: -------------------------------------------------------------------------------- 1 | var AmpersandState = require('ampersand-state'); 2 | 3 | module.exports = AmpersandState.extend({ 4 | props: { 5 | id: 'string', 6 | code: 'string', 7 | state: 'string' 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /models/callstack.js: -------------------------------------------------------------------------------- 1 | var AmpersandState = require('ampersand-state'); 2 | var AmpersandCollection = require('ampersand-collection'); 3 | 4 | var StackFrame = AmpersandState.extend({ 5 | props: { 6 | _id: 'any', 7 | _key: 'string', 8 | code: 'string' 9 | }, 10 | initialize: function () { 11 | this._key = this._key || Date.now().toString(); 12 | } 13 | }); 14 | 15 | module.exports = AmpersandCollection.extend({ 16 | model: StackFrame, 17 | last: function () { 18 | return this.at(this.length - 1); 19 | }, 20 | pop: function () { 21 | var removed = this.last(); 22 | this.remove(removed); 23 | return removed; 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /models/code.js: -------------------------------------------------------------------------------- 1 | var AmpersandState = require('ampersand-state'); 2 | var instrumentCode = require('../lib/instrument-code'); 3 | var deval = require('deval'); 4 | var wrapInsertionPoints = require('../lib/wrap-insertion-points'); 5 | var weevil = require('weevil'); 6 | var tag = require('../lib/tag'); 7 | var delayMaker = require('../lib/delay'); 8 | var debounce = require('lodash.debounce'); 9 | 10 | var $ = require('../lib/plugins/query'); 11 | var consolePlugin = require('../lib/plugins/console'); 12 | 13 | var cleanupCode = function (code) { 14 | return code.replace(/
/g, '\n'); 15 | }; 16 | 17 | module.exports = AmpersandState.extend({ 18 | props: { 19 | htmlScratchpad: ['array', true], 20 | codeLines: ['array', true], 21 | worker: 'any', 22 | delay: ['number', true, function () { 23 | return parseInt(localStorage.loupeDelay, 10) || 750; 24 | }], 25 | running: ['boolean', true, false], 26 | simulateRenders: ['boolean', true, false] 27 | }, 28 | derived: { 29 | rawHtmlScratchpad: { 30 | deps: ['htmlScratchpad'], 31 | fn: function () { 32 | return this.htmlScratchpad.join('\n'); 33 | } 34 | }, 35 | html: { 36 | deps: ['codeLines'], 37 | fn: function () { 38 | return this.codeLines.join('
'); 39 | } 40 | }, 41 | rawCode: { 42 | deps: ['codeLines'], 43 | fn: function () { 44 | return this.codeLines.join('\n'); 45 | } 46 | }, 47 | encodedSource: { 48 | deps: ['rawCode', 'rawHtmlScratchpad'], 49 | fn: function () { 50 | return encodeURIComponent(btoa(this.rawCode) + "!!!" + btoa(this.rawHtmlScratchpad)); 51 | } 52 | }, 53 | cleanCode: { 54 | deps: ['rawCode'], 55 | fn: function () { 56 | return this.rawCode; 57 | } 58 | }, 59 | instrumented: { 60 | deps: ['cleanCode'], 61 | fn: function () { 62 | return instrumentAndWrapHTML(this.cleanCode); 63 | } 64 | }, 65 | wrappedHtml: { 66 | deps: ['instrumented'], 67 | fn: function () { 68 | return this.instrumented.html; 69 | } 70 | }, 71 | runnableCode: { 72 | deps: ['instrumented'], 73 | fn: function () { 74 | return this.instrumented.code; 75 | } 76 | }, 77 | nodeSourceCode: { 78 | deps: ['instrumented'], 79 | fn: function () { 80 | return this.instrumented.nodeSourceCode; 81 | } 82 | } 83 | }, 84 | 85 | initialize: function () { 86 | this.on('ready-to-run', function () { this.running = true; }); 87 | this.on('resumed', function () { this.running = true; }); 88 | this.on('paused', function () { this.running = false; }); 89 | this.on('reset-everything', function () { this.running = false; }); 90 | 91 | var self = this; 92 | var pauseAndResume = debounce(function () { 93 | if (self.running) { 94 | self.pause(); 95 | self.resume(); 96 | } 97 | }, 100); 98 | 99 | this.on('change:delay', pauseAndResume); 100 | }, 101 | 102 | makeWorkerCode: function (fromId, appState) { 103 | return makeWorkerCode(this.runnableCode, { 104 | delay: this.delay, 105 | resumeFromDelayId: fromId, 106 | appState: appState 107 | }); 108 | }, 109 | 110 | decodeUriSource: function (encoded) { 111 | var parts = decodeURIComponent(encoded).split('!!!'); 112 | try { 113 | this.codeLines = atob(parts[0]).split('\n'); 114 | } catch (e) { 115 | this.codeLines = []; 116 | } 117 | try { 118 | this.htmlScratchpad = atob(parts[1]).split('\n'); 119 | } catch (e) { 120 | this.htmlScratchpad = []; 121 | } 122 | }, 123 | 124 | resetEverything: function () { 125 | this.trigger('reset-everything'); 126 | this.running = false; 127 | if (this.worker) { this.worker.kill(); } 128 | }, 129 | 130 | pause: function () { 131 | this.trigger('paused'); 132 | this.pausedExecution = this.currentExecution; 133 | this.worker.kill(); 134 | }, 135 | 136 | resume: function () { 137 | this.trigger('resumed'); 138 | var appState = { 139 | webapis: app.store.apis.getPausedState(), 140 | query: this.queryClient.historyLog 141 | }; 142 | 143 | this.ignoreEvents = true; 144 | this.run(this.pausedExecution, appState); 145 | }, 146 | 147 | run: function (fromId, appState) { 148 | appState = appState || { 149 | webapis: {}, 150 | query: {} 151 | }; 152 | 153 | setTimeout(function () { 154 | var self = this; 155 | 156 | if (!fromId) { 157 | this.resetEverything(); 158 | } 159 | 160 | this.trigger('ready-to-run'); 161 | this.worker = weevil(this.makeWorkerCode(fromId, appState)); 162 | 163 | //TODO this shouldn't know about the scratchpad 164 | this.queryClient = $.createClient(this, this.worker, document.querySelector('.html-scratchpad')); 165 | consolePlugin.createClient(this, this.worker); 166 | 167 | this.worker 168 | .on('node:before', function (node) { 169 | self.trigger('node:will-run', node.id, self.nodeSourceCode[node.id]); 170 | //$('#node-' + node.id).addClass('running'); 171 | }) 172 | .on('node:after', function (node) { 173 | self.trigger('node:did-run', node.id); 174 | //$('#node-' + node.id).removeClass('running'); 175 | }) 176 | .on('timeout:created', function (timer) { 177 | self.trigger('webapi:started', { 178 | id: 'timer:' + timer.id, 179 | type: 'timeout', 180 | timeout: timer.delay, 181 | code: timer.code.split('\n').join(' ') 182 | }); 183 | }) 184 | .on('timeout:started', function (timer) { 185 | self.trigger('callback:shifted', 'timer:' + timer.id); 186 | }) 187 | .on('timeout:finished', function (timer) { 188 | self.trigger('callback:completed', 'timer:' + timer.id); 189 | }) 190 | .on('callback:shifted', function (callbackId) { 191 | self.trigger('callback:shifted', callbackId); 192 | }) 193 | .on('callback:completed', function (callbackId) { 194 | self.trigger('callback:completed', callbackId); 195 | }) 196 | .on('delay', function (delayId) { 197 | if (self.pausedExecution) { 198 | if (delayId >= self.pausedExecution) { 199 | self.ignoreEvents = false; 200 | } 201 | } 202 | self.currentExecution = delayId; 203 | }); 204 | 205 | }.bind(this), 0); 206 | } 207 | }); 208 | 209 | var instrumentAndWrapHTML = function (code) { 210 | var instrumented = instrumentCode(code, { 211 | before: function (id, node) { 212 | var source = JSON.stringify(node.source()); 213 | return deval(function (id, type, source) { 214 | weevil.send('node:before', { id: $id$, type: "$type$", source: $source$ }), delay() 215 | }, id, node.type, source); 216 | }, 217 | after: function (id, node) { 218 | return deval(function (id) { 219 | weevil.send('node:after', { id: $id$ }), delay() 220 | }, id); 221 | } 222 | }); 223 | 224 | var nodeSourceCode = {}; 225 | var html = wrapInsertionPoints(code, instrumented.insertionPoints, { 226 | before: function (id) { 227 | return tag.o('span', { 228 | id: 'node-' + id, 229 | class: 'code-node', 230 | //style: "background: rgba(0,0,0,0.2);" 231 | }); 232 | }, 233 | after: function (id) { 234 | return tag.c('span'); 235 | }, 236 | withWrappedCode: function (id, code) { 237 | nodeSourceCode[id] = code; 238 | } 239 | }); 240 | 241 | html = html.replace(/;\n/g, ';'); 242 | 243 | return { 244 | code: instrumented.code, 245 | html: html, 246 | nodeSourceCode: nodeSourceCode 247 | }; 248 | }; 249 | 250 | function prependCode(prepend, code) { 251 | return prepend + ';\n' + code; 252 | } 253 | 254 | var makeWorkerCode = function (code, options) { 255 | var delayTime = options.delay; 256 | var resumeFromDelayId = options.resumeFromDelayId ? options.resumeFromDelayId.toString() : "null"; 257 | var appState = JSON.stringify(options.appState || {}); 258 | 259 | code = $.prependWorkerCode(code); 260 | code = consolePlugin.prependWorkerCode(code); 261 | code = prependCode(deval(function (delayMaker, delayTime, resumeFromDelayId, appState) { 262 | var loupe = {}; 263 | loupe.appState = $appState$; 264 | loupe._onDelayCallbacks = {}; 265 | loupe.onDelay = function (id, cb) { 266 | this._onDelayCallbacks[id] = this._onDelayCallbacks[id] || []; 267 | this._onDelayCallbacks[id].push(cb); 268 | }; 269 | loupe.triggerDelay = function (id) { 270 | var cbs = this._onDelayCallbacks[id]; 271 | if (cbs) { 272 | cbs.forEach(function (cb) { 273 | _setTimeout(cb, 0); 274 | }); 275 | } 276 | }; 277 | 278 | var _send = weevil.send; 279 | weevil.send = function (name) { 280 | if (loupe.skipDelays && name !== 'delay') { return; } 281 | return _send.apply(this, arguments); 282 | }; 283 | 284 | var delayMaker = $delayMaker$; 285 | 286 | var delay = delayMaker($delayTime$, $resumeFromDelayId$); 287 | 288 | //Override setTimeout 289 | var _setTimeout = self.setTimeout; 290 | self.setTimeout = function (fn, timeout/*, args...*/) { 291 | var args = Array.prototype.slice.call(arguments); 292 | fn = args.shift(); 293 | var timerId; 294 | 295 | var queued = +new Date(); 296 | var data = { id: timerId, delay: timeout, created: +new Date(), state: 'timing', code: (fn.name || "anonymous") + "()" }; 297 | args.unshift(function () { 298 | data.state = 'started'; 299 | data.started = +new Date(); 300 | data.error = (data.started - data.queued) - timeout; 301 | delay(); 302 | weevil.send('timeout:started', data); 303 | delay(); 304 | 305 | fn.apply(fn, arguments); 306 | 307 | data.state = 'finished'; 308 | data.finished = +new Date(); 309 | weevil.send('timeout:finished', data); 310 | delay(); 311 | }); 312 | 313 | if (loupe.appState.webapis[timerId]) { 314 | console.log('Overriding ' + args[1] + ' to ' + loupe.appState.webapis[timerId].remainingTime); 315 | args[1] = loupe.appState.webapis[timerId].remainingTime; 316 | } 317 | 318 | data.id = _setTimeout.apply(self, args); 319 | weevil.send('timeout:created', data); 320 | }; 321 | 322 | }, delayMaker.toString(), delayTime, resumeFromDelayId, appState), code); 323 | 324 | return code; 325 | }; 326 | -------------------------------------------------------------------------------- /models/render-queue.js: -------------------------------------------------------------------------------- 1 | var AmpersandCollection = require('ampersand-collection'); 2 | var AmpersandState = require('ampersand-state'); 3 | 4 | var Render = AmpersandState.extend({ 5 | props: { 6 | id: 'number', 7 | state: ['string', true, 'queued'], 8 | }, 9 | initialize: function () { 10 | setTimeout(function () { 11 | this.state = 'delayed'; 12 | }.bind(this), 20); 13 | } 14 | }); 15 | 16 | module.exports = AmpersandCollection.extend({ 17 | model: Render, 18 | 19 | initialize: function () { 20 | var id = 1; 21 | 22 | setInterval(function () { 23 | if (this.length === 0) { 24 | this.add({ id: id++ }); 25 | } 26 | }.bind(this), 1000); 27 | }, 28 | shift: function () { 29 | if (this.length > 0) { 30 | var model = this.at(0); 31 | setTimeout(function () { 32 | model.state = 'rendered'; 33 | setTimeout(function () { 34 | this.remove(model.id); 35 | }.bind(this), 250); 36 | }.bind(this), 20); 37 | } 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /models/stack-frame.js: -------------------------------------------------------------------------------- 1 | var AndModel = require('ampersand-model'); 2 | 3 | module.exports = AndModel.extend({ 4 | type: 'stack-frame', 5 | props: { 6 | _id: 'string', 7 | nodeId: 'number', 8 | source: 'string', 9 | expressionType: 'string', 10 | createdAt: 'number', 11 | isCallback: ['boolean', true, false] 12 | }, 13 | initialize: function () { 14 | this.id = Math.floor(Math.random() * 10000000); 15 | this.createdAt = +new Date(); 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /models/stack-frames.js: -------------------------------------------------------------------------------- 1 | var Collection = require('./base-collection'); 2 | 3 | var StackFrame = require('./stack-frame'); 4 | 5 | module.exports = Collection.extend({ 6 | model: StackFrame, 7 | comparator: function (m) { 8 | return -1 * m.createdAt; 9 | }, 10 | pop: function () { 11 | this.remove(this.at(this.length - 1)); 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /models/timeout.js: -------------------------------------------------------------------------------- 1 | var AndModel = require('ampersand-model'); 2 | 3 | module.exports = AndModel.extend({ 4 | type: 'timeout', 5 | props: { 6 | id: 'number', 7 | state: 'string', 8 | delay: 'number', 9 | created: ['date'], 10 | queued: ['date'], 11 | started: ['date'], 12 | finished: ['date'] 13 | }, 14 | initialize: function () { 15 | if (this.delay && this.created) { 16 | setTimeout(function () { 17 | this.state = 'queued'; 18 | this.queued = +new Date(); 19 | }.bind(this), this.delay); 20 | } 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /models/timeouts.js: -------------------------------------------------------------------------------- 1 | var Collection = require('./base-collection'); 2 | 3 | var Timeout = require('./timeout'); 4 | 5 | module.exports = Collection.extend({ 6 | model: Timeout 7 | }); 8 | -------------------------------------------------------------------------------- /npm-debug.log: -------------------------------------------------------------------------------- 1 | 0 info it worked if it ends with ok 2 | 1 verbose cli [ '/Users/latentflip/.nvm/v0.10.33/bin/node', 3 | 1 verbose cli '/Users/latentflip/.nvm/v0.10.33/bin/npm', 4 | 1 verbose cli 'run', 5 | 1 verbose cli 'build' ] 6 | 2 info using npm@1.4.28 7 | 3 info using node@v0.10.33 8 | 4 verbose run-script [ 'prebuild', 'build', 'postbuild' ] 9 | 5 info prebuild trace@0.0.0 10 | 6 info build trace@0.0.0 11 | 7 verbose unsafe-perm in lifecycle true 12 | 8 info trace@0.0.0 Failed to exec build script 13 | 9 error trace@0.0.0 build: `make build` 14 | 9 error Exit status 2 15 | 10 error Failed at the trace@0.0.0 build script. 16 | 10 error This is most likely a problem with the trace package, 17 | 10 error not with npm itself. 18 | 10 error Tell the author that this fails on your system: 19 | 10 error make build 20 | 10 error You can get their info via: 21 | 10 error npm owner ls trace 22 | 10 error There is likely additional logging output above. 23 | 11 error System Darwin 14.0.0 24 | 12 error command "/Users/latentflip/.nvm/v0.10.33/bin/node" "/Users/latentflip/.nvm/v0.10.33/bin/npm" "run" "build" 25 | 13 error cwd /Users/latentflip/Code/github/latentflip/loupe 26 | 14 error node -v v0.10.33 27 | 15 error npm -v 1.4.28 28 | 16 error code ELIFECYCLE 29 | 17 verbose exit [ 1, true ] 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trace", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "trace.js", 6 | "directories": { 7 | "test": "tests" 8 | }, 9 | "dependencies": { 10 | "ampersand-collection": "^1.3.15", 11 | "ampersand-collection-underscore-mixin": "^1.0.2", 12 | "ampersand-model": "^4.0.2", 13 | "ampersand-router": "^1.0.4", 14 | "ampersand-state": "^4.3.10", 15 | "ampersand-view": "^7.1.1", 16 | "brace": "^0.3.0", 17 | "deval": "~0.1.1", 18 | "escodegen": "~1.4.1", 19 | "esprima": "~1.2.2", 20 | "falafel": "~0.3.1", 21 | "hyperglue": "~1.3.1", 22 | "jquery": "~2.1.1", 23 | "lodash.debounce": "^2.4.1", 24 | "multiline": "~1.0.0", 25 | "react": "^0.12.0", 26 | "react-backbone-events-mixin": "^0.1.1", 27 | "react-modal": "0.0.5", 28 | "reactify": "^0.14.0", 29 | "tape": "~2.14.0", 30 | "underscore": "~1.7.0", 31 | "weevil": "~0.1.2" 32 | }, 33 | "devDependencies": { 34 | "autoprefixer": "^3.0.0", 35 | "browserify": "^5.11.1", 36 | "building-static-server": "^2.1.0", 37 | "glob-cli": "^1.0.0", 38 | "reactify": "^0.14.0" 39 | }, 40 | "scripts": { 41 | "build": "make build", 42 | "start": "building-static-server -p 3051" 43 | }, 44 | "author": "", 45 | "license": "ISC" 46 | } 47 | -------------------------------------------------------------------------------- /router.js: -------------------------------------------------------------------------------- 1 | var Router = require('ampersand-router'); 2 | 3 | 4 | module.exports = Router.extend({ 5 | routes: { 6 | '?code=:code': 'code', 7 | '': 'default' 8 | }, 9 | 10 | code: function (code) { 11 | app.store.code.decodeUriSource(code); 12 | }, 13 | default: function (code) { 14 | this.redirectTo("?code=JC5vbignYnV0dG9uJywgJ2NsaWNrJywgZnVuY3Rpb24gb25DbGljaygpIHsKICAgIHNldFRpbWVvdXQoZnVuY3Rpb24gdGltZXIoKSB7CiAgICAgICAgY29uc29sZS5sb2coJ1lvdSBjbGlja2VkIHRoZSBidXR0b24hJyk7ICAgIAogICAgfSwgMjAwMCk7Cn0pOwoKY29uc29sZS5sb2coIkhpISIpOwoKc2V0VGltZW91dChmdW5jdGlvbiB0aW1lb3V0KCkgewogICAgY29uc29sZS5sb2coIkNsaWNrIHRoZSBidXR0b24hIik7Cn0sIDUwMDApOwoKY29uc29sZS5sb2coIldlbGNvbWUgdG8gbG91cGUuIik7!!!PGJ1dHRvbj5DbGljayBtZSE8L2J1dHRvbj4%3D"); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /templates.js: -------------------------------------------------------------------------------- 1 | var multiline = require('multiline'); 2 | 3 | exports.timeoutItem = multiline(function () {/* 4 |
  • 5 | 6 | 7 |
  • 8 | */}); 9 | 10 | exports.timeoutList = multiline(function () {/* 11 | 12 | */}); 13 | 14 | exports.stackFrame = multiline(function () {/* 15 |
  • 16 | 17 |
  • 18 | */}); 19 | 20 | exports.stackFrameList = multiline(function () {/* 21 | 22 | */}); 23 | 24 | exports.mainView = multiline(function () {/* 25 |
    26 |
    27 |
    28 |
    29 |
    30 |
    31 | */}); 32 | 33 | exports.code = multiline(function () {/* 34 |
    35 |
    36 | console.log(console.log('hi' + 2)); 37 | var a = console.log('hi'); 38 | [1,2,3].map(function (a) { 39 | console.log(a * 2); 40 | }); 41 | console.log(a + a + a); 42 | var b = 2 + 2 + 2; 43 |
    44 |
    45 | */}); 46 | 47 | exports.code = multiline(function () {/* 48 |
    49 |
    function baz () { 50 | console.log('bar'); 51 | } 52 | function bar () { 53 | baz(); 54 | } 55 | function foo () { 56 | bar(); 57 | } 58 | 59 | 60 | setTimeout(function () { 61 | foo(); 62 | }, 2000); 63 | 64 | setTimeout(function () { 65 | foo(); 66 | }, 1000); 67 | 68 | setTimeout(function () { 69 | foo(); 70 | }, 3000); 71 |
    72 |
    73 | */}); 74 | -------------------------------------------------------------------------------- /tests/text-cursor-test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | 3 | var TextCursor = require('../text-cursor'); 4 | 5 | var code = [ 6 | "a123456789", 7 | "b123456", 8 | "", 9 | "c12345" 10 | ].join("\n"); 11 | 12 | 13 | test('within line: one at a time', function (t) { 14 | var cursor = new TextCursor(code); 15 | t.equal(cursor.stepTo(1,1), 'a'); 16 | t.equal(cursor.stepTo(1,2), '1'); 17 | t.equal(cursor.stepTo(1,3), '2'); 18 | t.end(); 19 | }); 20 | 21 | test('within line: many at a time', function (t) { 22 | var cursor = new TextCursor(code); 23 | t.equal(cursor.stepTo(1,4), 'a123'); 24 | t.equal(cursor.stepTo(1,5), '4'); 25 | t.equal(cursor.stepTo(1,8), '567'); 26 | t.end(); 27 | }); 28 | 29 | 30 | test('within line: to the end of the line', function (t) { 31 | var cursor = new TextCursor(code); 32 | t.equal(cursor.stepTo(1,10), 'a123456789\n'); 33 | t.end(); 34 | }); 35 | 36 | test('within line: going beyond end of line', function (t) { 37 | var cursor = new TextCursor(code); 38 | t.equal(cursor.stepTo(1,50), 'a123456789\n'); 39 | t.end(); 40 | }); 41 | 42 | test('across lines: to start of next line', function (t) { 43 | var cursor = new TextCursor(code); 44 | t.equal(cursor.stepTo(2,0), 'a123456789\n'); 45 | t.end(); 46 | }); 47 | --------------------------------------------------------------------------------