├── README.md ├── assets └── index.css ├── index.html ├── index.js ├── lib ├── analyzer.js ├── cm-js.js └── editor.js └── package.json /README.md: -------------------------------------------------------------------------------- 1 | # essim 2 | 3 | a mostly-for-funsies live JS visualizer. there are bugs. 4 | 5 | [check out the example here](http://didact.us/essim) 6 | 7 | ## License 8 | 9 | MIT 10 | -------------------------------------------------------------------------------- /assets/index.css: -------------------------------------------------------------------------------- 1 | /* swiped wholesale from hughsk's wonderful work on glslbin! */ 2 | html,body,div,span,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,abbr,address,cite,code,del,dfn,em,img,ins,kbd,q,samp,small,strong,sub,sup,var,b,i,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,figcaption,figure,footer,header,hgroup,menu,nav,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;outline:0;font-size:100%;vertical-align:baseline;background:0 0}body{line-height:1}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}nav ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:'';content:none}a{margin:0;padding:0;font-size:100%;vertical-align:baseline;background:0 0}ins{text-decoration:none}ins,mark{background-color:#ff9;color:#000}mark{font-style:italic;font-weight:700}del{text-decoration:line-through}abbr[title],dfn[title]{border-bottom:1px dotted;cursor:help}table{border-collapse:collapse;border-spacing:0}hr{display:block;height:1px;border:0;border-top:1px solid #ccc;margin:1em 0;padding:0}input,select{vertical-align:middle} 3 | .CodeMirror{font-family:'Fantasque Sans Mono',monospace;height:300px;color:#000}.CodeMirror-lines{padding:4px 0}.CodeMirror pre{padding:0 4px}.CodeMirror-scrollbar-filler,.CodeMirror-gutter-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror div.CodeMirror-cursor{border-left:1px solid #000}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.CodeMirror.cm-fat-cursor div.CodeMirror-cursor{width:auto;border:0;background:#7e7}.CodeMirror.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-animate-fat-cursor{width:auto;border:0;-webkit-animation:blink 1.06s steps(1)infinite;-moz-animation:blink 1.06s steps(1)infinite;animation:blink 1.06s steps(1)infinite}@-moz-keyframes blink{0%{background:#7e7}50%{background:0 0}100%{background:#7e7}}@-webkit-keyframes blink{0%{background:#7e7}50%{background:0 0}100%{background:#7e7}}@keyframes blink{0%{background:#7e7}50%{background:0 0}100%{background:#7e7}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-ruler{border-left:1px solid #ccc;position:absolute}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-variable-3{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta,.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-error,.cm-invalidchar{color:red}div.CodeMirror span.CodeMirror-matchingbracket{color:#0f0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#f22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:#fff}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-30px;margin-right:-30px;padding-bottom:30px;height:100%;outline:none}.CodeMirror-scroll,.CodeMirror-sizer{position:relative;-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-sizer{border-right:30px solid transparent}.CodeMirror-vscrollbar,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-gutter-filler{position:absolute;z-index:6;display:none}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;-moz-box-sizing:content-box;box-sizing:content-box;display:inline-block;margin-bottom:-30px;*zoom:1;*display:inline}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;height:100%}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-gutter-wrapper{-webkit-user-select:none;-moz-user-select:none;user-select:none}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:0 0;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent}.CodeMirror-wrap pre{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;overflow:auto}.CodeMirror-code{outline:none}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-measure pre{position:static}.CodeMirror div.CodeMirror-cursor{position:absolute;border-right:none;width:0}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}.CodeMirror-focused div.CodeMirror-cursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror ::selection{background:#d7d4f0}.CodeMirror ::-moz-selection{background:#d7d4f0}.cm-searching{background:#ffa;background:rgba(255,255,0,.4)}.CodeMirror span{*vertical-align:text-bottom}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:''}span.CodeMirror-selectedtext{background:0 0} 4 | 5 | /* 6 | Original theme by: Zeno Rocha 7 | Brackets theme adaptation by: Felipe KM 8 | Statusbar, Toolbar, Sidebar css added by: Rafael Artoo Derolez 9 | */ 10 | 11 | .cm-s-dracula .CodeMirror-scroll{ 12 | margin-right: 5px; 13 | border-color: #202020; 14 | } 15 | .cm-s-dracula .CodeMirror-vscrollbar{ 16 | overflow-y: hidden; 17 | } 18 | .cm-s-dracula ::-webkit-scrollbar { 19 | width: 10px; 20 | } 21 | .cm-s-dracula ::-webkit-scrollbar-track { 22 | background: transparent; 23 | } 24 | .cm-s-dracula ::-webkit-scrollbar-thumb { 25 | background: #383a48; 26 | border-radius: 5px; 27 | } 28 | .cm-s-dracula ::-webkit-scrollbar-thumb:hover{ 29 | background: #444756; 30 | } 31 | .cm-s-dracula ::-webkit-scrollbar-thumb:active{ 32 | background: #5d6073; 33 | } 34 | 35 | .cm-s-dracula.CodeMirror { 36 | background: #34363B; 37 | color: #EEE; 38 | font-size: 14px; 39 | line-height: 1.5em; 40 | } 41 | 42 | .cm-s-dracula div.CodeMirror-cursor { 43 | border-left: 1px solid #f8f8f0; 44 | z-index: 3; 45 | } 46 | 47 | .CodeMirror-selected { 48 | background: #3d3d3d; 49 | opacity: .23; 50 | } 51 | 52 | .cm-s-dracula span.cm-keyword {color: #66C4FF;} 53 | .cm-s-dracula span.cm-atom {color: #FFE169;} 54 | .cm-s-dracula span.cm-number {color: #FFE169;} 55 | .cm-s-dracula span.cm-def {color: #ffb86c;} 56 | .cm-s-dracula span.cm-variable {color: #EEE;} 57 | .cm-s-dracula span.cm-variable-2 {color: #FFE169;} 58 | .cm-s-dracula span.cm-property {color: #8be9fd;} 59 | .cm-s-dracula span.cm-operator {color: #66C4FF;} 60 | .cm-s-dracula span.cm-comment {color: #A9B0C2;} 61 | .cm-s-dracula span.cm-string {color: #FFE169;} 62 | .cm-s-dracula span.cm-string-2 {color: #FFE169;} 63 | .cm-s-dracula span.cm-meta {color: #61FF90;} 64 | .cm-s-dracula span.cm-qualifier, .CodeMirror span.cm-builtin {color: #61FF90;} 65 | .cm-s-dracula span.cm-bracket {color: #f9faf4;} 66 | .cm-s-dracula span.cm-tag {color: #66C4FF !important;} 67 | .cm-s-dracula span.cm-attribute {color: #61FF90;} 68 | .cm-s-dracula span.cm-matchhighlight {background-color: rgba(68,71,90.23);} 69 | 70 | .cm-s-dracula span.CodeMirror-matchingbracket { 71 | color: #61FF90; 72 | font-weight: bold; 73 | } 74 | 75 | .cm-s-dracula span.CodeMirror-matchingtag { 76 | color: #61FF90 !important; 77 | text-decoration:underline; 78 | background:none; 79 | } 80 | 81 | 82 | .cm-s-dracula span.CodeMirror-searching { 83 | background-color: none; 84 | background: none; 85 | box-shadow: 0 0 0 1px #fff; 86 | } 87 | 88 | .cm-s-dracula .CodeMirror-gutters { 89 | background: #34363B; 90 | border-right: 0.5rem solid #34363B; 91 | } 92 | 93 | .cm-s-dracula .CodeMirror-linenumber { 94 | color: #5B6173; 95 | } 96 | 97 | /** 98 | * Editor Styles 99 | */ 100 | .editor { 101 | position: fixed; 102 | top: 0; right: 0; left: 50%; 103 | bottom: 0; 104 | } 105 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const analyze = require('./lib/analyzer.js') 2 | const initializeEditor = require('./lib/editor.js') 3 | const graphlibDot = require('graphlib-dot') 4 | const d3dagre = require('dagre-d3') 5 | const through = require('through') 6 | const d3 = require('d3') 7 | 8 | initializeEditor(document.body) 9 | .pipe(through(oncode)) 10 | 11 | const renderFlow = d3dagre.render() 12 | const renderObject = d3dagre.render() 13 | const svg = d3.select('body').append('svg') 14 | const playPause = document.createElement('button') 15 | let playing = false 16 | let disabled = false 17 | let lastGoodCode = null 18 | let objectGraphStream = null 19 | 20 | playPause.setAttribute('id', 'play-pause') 21 | playPause.textContent = ' ' 22 | playPause.addEventListener('click', onclickbtn) 23 | document.body.appendChild(playPause) 24 | 25 | const zoomLayer = 26 | svg 27 | .attr('width', '50%') 28 | .attr('height', window.innerHeight) 29 | .append('g') 30 | .call(d3.behavior.zoom().scaleExtent([0.125, 1.25]).on('zoom', onzoom)) 31 | 32 | zoomLayer 33 | .append('rect') 34 | .attr('width', '100%') 35 | .attr('height', '100%') 36 | .attr('fill', '#666676') 37 | 38 | const flowGraph = 39 | zoomLayer 40 | .append('g') 41 | .attr('id', 'flow-graph') 42 | 43 | const objectGraph = 44 | zoomLayer 45 | .append('g') 46 | .attr('id', 'object-graph') 47 | 48 | window.onresize = function() { 49 | svg.attr('height', window.innerHeight) 50 | } 51 | 52 | function oncode(code) { 53 | if (objectGraphStream) { 54 | objectGraphStream.stop() 55 | objectGraphStream = null 56 | } 57 | analyze.createCFG(code, function(err, cfg) { 58 | if (err) { 59 | document.body.classList.add('play-disabled') 60 | disabled = true 61 | return 62 | } 63 | disabled = false 64 | document.body.classList.remove('play-disabled') 65 | lastGoodCode = code 66 | const results = graphlibDot.read(cfg.toDot()) 67 | if (!results.graph().hasOwnProperty('marginx') && 68 | !results.graph().hasOwnProperty('marginy')) { 69 | results.graph().marginx = 20 70 | results.graph().marginy = 20 71 | } 72 | 73 | results.graph().transition = function(selection) { 74 | return selection.transition().duration(500) 75 | } 76 | 77 | flowGraph.call(renderFlow, results) 78 | }) 79 | } 80 | 81 | function onzoom() { 82 | flowGraph.attr( 83 | 'transform', 84 | `translate(${d3.event.translate})scale(${d3.event.scale})` 85 | ) 86 | objectGraph.attr( 87 | 'transform', 88 | `translate(${d3.event.translate})scale(${d3.event.scale})` 89 | ) 90 | } 91 | 92 | function onclickbtn(ev) { 93 | ev.preventDefault() 94 | if (disabled) { 95 | return 96 | } 97 | playing = !playing 98 | if (!playing) { 99 | objectGraph.selectAll('*').remove() 100 | document.body.classList.remove('playing') 101 | return 102 | } else { 103 | document.body.classList.add('playing') 104 | } 105 | 106 | objectGraphStream = analyze.createObjectGraph(lastGoodCode) 107 | objectGraph.selectAll('*').remove() 108 | 109 | objectGraphStream.on('data', function({vertices, edges, builtins, root}) { 110 | const output = ['digraph {'] 111 | let ID = 1; 112 | const mapping = new Map() 113 | for (const vertex of vertices) { 114 | const id = ID++ 115 | mapping.set(vertex, id) 116 | output.push(id + ` [label=${ 117 | JSON.stringify( 118 | vertex.stack || vertex.toparent ? 'Stack Item' : 119 | vertex.root ? 'Root' : 120 | vertex.classInfo ? vertex.classInfo() : 121 | vertex.getName ? 'name: ' + vertex.getName() : '???' 122 | ) 123 | }]`) 124 | } 125 | for (const edge of edges) { 126 | if (!mapping.get(edge[0]) || !mapping.get(edge[1])) { 127 | continue 128 | } 129 | 130 | output.push(`${mapping.get(edge[0])} -> ${mapping.get(edge[1])} [label=${ 131 | JSON.stringify(edge[2]) 132 | }]`) 133 | } 134 | output.push('}') 135 | const results = graphlibDot.read(output.join('\n')) 136 | if (!results.graph().hasOwnProperty('marginx') && 137 | !results.graph().hasOwnProperty('marginy')) { 138 | results.graph().marginx = 20 139 | results.graph().marginy = 20 140 | } 141 | 142 | results.graph().transition = function(selection) { 143 | return selection.transition().duration(500) 144 | } 145 | 146 | objectGraph.call(renderObject, results) 147 | 148 | }) 149 | } 150 | -------------------------------------------------------------------------------- /lib/analyzer.js: -------------------------------------------------------------------------------- 1 | module.exports = {createCFG, createObjectGraph} 2 | 3 | const escontrol = require('escontrol') 4 | const through = require('through') 5 | const espree = require('espree') 6 | const d3 = require('d3') 7 | 8 | const parseOpts = { 9 | loc: true, 10 | ecmaFeatures: { 11 | arrowFunctions: true, 12 | blockBindings: true, 13 | destructuring: true, 14 | regexYFlag: true, 15 | regexUFlag: true, 16 | templateStrings: true, 17 | binaryLiterals: true, 18 | octalLiterals: true, 19 | unicodeCodePointEscapes: true, 20 | defaultParams: true, 21 | restParams: true, 22 | forOf: true, 23 | objectLiteralComputedProperties: true, 24 | objectLiteralShorthandMethods: true, 25 | objectLiteralShorthandProperties: true, 26 | objectLiteralDuplicateProperties: true, 27 | generators: true, 28 | spread: true, 29 | classes: true, 30 | modules: false, 31 | jsx: false, 32 | globalReturn: true 33 | } 34 | } 35 | 36 | function createCFG(code, ready) { 37 | let cfg = null 38 | try { 39 | cfg = escontrol(espree.parse(code, parseOpts)) 40 | } catch(err) { 41 | return ready(err) 42 | } 43 | 44 | return iterate() 45 | 46 | function iterate() { 47 | try { 48 | let times = 0 49 | while (cfg.advance()) { 50 | if (++times > 1000) { 51 | return setTimeout(iterate) 52 | } 53 | } 54 | return ready(null, cfg) 55 | } catch(err) { 56 | return ready(err) 57 | } 58 | } 59 | } 60 | 61 | function createObjectGraph(code) { 62 | let cfg = null 63 | const stream = through() 64 | const vertices = new Set() 65 | const edges = new Set() 66 | const fromVia = new WeakMap() 67 | let cancelled = false 68 | let stack = {stack: true} 69 | let root = {root: true} 70 | let isBuiltin = true 71 | let sawChange = true 72 | let sawGCEvent = false 73 | 74 | fromVia.set(root, new Map()) 75 | fromVia.set(stack, new Map()) 76 | const builtins = new Set() 77 | try { 78 | cfg = escontrol(espree.parse(code, parseOpts), { 79 | onvalue : onvalue, 80 | onpushvalue : onpushvalue, 81 | onpopvalue : onpopvalue, 82 | onlink : onlink, 83 | onunlink : onunlink, 84 | oncalled : oncalled 85 | }) 86 | } catch(err) { 87 | setTimeout(_ => stream.emit('error', err)) 88 | } 89 | isBuiltin = false 90 | vertices.add(stack) 91 | vertices.add(root) 92 | 93 | setTimeout(iterate) 94 | 95 | stream.stop = stop 96 | cfg.builtins().getprop('[[ArrayProto]]').value().classInfo = _ => '[[ArrayProto]]' 97 | cfg.builtins().getprop('[[ObjectProto]]').value().classInfo = _ => '[[ObjectProto]]' 98 | cfg.builtins().getprop('[[StringProto]]').value().classInfo = _ => '[[StringProto]]' 99 | cfg.builtins().getprop('[[RegExpProto]]').value().classInfo = _ => '[[RegExpProto]]' 100 | cfg.builtins().getprop('[[NumberProto]]').value().classInfo = _ => '[[NumberProto]]' 101 | cfg.builtins().getprop('[[FunctionProto]]').value().classInfo = _ => '[[FunctionProto]]' 102 | cfg.global().classInfo = _ => 'Global Scope' 103 | return stream 104 | 105 | function iterate() { 106 | if (cancelled) return 107 | sawChange = false 108 | while (cfg.advance()) { 109 | if (sawChange) { 110 | if (sawGCEvent) gc(cfg.global()) 111 | stream.queue({vertices, edges}) 112 | return setTimeout(iterate, 1000) 113 | } 114 | } 115 | 116 | stream.queue({vertices, edges, builtins}) 117 | stream.queue(null) 118 | } 119 | 120 | function stop() { 121 | cancelled = true 122 | } 123 | 124 | function onvalue(value) { 125 | sawChange = true 126 | fromVia.set(value, new Map()) 127 | if (isBuiltin) { 128 | builtins.add(value) 129 | return 130 | } 131 | vertices.add(value) 132 | if (value.isEither()) { 133 | for (const xs of value._outcomes) { 134 | onvalue(xs) 135 | onlink(value, xs, '[[maybe]]') 136 | } 137 | } 138 | } 139 | 140 | function onpushvalue(value) { 141 | sawChange = true 142 | let next = {parent: stack, value: value, toparent: null, tovalue: null} 143 | next.toparent = [stack, next, ''] 144 | next.tovalue = [next, value, ''] 145 | edges.add(next.toparent) 146 | edges.add(next.tovalue) 147 | vertices.add(next) 148 | stack = next 149 | } 150 | 151 | function onpopvalue(value) { 152 | sawChange = true 153 | let prev = stack 154 | if (!stack.parent) { 155 | throw new Error('cannot pop') 156 | } 157 | stack = stack.parent 158 | edges.delete(prev.toparent) 159 | edges.delete(prev.tovalue) 160 | vertices.delete(prev) 161 | } 162 | 163 | function onlink(from, to, via) { 164 | sawChange = true 165 | from = from || root 166 | to = to || root 167 | if (isBuiltin) { 168 | if (from.root || to.root) { 169 | vertices.add(from) 170 | vertices.add(to) 171 | } 172 | } else { 173 | if (builtins.has(from) || builtins.has(to)) { 174 | vertices.add(from) 175 | vertices.add(to) 176 | } 177 | } 178 | let tuple = [from, to, via] 179 | fromVia.get(from).set(via, tuple) 180 | edges.add(tuple) 181 | } 182 | 183 | function oncalled() { 184 | sawGCEvent = new Error().stack 185 | } 186 | 187 | function onunlink(from, to, via) { 188 | sawChange = true 189 | from = from || root 190 | to = to || root 191 | edges.delete(fromVia.get(from).get(via)) 192 | fromVia.get(from).delete(via) 193 | } 194 | 195 | function gc(root) { 196 | const trace = sawGCEvent 197 | sawGCEvent = false 198 | 199 | const seenValues = new Set() 200 | const marked = new WeakSet() 201 | for (const obj of iterateObjects(root, seenValues)) { 202 | marked.add(obj) 203 | } 204 | let currentStack = stack 205 | while (currentStack) { 206 | if (currentStack.value && currentStack.value.names) { 207 | const seenValues = new Set() 208 | for (const obj of iterateObjects(currentStack.value, seenValues)) { 209 | marked.add(obj) 210 | } 211 | } 212 | currentStack = currentStack.parent 213 | } 214 | marked.add(root) 215 | for (const obj of vertices) { 216 | if (!marked.has(obj) && !obj.stack && !obj.toparent) { 217 | console.log('DELETE obj', obj, cfg.stackInfo(), trace) 218 | vertices.delete(obj) 219 | } 220 | } 221 | } 222 | 223 | function iterateObjects(obj, seenValues) { 224 | const stack = [obj.names()] 225 | seenValues.add(null) 226 | return { 227 | [Symbol.iterator]() { 228 | return this 229 | }, 230 | next() { 231 | for (const name of stack[0]) { 232 | let value = name 233 | if (name.value) { 234 | value = name.value() 235 | } 236 | if (seenValues.has(value)) { 237 | continue 238 | } 239 | seenValues.add(value) 240 | stack.unshift(value.isEither() ? value._outcomes.values() : value.names()) 241 | return { 242 | value: value, 243 | done: false 244 | } 245 | } 246 | stack.shift() 247 | if (!stack.length) { 248 | return { 249 | done: true, 250 | value: undefined 251 | } 252 | } 253 | return this.next() 254 | } 255 | } 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /lib/cm-js.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function (CodeMirror) { 3 | "use strict"; 4 | 5 | CodeMirror.defineMode("javascript", function(config, parserConfig) { 6 | var indentUnit = config.indentUnit; 7 | var statementIndent = parserConfig.statementIndent; 8 | var jsonldMode = parserConfig.jsonld; 9 | var jsonMode = parserConfig.json || jsonldMode; 10 | var isTS = parserConfig.typescript; 11 | var wordRE = parserConfig.wordCharacters || /[\w$\xa1-\uffff]/; 12 | 13 | // Tokenizer 14 | 15 | var keywords = function(){ 16 | function kw(type) {return {type: type, style: "keyword"};} 17 | var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c"); 18 | var operator = kw("operator"), atom = {type: "atom", style: "atom"}; 19 | 20 | var jsKeywords = { 21 | "if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B, 22 | "return": C, "break": C, "continue": C, "new": C, "delete": C, "throw": C, "debugger": C, 23 | "var": kw("var"), "const": kw("var"), "let": kw("var"), 24 | "function": kw("function"), "catch": kw("catch"), 25 | "for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"), 26 | "in": operator, "typeof": operator, "instanceof": operator, 27 | "true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom, 28 | "this": kw("this"), "module": kw("module"), "class": kw("class"), "super": kw("atom"), 29 | "yield": C, "export": kw("export"), "import": kw("import"), "extends": C 30 | }; 31 | 32 | // Extend the 'normal' keywords with the TypeScript language extensions 33 | if (isTS) { 34 | var type = {type: "variable", style: "variable-3"}; 35 | var tsKeywords = { 36 | // object-like things 37 | "interface": kw("interface"), 38 | "extends": kw("extends"), 39 | "constructor": kw("constructor"), 40 | 41 | // scope modifiers 42 | "public": kw("public"), 43 | "private": kw("private"), 44 | "protected": kw("protected"), 45 | "static": kw("static"), 46 | 47 | // types 48 | "string": type, "number": type, "bool": type, "any": type 49 | }; 50 | 51 | for (var attr in tsKeywords) { 52 | jsKeywords[attr] = tsKeywords[attr]; 53 | } 54 | } 55 | 56 | return jsKeywords; 57 | }(); 58 | 59 | var isOperatorChar = /[+\-*&%=<>!?|~^]/; 60 | var isJsonldKeyword = /^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/; 61 | 62 | function readRegexp(stream) { 63 | var escaped = false, next, inSet = false; 64 | while ((next = stream.next()) != null) { 65 | if (!escaped) { 66 | if (next == "/" && !inSet) return; 67 | if (next == "[") inSet = true; 68 | else if (inSet && next == "]") inSet = false; 69 | } 70 | escaped = !escaped && next == "\\"; 71 | } 72 | } 73 | 74 | // Used as scratch variables to communicate multiple values without 75 | // consing up tons of objects. 76 | var type, content; 77 | function ret(tp, style, cont) { 78 | type = tp; content = cont; 79 | return style; 80 | } 81 | function tokenBase(stream, state) { 82 | var ch = stream.next(); 83 | if (ch == '"' || ch == "'") { 84 | state.tokenize = tokenString(ch); 85 | return state.tokenize(stream, state); 86 | } else if (ch == "." && stream.match(/^\d+(?:[eE][+\-]?\d+)?/)) { 87 | return ret("number", "number"); 88 | } else if (ch == "." && stream.match("..")) { 89 | return ret("spread", "meta"); 90 | } else if (/[\[\]{}\(\),;\:\.]/.test(ch)) { 91 | return ret(ch); 92 | } else if (ch == "=" && stream.eat(">")) { 93 | return ret("=>", "operator"); 94 | } else if (ch == "0" && stream.eat(/x/i)) { 95 | stream.eatWhile(/[\da-f]/i); 96 | return ret("number", "number"); 97 | } else if (/\d/.test(ch)) { 98 | stream.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/); 99 | return ret("number", "number"); 100 | } else if (ch == "/") { 101 | if (stream.eat("*")) { 102 | state.tokenize = tokenComment; 103 | return tokenComment(stream, state); 104 | } else if (stream.eat("/")) { 105 | stream.skipToEnd(); 106 | return ret("comment", "comment"); 107 | } else if (state.lastType == "operator" || state.lastType == "keyword c" || 108 | state.lastType == "sof" || /^[\[{}\(,;:]$/.test(state.lastType)) { 109 | readRegexp(stream); 110 | stream.match(/^\b(([gimyu])(?![gimyu]*\2))+\b/); 111 | return ret("regexp", "string-2"); 112 | } else { 113 | stream.eatWhile(isOperatorChar); 114 | return ret("operator", "operator", stream.current()); 115 | } 116 | } else if (ch == "`") { 117 | state.tokenize = tokenQuasi; 118 | return tokenQuasi(stream, state); 119 | } else if (ch == "#") { 120 | stream.skipToEnd(); 121 | return ret("error", "error"); 122 | } else if (isOperatorChar.test(ch)) { 123 | stream.eatWhile(isOperatorChar); 124 | return ret("operator", "operator", stream.current()); 125 | } else if (wordRE.test(ch)) { 126 | stream.eatWhile(wordRE); 127 | var word = stream.current(), known = keywords.propertyIsEnumerable(word) && keywords[word]; 128 | return (known && state.lastType != ".") ? ret(known.type, known.style, word) : 129 | ret("variable", "variable", word); 130 | } 131 | } 132 | 133 | function tokenString(quote) { 134 | return function(stream, state) { 135 | var escaped = false, next; 136 | if (jsonldMode && stream.peek() == "@" && stream.match(isJsonldKeyword)){ 137 | state.tokenize = tokenBase; 138 | return ret("jsonld-keyword", "meta"); 139 | } 140 | while ((next = stream.next()) != null) { 141 | if (next == quote && !escaped) break; 142 | escaped = !escaped && next == "\\"; 143 | } 144 | if (!escaped) state.tokenize = tokenBase; 145 | return ret("string", "string"); 146 | }; 147 | } 148 | 149 | function tokenComment(stream, state) { 150 | var maybeEnd = false, ch; 151 | while (ch = stream.next()) { 152 | if (ch == "/" && maybeEnd) { 153 | state.tokenize = tokenBase; 154 | break; 155 | } 156 | maybeEnd = (ch == "*"); 157 | } 158 | return ret("comment", "comment"); 159 | } 160 | 161 | function tokenQuasi(stream, state) { 162 | var escaped = false, next; 163 | while ((next = stream.next()) != null) { 164 | if (!escaped && (next == "`" || next == "$" && stream.eat("{"))) { 165 | state.tokenize = tokenBase; 166 | break; 167 | } 168 | escaped = !escaped && next == "\\"; 169 | } 170 | return ret("quasi", "string-2", stream.current()); 171 | } 172 | 173 | var brackets = "([{}])"; 174 | // This is a crude lookahead trick to try and notice that we're 175 | // parsing the argument patterns for a fat-arrow function before we 176 | // actually hit the arrow token. It only works if the arrow is on 177 | // the same line as the arguments and there's no strange noise 178 | // (comments) in between. Fallback is to only notice when we hit the 179 | // arrow, and not declare the arguments as locals for the arrow 180 | // body. 181 | function findFatArrow(stream, state) { 182 | if (state.fatArrowAt) state.fatArrowAt = null; 183 | var arrow = stream.string.indexOf("=>", stream.start); 184 | if (arrow < 0) return; 185 | 186 | var depth = 0, sawSomething = false; 187 | for (var pos = arrow - 1; pos >= 0; --pos) { 188 | var ch = stream.string.charAt(pos); 189 | var bracket = brackets.indexOf(ch); 190 | if (bracket >= 0 && bracket < 3) { 191 | if (!depth) { ++pos; break; } 192 | if (--depth == 0) break; 193 | } else if (bracket >= 3 && bracket < 6) { 194 | ++depth; 195 | } else if (wordRE.test(ch)) { 196 | sawSomething = true; 197 | } else if (/["'\/]/.test(ch)) { 198 | return; 199 | } else if (sawSomething && !depth) { 200 | ++pos; 201 | break; 202 | } 203 | } 204 | if (sawSomething && !depth) state.fatArrowAt = pos; 205 | } 206 | 207 | // Parser 208 | 209 | var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, "regexp": true, "this": true, "jsonld-keyword": true}; 210 | 211 | function JSLexical(indented, column, type, align, prev, info) { 212 | this.indented = indented; 213 | this.column = column; 214 | this.type = type; 215 | this.prev = prev; 216 | this.info = info; 217 | if (align != null) this.align = align; 218 | } 219 | 220 | function inScope(state, varname) { 221 | for (var v = state.localVars; v; v = v.next) 222 | if (v.name == varname) return true; 223 | for (var cx = state.context; cx; cx = cx.prev) { 224 | for (var v = cx.vars; v; v = v.next) 225 | if (v.name == varname) return true; 226 | } 227 | } 228 | 229 | function parseJS(state, style, type, content, stream) { 230 | var cc = state.cc; 231 | // Communicate our context to the combinators. 232 | // (Less wasteful than consing up a hundred closures on every call.) 233 | cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; cx.style = style; 234 | 235 | if (!state.lexical.hasOwnProperty("align")) 236 | state.lexical.align = true; 237 | 238 | while(true) { 239 | var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement; 240 | if (combinator(type, content)) { 241 | while(cc.length && cc[cc.length - 1].lex) 242 | cc.pop()(); 243 | if (cx.marked) return cx.marked; 244 | if (type == "variable" && inScope(state, content)) return "variable-2"; 245 | return style; 246 | } 247 | } 248 | } 249 | 250 | // Combinator utils 251 | 252 | var cx = {state: null, column: null, marked: null, cc: null}; 253 | function pass() { 254 | for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]); 255 | } 256 | function cont() { 257 | pass.apply(null, arguments); 258 | return true; 259 | } 260 | function register(varname) { 261 | function inList(list) { 262 | for (var v = list; v; v = v.next) 263 | if (v.name == varname) return true; 264 | return false; 265 | } 266 | var state = cx.state; 267 | if (state.context) { 268 | cx.marked = "def"; 269 | if (inList(state.localVars)) return; 270 | state.localVars = {name: varname, next: state.localVars}; 271 | } else { 272 | if (inList(state.globalVars)) return; 273 | if (parserConfig.globalVars) 274 | state.globalVars = {name: varname, next: state.globalVars}; 275 | } 276 | } 277 | 278 | // Combinators 279 | 280 | var defaultVars = {name: "this", next: {name: "arguments"}}; 281 | function pushcontext() { 282 | cx.state.context = {prev: cx.state.context, vars: cx.state.localVars}; 283 | cx.state.localVars = defaultVars; 284 | } 285 | function popcontext() { 286 | cx.state.localVars = cx.state.context.vars; 287 | cx.state.context = cx.state.context.prev; 288 | } 289 | function pushlex(type, info) { 290 | var result = function() { 291 | var state = cx.state, indent = state.indented; 292 | if (state.lexical.type == "stat") indent = state.lexical.indented; 293 | else for (var outer = state.lexical; outer && outer.type == ")" && outer.align; outer = outer.prev) 294 | indent = outer.indented; 295 | state.lexical = new JSLexical(indent, cx.stream.column(), type, null, state.lexical, info); 296 | }; 297 | result.lex = true; 298 | return result; 299 | } 300 | function poplex() { 301 | var state = cx.state; 302 | if (state.lexical.prev) { 303 | if (state.lexical.type == ")") 304 | state.indented = state.lexical.indented; 305 | state.lexical = state.lexical.prev; 306 | } 307 | } 308 | poplex.lex = true; 309 | 310 | function expect(wanted) { 311 | function exp(type) { 312 | if (type == wanted) return cont(); 313 | else if (wanted == ";") return pass(); 314 | else return cont(exp); 315 | }; 316 | return exp; 317 | } 318 | 319 | function statement(type, value) { 320 | if (type == "var") return cont(pushlex("vardef", value.length), vardef, expect(";"), poplex); 321 | if (type == "keyword a") return cont(pushlex("form"), expression, statement, poplex); 322 | if (type == "keyword b") return cont(pushlex("form"), statement, poplex); 323 | if (type == "{") return cont(pushlex("}"), block, poplex); 324 | if (type == ";") return cont(); 325 | if (type == "if") { 326 | if (cx.state.lexical.info == "else" && cx.state.cc[cx.state.cc.length - 1] == poplex) 327 | cx.state.cc.pop()(); 328 | return cont(pushlex("form"), expression, statement, poplex, maybeelse); 329 | } 330 | if (type == "function") return cont(functiondef); 331 | if (type == "for") return cont(pushlex("form"), forspec, statement, poplex); 332 | if (type == "variable") return cont(pushlex("stat"), maybelabel); 333 | if (type == "switch") return cont(pushlex("form"), expression, pushlex("}", "switch"), expect("{"), 334 | block, poplex, poplex); 335 | if (type == "case") return cont(expression, expect(":")); 336 | if (type == "default") return cont(expect(":")); 337 | if (type == "catch") return cont(pushlex("form"), pushcontext, expect("("), funarg, expect(")"), 338 | statement, poplex, popcontext); 339 | if (type == "module") return cont(pushlex("form"), pushcontext, afterModule, popcontext, poplex); 340 | if (type == "class") return cont(pushlex("form"), className, poplex); 341 | if (type == "export") return cont(pushlex("form"), afterExport, poplex); 342 | if (type == "import") return cont(pushlex("form"), afterImport, poplex); 343 | return pass(pushlex("stat"), expression, expect(";"), poplex); 344 | } 345 | function expression(type) { 346 | return expressionInner(type, false); 347 | } 348 | function expressionNoComma(type) { 349 | return expressionInner(type, true); 350 | } 351 | function expressionInner(type, noComma) { 352 | if (cx.state.fatArrowAt == cx.stream.start) { 353 | var body = noComma ? arrowBodyNoComma : arrowBody; 354 | if (type == "(") return cont(pushcontext, pushlex(")"), commasep(pattern, ")"), poplex, expect("=>"), body, popcontext); 355 | else if (type == "variable") return pass(pushcontext, pattern, expect("=>"), body, popcontext); 356 | } 357 | 358 | var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma; 359 | if (atomicTypes.hasOwnProperty(type)) return cont(maybeop); 360 | if (type == "function") return cont(functiondef, maybeop); 361 | if (type == "keyword c") return cont(noComma ? maybeexpressionNoComma : maybeexpression); 362 | if (type == "(") return cont(pushlex(")"), maybeexpression, comprehension, expect(")"), poplex, maybeop); 363 | if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression); 364 | if (type == "[") return cont(pushlex("]"), arrayLiteral, poplex, maybeop); 365 | if (type == "{") return contCommasep(objprop, "}", null, maybeop); 366 | if (type == "quasi") { return pass(quasi, maybeop); } 367 | return cont(); 368 | } 369 | function maybeexpression(type) { 370 | if (type.match(/[;\}\)\],]/)) return pass(); 371 | return pass(expression); 372 | } 373 | function maybeexpressionNoComma(type) { 374 | if (type.match(/[;\}\)\],]/)) return pass(); 375 | return pass(expressionNoComma); 376 | } 377 | 378 | function maybeoperatorComma(type, value) { 379 | if (type == ",") return cont(expression); 380 | return maybeoperatorNoComma(type, value, false); 381 | } 382 | function maybeoperatorNoComma(type, value, noComma) { 383 | var me = noComma == false ? maybeoperatorComma : maybeoperatorNoComma; 384 | var expr = noComma == false ? expression : expressionNoComma; 385 | if (type == "=>") return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext); 386 | if (type == "operator") { 387 | if (/\+\+|--/.test(value)) return cont(me); 388 | if (value == "?") return cont(expression, expect(":"), expr); 389 | return cont(expr); 390 | } 391 | if (type == "quasi") { return pass(quasi, me); } 392 | if (type == ";") return; 393 | if (type == "(") return contCommasep(expressionNoComma, ")", "call", me); 394 | if (type == ".") return cont(property, me); 395 | if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me); 396 | } 397 | function quasi(type, value) { 398 | if (type != "quasi") return pass(); 399 | if (value.slice(value.length - 2) != "${") return cont(quasi); 400 | return cont(expression, continueQuasi); 401 | } 402 | function continueQuasi(type) { 403 | if (type == "}") { 404 | cx.marked = "string-2"; 405 | cx.state.tokenize = tokenQuasi; 406 | return cont(quasi); 407 | } 408 | } 409 | function arrowBody(type) { 410 | findFatArrow(cx.stream, cx.state); 411 | return pass(type == "{" ? statement : expression); 412 | } 413 | function arrowBodyNoComma(type) { 414 | findFatArrow(cx.stream, cx.state); 415 | return pass(type == "{" ? statement : expressionNoComma); 416 | } 417 | function maybelabel(type) { 418 | if (type == ":") return cont(poplex, statement); 419 | return pass(maybeoperatorComma, expect(";"), poplex); 420 | } 421 | function property(type) { 422 | if (type == "variable") {cx.marked = "property"; return cont();} 423 | } 424 | function objprop(type, value) { 425 | if (type == "variable" || cx.style == "keyword") { 426 | cx.marked = "property"; 427 | if (value == "get" || value == "set") return cont(getterSetter); 428 | return cont(afterprop); 429 | } else if (type == "number" || type == "string") { 430 | cx.marked = jsonldMode ? "property" : (cx.style + " property"); 431 | return cont(afterprop); 432 | } else if (type == "jsonld-keyword") { 433 | return cont(afterprop); 434 | } else if (type == "[") { 435 | return cont(expression, expect("]"), afterprop); 436 | } 437 | } 438 | function getterSetter(type) { 439 | if (type != "variable") return pass(afterprop); 440 | cx.marked = "property"; 441 | return cont(functiondef); 442 | } 443 | function afterprop(type) { 444 | if (type == ":") return cont(expressionNoComma); 445 | if (type == "(") return pass(functiondef); 446 | } 447 | function commasep(what, end) { 448 | function proceed(type) { 449 | if (type == ",") { 450 | var lex = cx.state.lexical; 451 | if (lex.info == "call") lex.pos = (lex.pos || 0) + 1; 452 | return cont(what, proceed); 453 | } 454 | if (type == end) return cont(); 455 | return cont(expect(end)); 456 | } 457 | return function(type) { 458 | if (type == end) return cont(); 459 | return pass(what, proceed); 460 | }; 461 | } 462 | function contCommasep(what, end, info) { 463 | for (var i = 3; i < arguments.length; i++) 464 | cx.cc.push(arguments[i]); 465 | return cont(pushlex(end, info), commasep(what, end), poplex); 466 | } 467 | function block(type) { 468 | if (type == "}") return cont(); 469 | return pass(statement, block); 470 | } 471 | function maybetype(type) { 472 | if (isTS && type == ":") return cont(typedef); 473 | } 474 | function typedef(type) { 475 | if (type == "variable"){cx.marked = "variable-3"; return cont();} 476 | } 477 | function vardef() { 478 | return pass(pattern, maybetype, maybeAssign, vardefCont); 479 | } 480 | function pattern(type, value) { 481 | if (type == "variable") { register(value); return cont(); } 482 | if (type == "[") return contCommasep(pattern, "]"); 483 | if (type == "{") return contCommasep(proppattern, "}"); 484 | } 485 | function proppattern(type, value) { 486 | if (type == "variable" && !cx.stream.match(/^\s*:/, false)) { 487 | register(value); 488 | return cont(maybeAssign); 489 | } 490 | if (type == "variable") cx.marked = "property"; 491 | return cont(expect(":"), pattern, maybeAssign); 492 | } 493 | function maybeAssign(_type, value) { 494 | if (value == "=") return cont(expressionNoComma); 495 | } 496 | function vardefCont(type) { 497 | if (type == ",") return cont(vardef); 498 | } 499 | function maybeelse(type, value) { 500 | if (type == "keyword b" && value == "else") return cont(pushlex("form", "else"), statement, poplex); 501 | } 502 | function forspec(type) { 503 | if (type == "(") return cont(pushlex(")"), forspec1, expect(")"), poplex); 504 | } 505 | function forspec1(type) { 506 | if (type == "var") return cont(vardef, expect(";"), forspec2); 507 | if (type == ";") return cont(forspec2); 508 | if (type == "variable") return cont(formaybeinof); 509 | return pass(expression, expect(";"), forspec2); 510 | } 511 | function formaybeinof(_type, value) { 512 | if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression); } 513 | return cont(maybeoperatorComma, forspec2); 514 | } 515 | function forspec2(type, value) { 516 | if (type == ";") return cont(forspec3); 517 | if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression); } 518 | return pass(expression, expect(";"), forspec3); 519 | } 520 | function forspec3(type) { 521 | if (type != ")") cont(expression); 522 | } 523 | function functiondef(type, value) { 524 | if (value == "*") {cx.marked = "keyword"; return cont(functiondef);} 525 | if (type == "variable") {register(value); return cont(functiondef);} 526 | if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, statement, popcontext); 527 | } 528 | function funarg(type) { 529 | if (type == "spread") return cont(funarg); 530 | return pass(pattern, maybetype); 531 | } 532 | function className(type, value) { 533 | if (type == "variable") {register(value); return cont(classNameAfter);} 534 | } 535 | function classNameAfter(type, value) { 536 | if (value == "extends") return cont(expression, classNameAfter); 537 | if (type == "{") return cont(pushlex("}"), classBody, poplex); 538 | } 539 | function classBody(type, value) { 540 | if (type == "variable" || cx.style == "keyword") { 541 | cx.marked = "property"; 542 | if (value == "get" || value == "set") return cont(classGetterSetter, functiondef, classBody); 543 | return cont(functiondef, classBody); 544 | } 545 | if (value == "*") { 546 | cx.marked = "keyword"; 547 | return cont(classBody); 548 | } 549 | if (type == ";") return cont(classBody); 550 | if (type == "}") return cont(); 551 | } 552 | function classGetterSetter(type) { 553 | if (type != "variable") return pass(); 554 | cx.marked = "property"; 555 | return cont(); 556 | } 557 | function afterModule(type, value) { 558 | if (type == "string") return cont(statement); 559 | if (type == "variable") { register(value); return cont(maybeFrom); } 560 | } 561 | function afterExport(_type, value) { 562 | if (value == "*") { cx.marked = "keyword"; return cont(maybeFrom, expect(";")); } 563 | if (value == "default") { cx.marked = "keyword"; return cont(expression, expect(";")); } 564 | return pass(statement); 565 | } 566 | function afterImport(type) { 567 | if (type == "string") return cont(); 568 | return pass(importSpec, maybeFrom); 569 | } 570 | function importSpec(type, value) { 571 | if (type == "{") return contCommasep(importSpec, "}"); 572 | if (type == "variable") register(value); 573 | return cont(); 574 | } 575 | function maybeFrom(_type, value) { 576 | if (value == "from") { cx.marked = "keyword"; return cont(expression); } 577 | } 578 | function arrayLiteral(type) { 579 | if (type == "]") return cont(); 580 | return pass(expressionNoComma, maybeArrayComprehension); 581 | } 582 | function maybeArrayComprehension(type) { 583 | if (type == "for") return pass(comprehension, expect("]")); 584 | if (type == ",") return cont(commasep(maybeexpressionNoComma, "]")); 585 | return pass(commasep(expressionNoComma, "]")); 586 | } 587 | function comprehension(type) { 588 | if (type == "for") return cont(forspec, comprehension); 589 | if (type == "if") return cont(expression, comprehension); 590 | } 591 | 592 | function isContinuedStatement(state, textAfter) { 593 | return state.lastType == "operator" || state.lastType == "," || 594 | isOperatorChar.test(textAfter.charAt(0)) || 595 | /[,.]/.test(textAfter.charAt(0)); 596 | } 597 | 598 | // Interface 599 | 600 | return { 601 | startState: function(basecolumn) { 602 | var state = { 603 | tokenize: tokenBase, 604 | lastType: "sof", 605 | cc: [], 606 | lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false), 607 | localVars: parserConfig.localVars, 608 | context: parserConfig.localVars && {vars: parserConfig.localVars}, 609 | indented: 0 610 | }; 611 | if (parserConfig.globalVars && typeof parserConfig.globalVars == "object") 612 | state.globalVars = parserConfig.globalVars; 613 | return state; 614 | }, 615 | 616 | token: function(stream, state) { 617 | if (stream.sol()) { 618 | if (!state.lexical.hasOwnProperty("align")) 619 | state.lexical.align = false; 620 | state.indented = stream.indentation(); 621 | findFatArrow(stream, state); 622 | } 623 | if (state.tokenize != tokenComment && stream.eatSpace()) return null; 624 | var style = state.tokenize(stream, state); 625 | if (type == "comment") return style; 626 | state.lastType = type == "operator" && (content == "++" || content == "--") ? "incdec" : type; 627 | return parseJS(state, style, type, content, stream); 628 | }, 629 | 630 | indent: function(state, textAfter) { 631 | if (state.tokenize == tokenComment) return CodeMirror.Pass; 632 | if (state.tokenize != tokenBase) return 0; 633 | var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical; 634 | // Kludge to prevent 'maybelse' from blocking lexical scope pops 635 | if (!/^\s*else\b/.test(textAfter)) for (var i = state.cc.length - 1; i >= 0; --i) { 636 | var c = state.cc[i]; 637 | if (c == poplex) lexical = lexical.prev; 638 | else if (c != maybeelse) break; 639 | } 640 | if (lexical.type == "stat" && firstChar == "}") lexical = lexical.prev; 641 | if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat") 642 | lexical = lexical.prev; 643 | var type = lexical.type, closing = firstChar == type; 644 | 645 | if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info + 1 : 0); 646 | else if (type == "form" && firstChar == "{") return lexical.indented; 647 | else if (type == "form") return lexical.indented + indentUnit; 648 | else if (type == "stat") 649 | return lexical.indented + (isContinuedStatement(state, textAfter) ? statementIndent || indentUnit : 0); 650 | else if (lexical.info == "switch" && !closing && parserConfig.doubleIndentSwitch != false) 651 | return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit); 652 | else if (lexical.align) return lexical.column + (closing ? 0 : 1); 653 | else return lexical.indented + (closing ? 0 : indentUnit); 654 | }, 655 | 656 | electricInput: /^\s*(?:case .*?:|default:|\{|\})$/, 657 | blockCommentStart: jsonMode ? null : "/*", 658 | blockCommentEnd: jsonMode ? null : "*/", 659 | lineComment: jsonMode ? null : "//", 660 | fold: "brace", 661 | 662 | helperType: jsonMode ? "json" : "javascript", 663 | jsonldMode: jsonldMode, 664 | jsonMode: jsonMode 665 | }; 666 | }); 667 | 668 | CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/); 669 | 670 | CodeMirror.defineMIME("text/javascript", "javascript"); 671 | CodeMirror.defineMIME("text/ecmascript", "javascript"); 672 | CodeMirror.defineMIME("application/javascript", "javascript"); 673 | CodeMirror.defineMIME("application/x-javascript", "javascript"); 674 | CodeMirror.defineMIME("application/ecmascript", "javascript"); 675 | CodeMirror.defineMIME("application/json", {name: "javascript", json: true}); 676 | CodeMirror.defineMIME("application/x-json", {name: "javascript", json: true}); 677 | CodeMirror.defineMIME("application/ld+json", {name: "javascript", jsonld: true}); 678 | CodeMirror.defineMIME("text/typescript", { name: "javascript", typescript: true }); 679 | CodeMirror.defineMIME("application/typescript", { name: "javascript", typescript: true }); 680 | }; 681 | -------------------------------------------------------------------------------- /lib/editor.js: -------------------------------------------------------------------------------- 1 | module.exports = init 2 | 3 | const frameDebounce = require('frame-debounce') 4 | const getSize = require('element-size') 5 | const CodeMirror = require('codemirror') 6 | const debounce = require('debounce') 7 | const through = require('through') 8 | 9 | require('./cm-js.js')(CodeMirror) 10 | 11 | function init(el) { 12 | const targetEl = document.createElement('div') 13 | el.appendChild(targetEl) 14 | targetEl.classList.add('editor') 15 | const editor = new CodeMirror(targetEl, { 16 | container: targetEl, 17 | theme: 'dracula', 18 | mode: 'javascript', 19 | lineNumbers: true, 20 | matchBrackets: true, 21 | indentWithTabs: false, 22 | styleActiveLine: true, 23 | showCursorWhenSelecting: true, 24 | viewportMargin: Infinity, 25 | keyMap: 'default', 26 | indentUnit: 2, 27 | tabSize: 2, 28 | value: '' 29 | }) 30 | 31 | editor.addKeyMap({ 32 | 'Tab': _ => editor.execCommand('insertSoftTab') 33 | }) 34 | 35 | editor.on('change', debounce(_ => { 36 | stream.queue(editor.getValue() || '') 37 | })) 38 | 39 | const stream = through() 40 | 41 | window.addEventListener('resize', frameDebounce(resize)) 42 | setTimeout(resize) 43 | 44 | return stream 45 | 46 | function resize(w, h) { 47 | if (w && h) return editor.setSize(w, h) 48 | let size = getSize(el) 49 | editor.setSize(w || size[0], h || size[1]) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "essim", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "beefy index.js:bundle.js" 9 | }, 10 | "author": "Chris Dickinson (http://neversaw.us/)", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "babelify": "^6.0.2", 14 | "beefy": "^2.1.5", 15 | "browserify": "^9.0.8" 16 | }, 17 | "browserify": {"transform": ["babelify"]}, 18 | "dependencies": { 19 | "codemirror": "^5.2.0", 20 | "d3": "^3.5.5", 21 | "dagre-d3": "^0.4.3", 22 | "debounce": "^1.0.0", 23 | "element-size": "^1.1.1", 24 | "escontrol": "^4.0.0", 25 | "espree": "^2.0.2", 26 | "frame-debounce": "^1.0.1", 27 | "graphlib-dot": "^0.6.1", 28 | "inherits": "^2.0.1", 29 | "through": "^2.3.7" 30 | } 31 | } 32 | --------------------------------------------------------------------------------