├── braid.js ├── client.js ├── extras ├── coffee.js ├── dash.coffee ├── diffsync.js ├── getCaretCoordinates.js ├── react.js ├── sockjs.js ├── tcp-bind.js └── tests.js ├── package.json ├── readme.md ├── server.js ├── sync9.js └── test-sse.html /client.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var unique_sockjs_string = '_connect_to_braid_' 3 | 4 | // **************** 5 | // Connecting over the Network 6 | // function http_client (url_prefix) { 7 | // // bus(url_prefix).to_fetch = function (key) { 8 | // // var rest = key.substr(0,url_prefix.length) 9 | // // fetch( 10 | // // } 11 | 12 | // // Build request 13 | // var request = new XMLHttpRequest() 14 | // request.onload = function () { 15 | // // delete pending_gets[key] 16 | // if (request.status === 200) { 17 | // var result = JSON.parse(request.responseText) 18 | // put(result, null, this) 19 | // } 20 | // else if (request.status === 500) 21 | // if (window.on_ajax_error) window.on_ajax_error() 22 | // } 23 | 24 | // // Open request 25 | // // pending_gets[key] = request 26 | // request.open('GET', url_prefix + key, true) 27 | // request.setRequestHeader('Accept','application/json') 28 | // request.setRequestHeader('X-Requested-With','XMLHttpRequest') 29 | 30 | // request.send(null) 31 | // } 32 | 33 | // this.put = function(object, continuation) { 34 | // console.error('Xmlhttperequest.put is unfinished') 35 | 36 | // console.log('pending saves', pending_puts[object.key]) 37 | // if (pending_puts[object.key]) { 38 | // console.log('Yo foo, aborting') 39 | // pending_puts[object.key].abort() 40 | // delete pending_puts[object.key] 41 | // } 42 | 43 | // var original_key = object.key 44 | 45 | // // Special case for /new. Grab the pieces of the URL. 46 | // // var pattern = new RegExp("/new/([^/]+)/(\\d+)") 47 | // // var match = original_key.match(pattern) 48 | // // var url = (match && '/' + match[1]) || original_key 49 | 50 | // // Build request 51 | // var request = new XMLHttpRequest() 52 | // request.onload = function () { 53 | // // No longer pending 54 | // delete pending_puts[original_key] 55 | 56 | // if (request.status === 200) { 57 | // var result = JSON.parse(request.responseText) 58 | // // console.log('New save result', result) 59 | // // Handle /new/stuff 60 | // // deep_map(function (obj) { 61 | // // match = obj.key && obj.key.match(/(.*)\?original_id=(\d+)$/) 62 | // // if (match && match[2]) { 63 | // // // Let's map the old and new together 64 | // // var new_key = match[1] // It's got a fresh key 65 | // // cache[new_key] = cache[original_key] // Point them at the same thing 66 | // // obj.key = new_key // And it's no longer "/new/*" 67 | // // } 68 | // // }, 69 | // // result) 70 | 71 | // // TODO: Handle rename events: {key:'other_event', command:___} 72 | 73 | // put(result, null, this) 74 | // if (continuation) continuation() 75 | // } 76 | // else if (request.status === 500) 77 | // window.ajax_error && window.ajax_error() 78 | // } 79 | 80 | // object = clone(object) 81 | // object['authenticity_token'] = csrf() 82 | 83 | // // Open request 84 | // var POST_or_PUT = match ? 'POST' : 'PUT' 85 | // request.open(POST_or_PUT, url, true) 86 | // request.setRequestHeader('Accept','application/json') 87 | // request.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); 88 | // request.setRequestHeader('X-CSRF-Token', csrf()) 89 | // request.setRequestHeader('X-Requested-With','XMLHttpRequest') 90 | // request.send(JSON.stringify(object)) 91 | 92 | // // Remember it 93 | // pending_puts[original_key] = request 94 | // } 95 | 96 | // this.del = function(key, continuation) { 97 | // // Build request 98 | // var request = new XMLHttpRequest() 99 | // request.onload = function () { 100 | // if (request.status === 200) { 101 | // console.log('Delete returned for', key) 102 | // var result = JSON.parse(request.responseText) 103 | // delete cache[key] 104 | // put(result, null, this) 105 | // if (continuation) continuation() 106 | // } 107 | // else if (request.status === 500) 108 | // if (window.on_ajax_error) window.on_ajax_error() 109 | // else { 110 | // // TODO: give user feedback that DELETE failed 111 | // console.log('DELETE of', key, 'failed!') 112 | // } 113 | 114 | // } 115 | 116 | // payload = {'authenticity_token': csrf()} 117 | 118 | // // Open request 119 | // request.open('DELETE', url_prefix + key, true) 120 | // request.setRequestHeader('Accept','application/json') 121 | // request.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); 122 | // request.setRequestHeader('X-CSRF-Token', csrf()) 123 | // request.setRequestHeader('X-Requested-With','XMLHttpRequest') 124 | // request.send(JSON.stringify(payload)) 125 | // } 126 | 127 | // var csrf_token = null 128 | // this.csrf = function (new_token) { 129 | // if (new_token) csrf_token = new_token 130 | // if (csrf_token) return csrf_token 131 | // var metas = document.getElementsByTagName('meta') 132 | // for (i=0; i 500) clearInterval(interval) 284 | i += d 285 | window.scrollTo(0, old_scroll_position) 286 | }, d) 287 | }) 288 | window.live_reload_initialized = true 289 | } 290 | } 291 | 292 | 293 | // **************** 294 | // Wrapper for React Components 295 | 296 | // XXX Currently assumes there's a braid named "bus" in global 297 | // XXX scope. 298 | 299 | var components = {} // Indexed by 'component/0', 'component/1', etc. 300 | var components_count = 0 301 | var dirty_components = {} 302 | function createReactClass(component) { 303 | function wrap(name, new_func) { 304 | var old_func = component[name] 305 | component[name] = function wrapper () { return new_func.bind(this)(old_func) } 306 | } 307 | 308 | // Register the component's basic info 309 | wrap('componentWillMount', function new_cwm (orig_func) { 310 | if (component.displayName === undefined) 311 | throw 'Component needs a displayName' 312 | this.name = component.displayName.toLowerCase().replace(' ', '_') 313 | this.key = 'component/' + components_count++ 314 | components[this.key] = this 315 | 316 | function add_shortcut (obj, shortcut_name, to_key) { 317 | delete obj[shortcut_name] 318 | Object.defineProperty(obj, shortcut_name, { 319 | get: function () { return bus.get(to_key) }, 320 | configurable: true }) 321 | } 322 | add_shortcut(this, 'local', this.key) 323 | 324 | orig_func && orig_func.apply(this, arguments) 325 | 326 | // Make render reactive 327 | var orig_render = this.render 328 | this.render = bus.reactive(function () { 329 | console.assert(this !== window) 330 | if (!this.render.reacting) { 331 | delete dirty_components[this.key] 332 | 333 | // Add reactivity to any keys passed inside objects in props. 334 | for (var k in this.props) 335 | if (this.props.hasOwnProperty(k) 336 | && this.props[k] !== null 337 | && typeof this.props[k] === 'object' 338 | && this.props[k].key) 339 | 340 | bus.get(this.props[k].key) 341 | 342 | // Call the renderer! 343 | return orig_render.apply(this, arguments) 344 | } else { 345 | dirty_components[this.key] = true 346 | schedule_re_render() 347 | } 348 | }) 349 | }) 350 | 351 | wrap('componentWillUnmount', function new_cwu (orig_func) { 352 | orig_func && orig_func.apply(this, arguments) 353 | // Clean up 354 | bus.delete(this.key) 355 | delete components[this.key] 356 | delete dirty_components[this.key] 357 | }) 358 | 359 | function shallow_clone(original) { 360 | var clone = Object.create(Object.getPrototypeOf(original)) 361 | var i, keys = Object.getOwnPropertyNames(original) 362 | for (i=0; i < keys.length; i++){ 363 | Object.defineProperty(clone, keys[i], 364 | Object.getOwnPropertyDescriptor(original, keys[i]) 365 | ) 366 | } 367 | return clone 368 | } 369 | 370 | component.shouldComponentUpdate = function new_scu (next_props, next_state) { 371 | // This component definitely needs to update if it is marked as dirty 372 | if (dirty_components[this.key] !== undefined) return true 373 | 374 | // Otherwise, we'll check to see if its state or props 375 | // have changed. But ignore React's 'children' prop, 376 | // because it often has a circular reference. 377 | next_props = shallow_clone(next_props) 378 | this_props = shallow_clone(this.props) 379 | 380 | delete next_props['children']; delete this_props['children'] 381 | // delete next_props['kids']; delete this_props['kids'] 382 | 383 | next_props = bus.clone(next_props) 384 | this_props = bus.clone(this_props) 385 | 386 | 387 | return !bus.deep_equals([next_state, next_props], [this.state, this_props]) 388 | 389 | // TODO: 390 | // 391 | // - Check children too. Right now we just silently fail 392 | // on components with children. WTF? 393 | // 394 | // - A better method might be to mark a component dirty when 395 | // it receives new props in the 396 | // componentWillReceiveProps React method. 397 | } 398 | 399 | component.loading = function loading () { 400 | return this.render.loading() 401 | } 402 | 403 | // Now create the actual React class with this definition, and 404 | // return it. 405 | var react_class = React.createClass(component) 406 | var result = function (props, children) { 407 | props = props || {} 408 | props['data-key'] = props.key 409 | props['data-widget'] = component.displayName 410 | 411 | return (React.version >= '0.12.' 412 | ? React.createElement(react_class, props, children) 413 | : react_class(props, children)) 414 | } 415 | // Give it the same prototype as the original class so that it 416 | // passes React.isValidClass() inspection 417 | result.prototype = react_class.prototype 418 | return result 419 | } 420 | window.createReactClass = createReactClass 421 | if (window.braid) window.braid.createReactClass = createReactClass 422 | 423 | // ***************** 424 | // Re-rendering react components 425 | var re_render_scheduled = false 426 | re_rendering = false 427 | function schedule_re_render() { 428 | if (!re_render_scheduled) { 429 | requestAnimationFrame(function () { 430 | re_render_scheduled = false 431 | 432 | // Re-renders dirty components 433 | for (var comp_key in dirty_components) { 434 | if (dirty_components[comp_key] // Since another component's update might update this 435 | && components[comp_key]) // Since another component might unmount this 436 | 437 | try { 438 | re_rendering = true 439 | components[comp_key].forceUpdate() 440 | } finally { 441 | re_rendering = false 442 | } 443 | } 444 | }) 445 | re_render_scheduled = true 446 | } 447 | } 448 | 449 | // ############################################################################## 450 | // ### 451 | // ### Full-featured single-file app methods 452 | // ### 453 | 454 | function make_client_bus_maker () { 455 | var extra_stuff = ['localstorage_client make_websocket client_creds', 456 | 'url_store components live_reload_from'].join(' ').split(' ') 457 | if (window.braid) { 458 | var orig_braid = braid 459 | window.braid = function make_client_bus () { 460 | var bus = orig_braid() 461 | for (var i=0; i') 486 | 487 | document.addEventListener('DOMContentLoaded', scripts_ready, false) 488 | } else 489 | scripts_ready() 490 | 491 | } 492 | 493 | function script_option (option_name) { 494 | var script_elem = document.querySelector('script[src*="client"][src$=".js"]') 495 | return script_elem && script_elem.getAttribute(option_name) 496 | } 497 | var loaded_from_file_url = window.location.href.match(/^file:\/\//) 498 | window.braid_server = window.braid_server || script_option('server') || 499 | (loaded_from_file_url ? 'none' : '/') 500 | window.braid_backdoor = window.braid_backdoor || script_option('backdoor') 501 | var react_render 502 | function scripts_ready () { 503 | react_render = React.version >= '0.14.' ? ReactDOM.render : React.render 504 | make_client_bus_maker() 505 | window.bus = window.braid() 506 | window.bus.label = 'bus' 507 | window.sb = bus.sb 508 | braid.widget = createReactClass 509 | braid.createReactClass = createReactClass 510 | 511 | improve_react() 512 | window.dom = window.ui = window.dom || window.ui || {} 513 | window.ignore_flashbacks = false 514 | if (braid_server !== 'none') 515 | // bus.net_mount ('/*', braid_server) 516 | bus.h2_mount('/*', braid_server) 517 | 518 | if (window.braid_backdoor) { 519 | window.master = braid() 520 | master.net_mount('*', braid_backdoor) 521 | } 522 | bus.net_automount() 523 | 524 | // bus('*').to_set = function (obj) { bus.set.fire(obj) } 525 | bus('/new/*').to_set = function (o) { 526 | if (o.key.split('/').length > 3) return 527 | 528 | var old_key = o.key 529 | o.key = old_key + '/' + Math.random().toString(36).substring(2,12) 530 | braid.cache[o.key] = o 531 | delete braid.cache[old_key] 532 | bus.set(o) 533 | } 534 | load_coffee() 535 | 536 | braid.compile_coffee = compile_coffee 537 | braid.load_client_code = load_client_code 538 | braid.load_widgets = load_widgets 539 | 540 | if (window.braid_ready) 541 | for (var i=0; i' in CSS selectors 667 | return s.replace(//g, ">") 668 | } 669 | window.STYLE = function (s) { 670 | return React.DOM.style({dangerouslySetInnerHTML: {__html: escape_html(s)}}) 671 | } 672 | window.TITLE = function (s) { 673 | return React.DOM.title({dangerouslySetInnerHTML: {__html: escape_html(s)}}) 674 | } 675 | } 676 | 677 | function autodetect_args (func) { 678 | if (func.args) return 679 | 680 | // Get an array of the func's params 681 | var comments = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg, 682 | params = /([^\s,]+)/g, 683 | s = func.toString().replace(comments, '') 684 | func.args = s.slice(s.indexOf('(')+1, s.indexOf(')')).match(params) || [] 685 | } 686 | 687 | 688 | // Load the components 689 | var users_widgets = {} 690 | function make_component(name) { 691 | var func = window.dom[name] 692 | 693 | // Define the component 694 | window[name] = users_widgets[name] = window.createReactClass({ 695 | displayName: name, 696 | render: function () { 697 | var args = [] 698 | 699 | // Parse the function's args, and pass props into them directly 700 | autodetect_args(func) 701 | // this.props.kids = this.props.kids || this.props.children 702 | for (var i=0; i 3 | return true 4 | for k2 of dashable_keys 5 | if k == k2 or (k2.match(/\*$/) and k2.substr(k2.length-1) == k.substr(k2.length-1)) 6 | return true 7 | return false 8 | 9 | parse_key = (key) -> 10 | word = "([^/]+)" 11 | # Matching things like: "/new/name/number" 12 | # or: "/name/number" 13 | # or: "/name" 14 | # or: "name/number" 15 | # or: "name" 16 | # ... and you can optionally include a final slash. 17 | regexp = new RegExp("(/)?(new/)?#{word}(/#{word})?(/)?") 18 | m = key.match(regexp) 19 | if not m 20 | return null 21 | 22 | [has_match, server_owned, is_new, name, tmp1, number, tmp2] = m 23 | owner = if server_owned then 'server' else 'client' 24 | return has_match and {owner, 'new': is_new, name, number} 25 | 26 | dom.STATE_DASH = -> 27 | dash = bus.fetch('state_dash') 28 | 29 | if !dash.on? 30 | dash.on = false 31 | dash.selected = {owner: null, name: null, number: null} 32 | dash.filter = null 33 | 34 | if dash.filter?.match(/idkfa/) or dash.filter?.match(/idmap/) 35 | dash.on = false 36 | dash.filter = '' 37 | bus.save(dash) 38 | 39 | if not dash.on 40 | return DIV null, '' 41 | 42 | url_tree = (cache) -> 43 | # The key tree looks like: 44 | # 45 | # {server: {thing: [obj1, obj2], shing: [obj1, obj2], ...} 46 | # client: {dong: [obj1, ...]}} 47 | # 48 | # And objects without a number, like '/shong' will go on: 49 | # key_tree.server.shong[null] 50 | tree = {server: {}, client: {}} 51 | 52 | add_key = (key) -> 53 | p = parse_key(key) 54 | if not p 55 | console.log('The state dash can\'t deal with key', key); return null 56 | 57 | if not p.name in dashable_keys then return null 58 | 59 | tree[p.owner][p.name] ||= [] 60 | tree[p.owner][p.name][p.number or null] = bus.fetch(key) 61 | 62 | for key of cache 63 | add_key(key) 64 | return tree 65 | 66 | search_results = get_search_results() 67 | cache = search_results.matches 68 | tree = url_tree(cache) 69 | 70 | first_key = (cache) -> 71 | for key of cache 72 | reject_name = dash.selected.name and parse_key(key).name != dash.selected.name 73 | reject_numb = dash.selected.number and parse_key(key).number != dash.selected.number 74 | if !reject_name and !reject_numb 75 | return key 76 | return null 77 | 78 | best_guess = first_key(search_results.key_matches) or \ 79 | first_key(search_results.data_matches) 80 | 81 | cluster_rows = (keys) -> 82 | clusters = {} 83 | for key in keys 84 | fields = JSON.stringify(Object.keys(cache[key])) 85 | clusters[fields] = clusters[fields] or [] 86 | clusters[fields].push(key) 87 | return clusters 88 | 89 | filter_to = (e) -> 90 | dash.filter = e.target.value 91 | if dash.filter.length == 0 92 | dash.filter = null 93 | bus.save(dash) 94 | true 95 | 96 | 97 | DIV className: 'state_dash', style: this.props.style, 98 | React.DOM.style({dangerouslySetInnerHTML: {__html: 99 | """ 100 | .state_dash { 101 | position: fixed; 102 | margin: 20px; 103 | z-index: 10000; 104 | top: 0; 105 | left: 0; 106 | } 107 | .state_dash .left, .state_dash .right, .state_dash .top { 108 | background-color: #eee; 109 | overflow-wrap: break-word; 110 | padding: 10px; 111 | vertical-align: top; 112 | } 113 | .state_dash .left { min-width: 40px; max-width: 140px; } 114 | .state_dash .right { left: 180px; position: absolute; white-space: nowrap; } 115 | .state_dash .top { max-width: 100%; margin: 20px 0; } 116 | 117 | .string { color: green; } 118 | .number { color: darkorange; } 119 | .boolean { color: blue; } 120 | .null { color: magenta; } 121 | .key { color: red; } 122 | 123 | .cell { 124 | width: 150px; 125 | height: 26px; 126 | border: 1px solid grey; 127 | padding: 2px; 128 | display: inline-block; 129 | white-space: nowrap; 130 | overflow: hidden; 131 | text-overflow: clip; 132 | margin: 0; 133 | } 134 | """}}) 135 | 136 | # Render the object 137 | # PRE className: 'right', ref: 'json_preview', 138 | # JSON.stringify(arest.cache[best_guess], undefined, 3) 139 | DIV className: 'right', 140 | # Next: go through the list, cluster those that fit the same 141 | # schema, and display a table for each. 142 | if dash.selected.name 143 | selected_keys = (key for key of cache when parse_key(key).name == dash.selected.name) 144 | clusters = cluster_rows(selected_keys) 145 | for fieldset of clusters 146 | # Print each table 147 | DIV null, 148 | # Print the table header 149 | for field in JSON.parse(fieldset) 150 | DIV 151 | key: field 152 | style: {fontWeight: 'bold', borderColor: '#eee'} 153 | className: 'cell' 154 | field 155 | 156 | # Print the table body 157 | for key in clusters[fieldset] 158 | row = cache[key] 159 | DIV key: key, 160 | for field of row 161 | DIV 162 | className: 'cell' 163 | key: field 164 | onClick: do (key, field) => (evt) => 165 | @local.editing = {key_:key, field:field} 166 | bus.save(@local) 167 | setTimeout(=> @refs.dash_input.getDOMNode().focus()) 168 | if (@local.editing \ 169 | and @local.editing.key_ == key \ 170 | and @local.editing.field == field) 171 | SPAN null, 172 | TEXTAREA 173 | key: field 174 | type: 'text' 175 | ref: 'dash_input' 176 | defaultValue: JSON.stringify(row[field]) 177 | onChange: (event) => 178 | try 179 | val = JSON.parse(@refs.dash_input.getDOMNode().value) 180 | cache[@local.editing.key_][@local.editing.field] = val 181 | bus.save(cache[@local.editing.key_]) 182 | event.stopPropagation() 183 | 184 | delete @local.editing.error; bus.save(@local) 185 | catch e 186 | @local.editing.error = true; bus.save(@local) 187 | onBlur: (event) => 188 | delete @local.editing; bus.save(@local) 189 | style: 190 | position: 'absolute' 191 | backgroundColor: '#faa' if @local.editing.error 192 | # INPUT 193 | # type: 'submit' 194 | # onClick: (event) => 195 | # try 196 | # val = JSON.parse(@refs.dash_input.getDOMNode().value) 197 | # cache[@local.editing.key_][@local.editing.field] = val 198 | # bus.save(cache[@local.editing.key_]) 199 | # c = bus.fetch('component/1') 200 | # delete c.editing 201 | # bus.save(c) 202 | # event.stopPropagation() 203 | # catch e 204 | # @local.editing.error = true 205 | # bus.save(@local) 206 | # style: 207 | # position: 'absolute' 208 | # marginTop: 30 209 | 210 | JSON.stringify(row[field]) 211 | 212 | # Render the top (name) menu 213 | DIV className: 'left', #onMouseLeave: reset_selection, 214 | INPUT 215 | ref: 'search' 216 | className: 'search' 217 | onChange: filter_to 218 | onMouseEnter: reset_selection 219 | 220 | for owner in ['client', 'server'] 221 | prefix = (owner == 'server') and '/' or '' 222 | names = (n for n of tree[owner]) 223 | DIV {key: owner}, 224 | for name in names.sort() 225 | do (owner, name) -> 226 | f = -> dash.selected={owner, name, number:null}; bus.save(dash) 227 | style = (name == dash.selected.name) and {'background-color':'#aaf'} or {} 228 | DIV onMouseEnter: f, key: name, style: style, 229 | prefix + name 230 | 231 | 232 | recent_keys = [0,0,0,0,0] 233 | 234 | dash_initialized = false 235 | window.addEventListener('DOMContentLoaded', -> 236 | document.addEventListener "keypress", (e) => 237 | key = (e and e.keyCode) or e.keyCode 238 | recent_keys.push(key) 239 | recent_keys = recent_keys.slice(1) 240 | 241 | # console.log('recent keys:', recent_keys) 242 | if key==4 or "#{recent_keys}" == "#{[105, 100, 109, 97, 112]}" \ # idmap 243 | or "#{recent_keys}" == "#{[105, 100, 107, 102, 97]}" # idkfa 244 | dash = bus.fetch('state_dash') 245 | if dash.on 246 | dash.on = false 247 | else 248 | dash.on = true 249 | bus.save(dash) 250 | # setTimeout => 251 | # console.log @refs 252 | # @refs.search.getDOMNode().focus() 253 | ) 254 | 255 | get_search_results = -> 256 | # Returns two filtered views of the cache: 257 | # 258 | # { key_matches: {...a cache filtered to matching keys... } 259 | # data_matches: {...a cache filtered to matching data...} } 260 | 261 | dash = bus.fetch('state_dash') 262 | matches = {} 263 | key_matches = {} 264 | data_matches = {} 265 | if dash.filter 266 | for key of bus.cache 267 | if key.match(dash.filter) 268 | matches[key] = bus.cache[key] 269 | key_matches[key] = bus.cache[key] 270 | if JSON.stringify(bus.cache[key]).match(dash.filter) 271 | matches[key] = bus.cache[key] 272 | data_matches[key] = bus.cache[key] 273 | else 274 | matches = key_matches = data_matches = bus.cache 275 | 276 | return {matches, key_matches, data_matches} 277 | 278 | 279 | # componentDidUpdate: () -> 280 | # el = @refs.json_preview.getDOMNode() 281 | # el.innerHTML = rainbows(el.innerHTML) 282 | 283 | reset_selection = () -> 284 | dash = bus.fetch('state_dash') 285 | dash.selected = {owner: null, name: null, number: null} 286 | bus.save(dash) 287 | 288 | rainbows = (json) -> 289 | json = json.replace(/&/g, '&').replace(//g, '>') 290 | return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, (match) -> 291 | cls = 'number' 292 | if (/^"/.test(match)) 293 | if (/:$/.test(match)) 294 | cls = 'key' 295 | else 296 | cls = 'string'; 297 | else if (/true|false/.test(match)) 298 | cls = 'boolean' 299 | else if (/null/.test(match)) 300 | cls = 'null'; 301 | 302 | return "#{match}") -------------------------------------------------------------------------------- /extras/diffsync.js: -------------------------------------------------------------------------------- 1 | 2 | var diffsync = (typeof(module) != 'undefined') ? module.exports : {} 3 | 4 | diffsync.version = 1039 5 | diffsync.port = 60607 6 | 7 | // var client = diffsync.create_client({ 8 | // ws_url : 'ws://invisible.college:' + diffsync.port, 9 | // channel : 'the_cool_room', 10 | // get_text : function () { 11 | // return current_text_displayed_to_user 12 | // }, 13 | // get_range : function () { 14 | // return [selection_start, selection_end] 15 | // }, 16 | // on_text : function (text, range) { 17 | // current_text_displayed_to_user = text 18 | // set_selection(range[0], range[1]) 19 | // }, 20 | // on_ranges : function (peer_ranges) { 21 | //. for (peer in peer_ranges) { 22 | // set_peer_selection(peer_ranges[peer][0], peer_ranges[peer][1]) 23 | // } 24 | // } 25 | // }) 26 | // 27 | // client.on_change() <-- call this when the user changes the text or cursor/selection position 28 | // 29 | diffsync.create_client = function (options) { 30 | var self = {} 31 | self.on_change = null 32 | self.on_window_closing = null 33 | self.get_channels = null 34 | 35 | var on_channels = null 36 | 37 | var uid = guid() 38 | var minigit = diffsync.create_minigit() 39 | var unacknowledged_commits = {} 40 | 41 | var prev_range = [-1, -1] 42 | var peer_ranges = {} 43 | 44 | window.addEventListener('beforeunload', function () { 45 | if (self.on_window_closing) self.on_window_closing() 46 | }) 47 | 48 | var connected = false 49 | function reconnect() { 50 | connected = false 51 | console.log('connecting...') 52 | var ws = new WebSocket(options.ws_url) 53 | 54 | function send(o) { 55 | o.v = diffsync.version 56 | o.uid = uid 57 | o.channel = options.channel 58 | try { 59 | ws.send(JSON.stringify(o)) 60 | } catch (e) {} 61 | } 62 | 63 | self.on_window_closing = function () { 64 | send({ close : true }) 65 | } 66 | 67 | self.get_channels = function (cb) { 68 | on_channels = cb 69 | send({ get_channels : true }) 70 | } 71 | 72 | ws.onopen = function () { 73 | connected = true 74 | send({ join : true }) 75 | on_pong() 76 | } 77 | 78 | var pong_timer = null 79 | function on_pong() { 80 | clearTimeout(pong_timer) 81 | setTimeout(function () { 82 | send({ ping : true }) 83 | pong_timer = setTimeout(function () { 84 | console.log('no pong came!!') 85 | if (ws) { 86 | ws = null 87 | reconnect() 88 | } 89 | }, 4000) 90 | }, 3000) 91 | } 92 | 93 | ws.onclose = function () { 94 | console.log('connection closed...') 95 | if (ws) { 96 | ws = null 97 | reconnect() 98 | } 99 | } 100 | 101 | var sent_unacknowledged_commits = false 102 | 103 | function adjust_range(range, patch) { 104 | return map_array(range, function (x) { 105 | each(patch, function (p) { 106 | if (p[0] < x) { 107 | if (p[0] + p[1] <= x) { 108 | x += -p[1] + p[2].length 109 | } else { 110 | x = p[0] + p[2].length 111 | } 112 | } else return false 113 | }) 114 | return x 115 | }) 116 | } 117 | 118 | ws.onmessage = function (event) { 119 | if (!ws) { return } 120 | var o = JSON.parse(event.data) 121 | if (o.pong) { return on_pong() } 122 | 123 | console.log('message: ' + event.data) 124 | 125 | if (o.channels) { 126 | if (on_channels) on_channels(o.channels) 127 | } 128 | if (o.commits) { 129 | self.on_change() 130 | minigit.merge(o.commits) 131 | 132 | var patch = get_diff_patch(options.get_text(), minigit.cache) 133 | each(peer_ranges, function (range, peer) { 134 | peer_ranges[peer] = adjust_range(range, patch) 135 | }) 136 | 137 | prev_range = adjust_range(options.get_range(), patch) 138 | options.on_text(minigit.cache, prev_range) 139 | 140 | if (o.welcome) { 141 | each(extend(o.commits, minigit.get_ancestors(o.commits)), function (_, id) { 142 | delete unacknowledged_commits[id] 143 | }) 144 | if (Object.keys(unacknowledged_commits).length > 0) { 145 | send({ commits : unacknowledged_commits }) 146 | } 147 | sent_unacknowledged_commits = true 148 | } 149 | 150 | send({ leaves : minigit.leaves }) 151 | } 152 | if (o.may_delete) { 153 | each(o.may_delete, function (_, id) { 154 | delete unacknowledged_commits[id] 155 | minigit.remove(id) 156 | }) 157 | } 158 | if (o.range) { 159 | peer_ranges[o.uid] = o.range 160 | } 161 | if (o.close) { 162 | delete peer_ranges[o.uid] 163 | } 164 | if ((o.range || o.close || o.commits) && options.on_ranges) { 165 | options.on_ranges(peer_ranges) 166 | } 167 | } 168 | 169 | self.on_change = function () { 170 | if (!connected) { return } 171 | 172 | var old_cache = minigit.cache 173 | var cs = minigit.commit(options.get_text()) 174 | if (cs) { 175 | extend(unacknowledged_commits, cs) 176 | 177 | var patch = null 178 | var c = cs[Object.keys(cs)[0]] 179 | var parents = Object.keys(c.from_parents) 180 | if (parents.length == 1) 181 | patch = c.from_parents[parents[0]] 182 | else 183 | patch = get_diff_patch(old_cache, minigit.cache) 184 | 185 | each(peer_ranges, function (range, peer) { 186 | peer_ranges[peer] = adjust_range(range, patch) 187 | }) 188 | if (options.on_ranges) options.on_ranges(peer_ranges) 189 | } 190 | 191 | if (!sent_unacknowledged_commits) { return } 192 | 193 | var range = options.get_range() 194 | var range_changed = (range[0] != prev_range[0]) || (range[1] != prev_range[1]) 195 | prev_range = range 196 | 197 | var msg = {} 198 | if (cs) msg.commits = cs 199 | 200 | if (range_changed) msg.range = range 201 | if (cs || range_changed) send(msg) 202 | } 203 | } 204 | reconnect() 205 | 206 | return self 207 | } 208 | 209 | // options is an object like this: { 210 | // wss : a websocket server from the 'ws' module, 211 | // initial_data : { 212 | // 'some_channel_name' : { 213 | // commits : { 214 | // 'asdfasdf' : { 215 | // to_parents : {}, 216 | // from_parents : {}, 217 | // text : 'hello' 218 | // } 219 | // }, 220 | // members : { 221 | // 'lkjlkjlkj' : { 222 | // do_not_delete : { 223 | // 'asdfasdf' : true 224 | // } 225 | // }, 226 | // last_seen : 1510878755554, 227 | // last_sent : 1510878755554 228 | // } 229 | // } 230 | // }, 231 | // on_change : function (changes) { 232 | // changes contains commits and members that changed, 233 | // and looks like: { 234 | // channel : 'some_channel_name', 235 | // commits : {...}, 236 | // members : {...} 237 | // } 238 | // } 239 | // } 240 | // 241 | diffsync.create_server = function (options) { 242 | var self = {} 243 | self.channels = {} 244 | 245 | function new_channel(name) { 246 | return self.channels[name] = { 247 | name : name, 248 | minigit : diffsync.create_minigit(), 249 | members : {} 250 | } 251 | } 252 | function get_channel(name) { 253 | return self.channels[name] || new_channel(name) 254 | } 255 | 256 | var users_to_sockets = {} 257 | 258 | if (options.initial_data) { 259 | each(options.initial_data, function (data, channel) { 260 | var c = get_channel(channel) 261 | c.minigit.merge(data.commits) 262 | extend(c.members, data.members) 263 | }) 264 | } 265 | 266 | options.wss.on('connection', function connection(ws) { 267 | console.log('new connection') 268 | var uid = null 269 | var channel_name = null 270 | 271 | function myClose() { 272 | if (!uid) { return } 273 | delete users_to_sockets[uid] 274 | each(users_to_sockets, function (_ws, _uid) { 275 | try { 276 | _ws.send(JSON.stringify({ 277 | v : diffsync.version, 278 | uid : uid, 279 | channel : channel_name, 280 | close : true 281 | })) 282 | } catch (e) {} 283 | }) 284 | } 285 | 286 | ws.on('close', myClose) 287 | ws.on('error', myClose) 288 | 289 | ws.on('message', function (message) { 290 | var o = JSON.parse(message) 291 | if (o.v != diffsync.version) { return } 292 | if (o.ping) { return try_send(ws, JSON.stringify({ pong : true })) } 293 | 294 | console.log('message: ' + message) 295 | 296 | uid = o.uid 297 | var channel = get_channel(o.channel) 298 | channel_name = channel.name 299 | users_to_sockets[uid] = ws 300 | 301 | var changes = { channel : channel.name, commits : {}, members : {} } 302 | 303 | if (!channel.members[uid]) channel.members[uid] = { do_not_delete : {}, last_sent : 0 } 304 | channel.members[uid].last_seen = Date.now() 305 | changes.members[uid] = channel.members[uid] 306 | 307 | function try_send(ws, message) { 308 | try { 309 | ws.send(message) 310 | } catch (e) {} 311 | } 312 | function send_to_all_but_me(message) { 313 | each(channel.members, function (_, them) { 314 | if (them != uid) { 315 | try_send(users_to_sockets[them], message) 316 | } 317 | }) 318 | } 319 | 320 | if (o.get_channels) { 321 | try_send(ws, JSON.stringify({ channels : Object.keys(self.channels) })) 322 | } 323 | if (o.join) { 324 | try_send(ws, JSON.stringify({ commits : channel.minigit.commits, welcome : true })) 325 | } 326 | if (o.commits) { 327 | var new_commits = {} 328 | each(o.commits, function (c, id) { 329 | if (!channel.minigit.commits[id]) { 330 | new_commits[id] = c 331 | changes.commits[id] = c 332 | } 333 | }) 334 | channel.minigit.merge(new_commits) 335 | 336 | var new_message = { 337 | channel : channel.name, 338 | commits : new_commits 339 | } 340 | if (o.range) { 341 | new_message.uid = o.uid 342 | new_message.range = o.range 343 | } 344 | new_message = JSON.stringify(new_message) 345 | 346 | leaves = channel.minigit.get_leaves(new_commits) 347 | var now = Date.now() 348 | each(channel.members, function (m, them) { 349 | if (them != uid) { 350 | if (m.last_seen > m.last_sent) { 351 | m.last_sent = now 352 | changes.members[them] = m 353 | } else if (m.last_sent < now - 3000) { 354 | return 355 | } 356 | extend(m.do_not_delete, leaves) 357 | try_send(users_to_sockets[them], new_message) 358 | } 359 | }) 360 | if (!o.leaves) o.leaves = channel.minigit.get_leaves(o.commits) 361 | } else if (o.range) { 362 | send_to_all_but_me(message) 363 | } 364 | if (o.leaves) { 365 | extend(channel.members[uid].do_not_delete, o.leaves) 366 | each(channel.minigit.get_ancestors(o.leaves), function (_, id) { 367 | delete channel.members[uid].do_not_delete[id] 368 | }) 369 | 370 | var necessary = {} 371 | each(channel.members, function (m) { 372 | extend(necessary, m.do_not_delete) 373 | }) 374 | 375 | var affected = channel.minigit.remove_unnecessary(necessary) 376 | extend(changes.commits, affected) 377 | 378 | var new_message = { 379 | channel : channel.name, 380 | may_delete : {} 381 | } 382 | each(affected, function (c, id) { 383 | if (c.delete_me) { 384 | new_message.may_delete[id] = true 385 | } 386 | }) 387 | if (Object.keys(new_message.may_delete).length > 0) { 388 | new_message = JSON.stringify(new_message) 389 | each(channel.members, function (m, them) { 390 | try_send(users_to_sockets[them], new_message) 391 | }) 392 | } 393 | } 394 | if (o.close) { 395 | channel.members[uid].delete_me = true 396 | delete channel.members[uid] 397 | } 398 | 399 | if (options.on_change) options.on_change(changes) 400 | }) 401 | }) 402 | 403 | return self 404 | } 405 | 406 | /////////////// 407 | 408 | diffsync.create_minigit = function () { 409 | var self = { 410 | commits : {}, 411 | to_children : {}, 412 | commit_cache : {}, 413 | leaves : {}, 414 | cache : '' 415 | } 416 | 417 | self.remove_unnecessary = function (spare_us) { 418 | var affected = {} 419 | while (true) { 420 | var found = false 421 | each(self.commits, function (c, id) { 422 | if (spare_us[id]) { return } 423 | var aff = self.remove(id) 424 | if (aff) { 425 | extend(affected, aff) 426 | found = true 427 | } 428 | }) 429 | if (!found) break 430 | } 431 | return affected 432 | } 433 | 434 | self.remove = function (id) { 435 | var keys = Object.keys(self.to_children[id]) 436 | if (keys.length == 1) { 437 | var affected = {} 438 | 439 | var being_removed = self.commits[id] 440 | var c_id = keys[0] 441 | var c = self.commits[c_id] 442 | 443 | self.get_text(c_id) 444 | each(being_removed.to_parents, function (_, id) { 445 | self.get_text(id) 446 | }) 447 | 448 | delete self.commits[id] 449 | delete self.commit_cache[id] 450 | delete c.to_parents[id] 451 | delete c.from_parents[id] 452 | being_removed.delete_me = true 453 | affected[id] = being_removed 454 | 455 | each(being_removed.to_parents, function (_, id) { 456 | var x = get_diff_patch_2(self.get_text(c_id), self.get_text(id)) 457 | c.to_parents[id] = x[0] 458 | c.from_parents[id] = x[1] 459 | }) 460 | if (Object.keys(c.to_parents).length == 0) { 461 | c.text = self.get_text(c_id) 462 | } 463 | affected[c_id] = c 464 | 465 | self.calc_children() 466 | 467 | return affected 468 | } 469 | } 470 | 471 | self.commit = function (s) { 472 | if (s == self.cache) { return } 473 | 474 | var c = { 475 | to_parents : {}, 476 | from_parents : {} 477 | } 478 | if (Object.keys(self.leaves).length == 0) { 479 | c.text = s 480 | } else { 481 | each(self.leaves, function (_, leaf) { 482 | var x = get_diff_patch_2(s, self.get_text(leaf)) 483 | c.to_parents[leaf] = x[0] 484 | c.from_parents[leaf] = x[1] 485 | }) 486 | } 487 | 488 | var id = guid() 489 | self.commits[id] = c 490 | self.calc_children() 491 | self.leaves = {} 492 | self.leaves[id] = true 493 | self.commit_cache[id] = s 494 | self.cache = s 495 | self.purge_cache() 496 | 497 | var cs = {} 498 | cs[id] = c 499 | return cs 500 | } 501 | 502 | self.merge = function (cs) { 503 | each(cs, function (c, id) { 504 | if (!self.commits[id]) { 505 | self.commits[id] = c 506 | } else { 507 | if (c.text) self.commits[id].text = c.text 508 | extend(self.commits[id].to_parents, c.to_parents) 509 | extend(self.commits[id].from_parents, c.from_parents) 510 | } 511 | }) 512 | self.calc_children() 513 | self.leaves = self.get_leaves() 514 | self.cache = self.rec_merge(self.leaves) 515 | self.purge_cache() 516 | return self.cache 517 | } 518 | 519 | self.calc_children = function () { 520 | self.to_children = {} 521 | each(self.commits, function (c, id) { 522 | self.to_children[id] = {} 523 | }) 524 | each(self.commits, function (c, id) { 525 | each(c.from_parents, function (d, p_id) { 526 | self.to_children[p_id][id] = d 527 | }) 528 | }) 529 | } 530 | 531 | self.purge_cache = function () { 532 | each(self.commits, function (c, id) { 533 | if (Object.keys(c.to_parents).length > 0 && !self.leaves[id]) { 534 | delete self.commit_cache[id] 535 | } 536 | }) 537 | } 538 | 539 | self.get_text = function (id) { 540 | if (self.commit_cache[id] != null) return self.commit_cache[id] 541 | 542 | var frontier = [id] 543 | var back_pointers = {} 544 | back_pointers[id] = id 545 | while (true) { 546 | var next = frontier.shift() 547 | 548 | if (!next) { throw 'data structure corrupted' } 549 | var c_id = next 550 | var c = self.commits[c_id] 551 | var text = (c.text != null) ? c.text : self.commit_cache[c_id] 552 | if (text != null) { 553 | var snowball = text 554 | while (true) { 555 | if (next == id) { 556 | return self.commit_cache[id] = snowball 557 | } 558 | next = back_pointers[next] 559 | snowball = apply_diff_patch(snowball, c.to_parents[next] || self.to_children[c_id][next]) 560 | c_id = next 561 | c = self.commits[c_id] 562 | } 563 | } 564 | 565 | each(c.to_parents, function (_, id) { 566 | if (!back_pointers[id]) { 567 | back_pointers[id] = next 568 | frontier.push(id) 569 | } 570 | }) 571 | each(self.to_children[c_id], function (_, id) { 572 | if (!back_pointers[id]) { 573 | back_pointers[id] = next 574 | frontier.push(id) 575 | } 576 | }) 577 | } 578 | } 579 | 580 | self.rec_merge = function (these) { 581 | these = Object.keys(these) 582 | if (these.length == 0) { return '' } 583 | var r = self.get_text(these[0]) 584 | if (these.length == 1) { return r } 585 | var r_ancestors = self.get_ancestors(these[0]) 586 | for (var i = 1; i < these.length; i++) { 587 | var i_ancestors = self.get_ancestors(these[i]) 588 | var o = self.rec_merge(self.get_leaves(intersection(r_ancestors, i_ancestors))) 589 | r = apply_diff_patch(o, get_merged_diff_patch(r, self.get_text(these[i]), o)) 590 | extend(r_ancestors, i_ancestors) 591 | } 592 | return r 593 | } 594 | 595 | self.get_leaves = function (commits) { 596 | if (!commits) commits = self.commits 597 | var leaves = {} 598 | each(commits, function (_, id) { leaves[id] = true }) 599 | each(commits, function (c) { 600 | each(c.to_parents, function (_, p) { 601 | delete leaves[p] 602 | }) 603 | }) 604 | return leaves 605 | } 606 | 607 | self.get_ancestors = function (id_or_set) { 608 | var frontier = null 609 | if (typeof(id_or_set) == 'object') { 610 | frontier = Object.keys(id_or_set) 611 | } else { 612 | frontier = [id_or_set] 613 | } 614 | var ancestors = {} 615 | while (frontier.length > 0) { 616 | var next = frontier.shift() 617 | each(self.commits[next].to_parents, function (_, p) { 618 | if (!ancestors[p]) { 619 | ancestors[p] = self.commits[p] 620 | frontier.push(p) 621 | } 622 | }) 623 | } 624 | return ancestors 625 | } 626 | 627 | return self 628 | } 629 | 630 | /////////////// 631 | 632 | function guid() { 633 | var x = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' 634 | var s = [] 635 | for (var i = 0; i < 15; i++) { 636 | s.push(x[Math.floor(Math.random() * x.length)]) 637 | } 638 | return s.join('') 639 | } 640 | 641 | function each(o, cb) { 642 | if (o instanceof Array) { 643 | for (var i = 0; i < o.length; i++) { 644 | if (cb(o[i], i, o) == false) 645 | return false 646 | } 647 | } else { 648 | for (var k in o) { 649 | if (o.hasOwnProperty(k)) { 650 | if (cb(o[k], k, o) == false) 651 | return false 652 | } 653 | } 654 | } 655 | return true 656 | } 657 | 658 | function map_array(a, f) { 659 | var b = [] 660 | each(a, function (v, k) { b[k] = f(v) }) 661 | return b 662 | } 663 | 664 | function extend(a, b) { 665 | each(b, function (x, key) { a[key] = x }) 666 | return a 667 | } 668 | 669 | function intersection(a, b) { 670 | var common = {} 671 | each(a, function (_, x) { 672 | if (b[x]) { 673 | common[x] = a[x] 674 | } 675 | }) 676 | return common 677 | } 678 | 679 | /////////////// 680 | 681 | var DIFF_DELETE = -1; 682 | var DIFF_INSERT = 1; 683 | var DIFF_EQUAL = 0; 684 | 685 | function get_merged_diff_patch(a, b, o) { 686 | var a_diff = get_diff_patch(o, a) 687 | var b_diff = get_diff_patch(o, b) 688 | var ds = [] 689 | var prev_d = null 690 | while (a_diff.length > 0 || b_diff.length > 0) { 691 | var d = a_diff.length == 0 ? 692 | b_diff.shift() : 693 | b_diff.length == 0 ? 694 | a_diff.shift() : 695 | a_diff[0][0] < b_diff[0][0] ? 696 | a_diff.shift() : 697 | a_diff[0][0] > b_diff[0][0] ? 698 | b_diff.shift() : 699 | a_diff[0][2] < b_diff[0][2] ? 700 | a_diff.shift() : 701 | b_diff.shift() 702 | if (prev_d && d[0] < prev_d[0] + prev_d[1]) { 703 | if (d[0] + d[1] > prev_d[0] + prev_d[1]) { 704 | prev_d[1] = d[0] + d[1] - prev_d[0] 705 | } 706 | prev_d[2] += d[2] 707 | } else { 708 | ds.push(d) 709 | prev_d = d 710 | } 711 | } 712 | return ds 713 | } 714 | 715 | function apply_diff_patch(s, diff) { 716 | var offset = 0 717 | for (var i = 0; i < diff.length; i++) { 718 | var d = diff[i] 719 | s = s.slice(0, d[0] + offset) + d[2] + s.slice(d[0] + offset + d[1]) 720 | offset += d[2].length - d[1] 721 | } 722 | return s 723 | } 724 | 725 | function diff_convert_to_my_format(d, factor) { 726 | if (factor === undefined) factor = 1 727 | var x = [] 728 | var ii = 0 729 | for (var i = 0; i < d.length; i++) { 730 | var dd = d[i] 731 | if (dd[0] == DIFF_EQUAL) { 732 | ii += dd[1].length 733 | continue 734 | } 735 | var xx = [ii, 0, ''] 736 | if (dd[0] == DIFF_INSERT * factor) { 737 | xx[2] = dd[1] 738 | } else if (dd[0] == DIFF_DELETE * factor) { 739 | xx[1] = dd[1].length 740 | ii += xx[1] 741 | } 742 | if (i + 1 < d.length) { 743 | dd = d[i + 1] 744 | if (dd[0] != DIFF_EQUAL) { 745 | if (dd[0] == DIFF_INSERT * factor) { 746 | xx[2] = dd[1] 747 | } else if (dd[0] == DIFF_DELETE * factor) { 748 | xx[1] = dd[1].length 749 | ii += xx[1] 750 | } 751 | i++ 752 | } 753 | } 754 | x.push(xx) 755 | } 756 | return x 757 | } 758 | 759 | function get_diff_patch(a, b) { 760 | return diff_convert_to_my_format(diff_main(a, b)) 761 | } 762 | 763 | function get_diff_patch_2(a, b) { 764 | var x = diff_main(a, b) 765 | return [diff_convert_to_my_format(x), 766 | diff_convert_to_my_format(x, -1)] 767 | } 768 | 769 | diffsync.get_diff_patch = get_diff_patch 770 | diffsync.get_diff_patch_2 = get_diff_patch_2 771 | 772 | /** 773 | * This library modifies the diff-patch-match library by Neil Fraser 774 | * by removing the patch and match functionality and certain advanced 775 | * options in the diff function. The original license is as follows: 776 | * 777 | * === 778 | * 779 | * Diff Match and Patch 780 | * 781 | * Copyright 2006 Google Inc. 782 | * http://code.google.com/p/google-diff-match-patch/ 783 | * 784 | * Licensed under the Apache License, Version 2.0 (the "License"); 785 | * you may not use this file except in compliance with the License. 786 | * You may obtain a copy of the License at 787 | * 788 | * http://www.apache.org/licenses/LICENSE-2.0 789 | * 790 | * Unless required by applicable law or agreed to in writing, software 791 | * distributed under the License is distributed on an "AS IS" BASIS, 792 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 793 | * See the License for the specific language governing permissions and 794 | * limitations under the License. 795 | */ 796 | 797 | 798 | /** 799 | * The data structure representing a diff is an array of tuples: 800 | * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']] 801 | * which means: delete 'Hello', add 'Goodbye' and keep ' world.' 802 | */ 803 | var DIFF_DELETE = -1; 804 | var DIFF_INSERT = 1; 805 | var DIFF_EQUAL = 0; 806 | 807 | 808 | /** 809 | * Find the differences between two texts. Simplifies the problem by stripping 810 | * any common prefix or suffix off the texts before diffing. 811 | * @param {string} text1 Old string to be diffed. 812 | * @param {string} text2 New string to be diffed. 813 | * @param {Int} cursor_pos Expected edit position in text1 (optional) 814 | * @return {Array} Array of diff tuples. 815 | */ 816 | function diff_main(text1, text2, cursor_pos) { 817 | // Check for equality (speedup). 818 | if (text1 == text2) { 819 | if (text1) { 820 | return [[DIFF_EQUAL, text1]]; 821 | } 822 | return []; 823 | } 824 | 825 | // Check cursor_pos within bounds 826 | if (cursor_pos < 0 || text1.length < cursor_pos) { 827 | cursor_pos = null; 828 | } 829 | 830 | // Trim off common prefix (speedup). 831 | var commonlength = diff_commonPrefix(text1, text2); 832 | var commonprefix = text1.substring(0, commonlength); 833 | text1 = text1.substring(commonlength); 834 | text2 = text2.substring(commonlength); 835 | 836 | // Trim off common suffix (speedup). 837 | commonlength = diff_commonSuffix(text1, text2); 838 | var commonsuffix = text1.substring(text1.length - commonlength); 839 | text1 = text1.substring(0, text1.length - commonlength); 840 | text2 = text2.substring(0, text2.length - commonlength); 841 | 842 | // Compute the diff on the middle block. 843 | var diffs = diff_compute_(text1, text2); 844 | 845 | // Restore the prefix and suffix. 846 | if (commonprefix) { 847 | diffs.unshift([DIFF_EQUAL, commonprefix]); 848 | } 849 | if (commonsuffix) { 850 | diffs.push([DIFF_EQUAL, commonsuffix]); 851 | } 852 | diff_cleanupMerge(diffs); 853 | if (cursor_pos != null) { 854 | diffs = fix_cursor(diffs, cursor_pos); 855 | } 856 | return diffs; 857 | }; 858 | 859 | 860 | /** 861 | * Find the differences between two texts. Assumes that the texts do not 862 | * have any common prefix or suffix. 863 | * @param {string} text1 Old string to be diffed. 864 | * @param {string} text2 New string to be diffed. 865 | * @return {Array} Array of diff tuples. 866 | */ 867 | function diff_compute_(text1, text2) { 868 | var diffs; 869 | 870 | if (!text1) { 871 | // Just add some text (speedup). 872 | return [[DIFF_INSERT, text2]]; 873 | } 874 | 875 | if (!text2) { 876 | // Just delete some text (speedup). 877 | return [[DIFF_DELETE, text1]]; 878 | } 879 | 880 | var longtext = text1.length > text2.length ? text1 : text2; 881 | var shorttext = text1.length > text2.length ? text2 : text1; 882 | var i = longtext.indexOf(shorttext); 883 | if (i != -1) { 884 | // Shorter text is inside the longer text (speedup). 885 | diffs = [[DIFF_INSERT, longtext.substring(0, i)], 886 | [DIFF_EQUAL, shorttext], 887 | [DIFF_INSERT, longtext.substring(i + shorttext.length)]]; 888 | // Swap insertions for deletions if diff is reversed. 889 | if (text1.length > text2.length) { 890 | diffs[0][0] = diffs[2][0] = DIFF_DELETE; 891 | } 892 | return diffs; 893 | } 894 | 895 | if (shorttext.length == 1) { 896 | // Single character string. 897 | // After the previous speedup, the character can't be an equality. 898 | return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]]; 899 | } 900 | 901 | // Check to see if the problem can be split in two. 902 | var hm = diff_halfMatch_(text1, text2); 903 | if (hm) { 904 | // A half-match was found, sort out the return data. 905 | var text1_a = hm[0]; 906 | var text1_b = hm[1]; 907 | var text2_a = hm[2]; 908 | var text2_b = hm[3]; 909 | var mid_common = hm[4]; 910 | // Send both pairs off for separate processing. 911 | var diffs_a = diff_main(text1_a, text2_a); 912 | var diffs_b = diff_main(text1_b, text2_b); 913 | // Merge the results. 914 | return diffs_a.concat([[DIFF_EQUAL, mid_common]], diffs_b); 915 | } 916 | 917 | return diff_bisect_(text1, text2); 918 | }; 919 | 920 | 921 | /** 922 | * Find the 'middle snake' of a diff, split the problem in two 923 | * and return the recursively constructed diff. 924 | * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. 925 | * @param {string} text1 Old string to be diffed. 926 | * @param {string} text2 New string to be diffed. 927 | * @return {Array} Array of diff tuples. 928 | * @private 929 | */ 930 | function diff_bisect_(text1, text2) { 931 | // Cache the text lengths to prevent multiple calls. 932 | var text1_length = text1.length; 933 | var text2_length = text2.length; 934 | var max_d = Math.ceil((text1_length + text2_length) / 2); 935 | var v_offset = max_d; 936 | var v_length = 2 * max_d; 937 | var v1 = new Array(v_length); 938 | var v2 = new Array(v_length); 939 | // Setting all elements to -1 is faster in Chrome & Firefox than mixing 940 | // integers and undefined. 941 | for (var x = 0; x < v_length; x++) { 942 | v1[x] = -1; 943 | v2[x] = -1; 944 | } 945 | v1[v_offset + 1] = 0; 946 | v2[v_offset + 1] = 0; 947 | var delta = text1_length - text2_length; 948 | // If the total number of characters is odd, then the front path will collide 949 | // with the reverse path. 950 | var front = (delta % 2 != 0); 951 | // Offsets for start and end of k loop. 952 | // Prevents mapping of space beyond the grid. 953 | var k1start = 0; 954 | var k1end = 0; 955 | var k2start = 0; 956 | var k2end = 0; 957 | for (var d = 0; d < max_d; d++) { 958 | // Walk the front path one step. 959 | for (var k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { 960 | var k1_offset = v_offset + k1; 961 | var x1; 962 | if (k1 == -d || (k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1])) { 963 | x1 = v1[k1_offset + 1]; 964 | } else { 965 | x1 = v1[k1_offset - 1] + 1; 966 | } 967 | var y1 = x1 - k1; 968 | while (x1 < text1_length && y1 < text2_length && 969 | text1.charAt(x1) == text2.charAt(y1)) { 970 | x1++; 971 | y1++; 972 | } 973 | v1[k1_offset] = x1; 974 | if (x1 > text1_length) { 975 | // Ran off the right of the graph. 976 | k1end += 2; 977 | } else if (y1 > text2_length) { 978 | // Ran off the bottom of the graph. 979 | k1start += 2; 980 | } else if (front) { 981 | var k2_offset = v_offset + delta - k1; 982 | if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1) { 983 | // Mirror x2 onto top-left coordinate system. 984 | var x2 = text1_length - v2[k2_offset]; 985 | if (x1 >= x2) { 986 | // Overlap detected. 987 | return diff_bisectSplit_(text1, text2, x1, y1); 988 | } 989 | } 990 | } 991 | } 992 | 993 | // Walk the reverse path one step. 994 | for (var k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { 995 | var k2_offset = v_offset + k2; 996 | var x2; 997 | if (k2 == -d || (k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1])) { 998 | x2 = v2[k2_offset + 1]; 999 | } else { 1000 | x2 = v2[k2_offset - 1] + 1; 1001 | } 1002 | var y2 = x2 - k2; 1003 | while (x2 < text1_length && y2 < text2_length && 1004 | text1.charAt(text1_length - x2 - 1) == 1005 | text2.charAt(text2_length - y2 - 1)) { 1006 | x2++; 1007 | y2++; 1008 | } 1009 | v2[k2_offset] = x2; 1010 | if (x2 > text1_length) { 1011 | // Ran off the left of the graph. 1012 | k2end += 2; 1013 | } else if (y2 > text2_length) { 1014 | // Ran off the top of the graph. 1015 | k2start += 2; 1016 | } else if (!front) { 1017 | var k1_offset = v_offset + delta - k2; 1018 | if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1) { 1019 | var x1 = v1[k1_offset]; 1020 | var y1 = v_offset + x1 - k1_offset; 1021 | // Mirror x2 onto top-left coordinate system. 1022 | x2 = text1_length - x2; 1023 | if (x1 >= x2) { 1024 | // Overlap detected. 1025 | return diff_bisectSplit_(text1, text2, x1, y1); 1026 | } 1027 | } 1028 | } 1029 | } 1030 | } 1031 | // Diff took too long and hit the deadline or 1032 | // number of diffs equals number of characters, no commonality at all. 1033 | return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]]; 1034 | }; 1035 | 1036 | 1037 | /** 1038 | * Given the location of the 'middle snake', split the diff in two parts 1039 | * and recurse. 1040 | * @param {string} text1 Old string to be diffed. 1041 | * @param {string} text2 New string to be diffed. 1042 | * @param {number} x Index of split point in text1. 1043 | * @param {number} y Index of split point in text2. 1044 | * @return {Array} Array of diff tuples. 1045 | */ 1046 | function diff_bisectSplit_(text1, text2, x, y) { 1047 | var text1a = text1.substring(0, x); 1048 | var text2a = text2.substring(0, y); 1049 | var text1b = text1.substring(x); 1050 | var text2b = text2.substring(y); 1051 | 1052 | // Compute both diffs serially. 1053 | var diffs = diff_main(text1a, text2a); 1054 | var diffsb = diff_main(text1b, text2b); 1055 | 1056 | return diffs.concat(diffsb); 1057 | }; 1058 | 1059 | 1060 | /** 1061 | * Determine the common prefix of two strings. 1062 | * @param {string} text1 First string. 1063 | * @param {string} text2 Second string. 1064 | * @return {number} The number of characters common to the start of each 1065 | * string. 1066 | */ 1067 | function diff_commonPrefix(text1, text2) { 1068 | // Quick check for common null cases. 1069 | if (!text1 || !text2 || text1.charAt(0) != text2.charAt(0)) { 1070 | return 0; 1071 | } 1072 | // Binary search. 1073 | // Performance analysis: http://neil.fraser.name/news/2007/10/09/ 1074 | var pointermin = 0; 1075 | var pointermax = Math.min(text1.length, text2.length); 1076 | var pointermid = pointermax; 1077 | var pointerstart = 0; 1078 | while (pointermin < pointermid) { 1079 | if (text1.substring(pointerstart, pointermid) == 1080 | text2.substring(pointerstart, pointermid)) { 1081 | pointermin = pointermid; 1082 | pointerstart = pointermin; 1083 | } else { 1084 | pointermax = pointermid; 1085 | } 1086 | pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); 1087 | } 1088 | return pointermid; 1089 | }; 1090 | 1091 | 1092 | /** 1093 | * Determine the common suffix of two strings. 1094 | * @param {string} text1 First string. 1095 | * @param {string} text2 Second string. 1096 | * @return {number} The number of characters common to the end of each string. 1097 | */ 1098 | function diff_commonSuffix(text1, text2) { 1099 | // Quick check for common null cases. 1100 | if (!text1 || !text2 || 1101 | text1.charAt(text1.length - 1) != text2.charAt(text2.length - 1)) { 1102 | return 0; 1103 | } 1104 | // Binary search. 1105 | // Performance analysis: http://neil.fraser.name/news/2007/10/09/ 1106 | var pointermin = 0; 1107 | var pointermax = Math.min(text1.length, text2.length); 1108 | var pointermid = pointermax; 1109 | var pointerend = 0; 1110 | while (pointermin < pointermid) { 1111 | if (text1.substring(text1.length - pointermid, text1.length - pointerend) == 1112 | text2.substring(text2.length - pointermid, text2.length - pointerend)) { 1113 | pointermin = pointermid; 1114 | pointerend = pointermin; 1115 | } else { 1116 | pointermax = pointermid; 1117 | } 1118 | pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); 1119 | } 1120 | return pointermid; 1121 | }; 1122 | 1123 | 1124 | /** 1125 | * Do the two texts share a substring which is at least half the length of the 1126 | * longer text? 1127 | * This speedup can produce non-minimal diffs. 1128 | * @param {string} text1 First string. 1129 | * @param {string} text2 Second string. 1130 | * @return {Array.} Five element Array, containing the prefix of 1131 | * text1, the suffix of text1, the prefix of text2, the suffix of 1132 | * text2 and the common middle. Or null if there was no match. 1133 | */ 1134 | function diff_halfMatch_(text1, text2) { 1135 | var longtext = text1.length > text2.length ? text1 : text2; 1136 | var shorttext = text1.length > text2.length ? text2 : text1; 1137 | if (longtext.length < 4 || shorttext.length * 2 < longtext.length) { 1138 | return null; // Pointless. 1139 | } 1140 | 1141 | /** 1142 | * Does a substring of shorttext exist within longtext such that the substring 1143 | * is at least half the length of longtext? 1144 | * Closure, but does not reference any external variables. 1145 | * @param {string} longtext Longer string. 1146 | * @param {string} shorttext Shorter string. 1147 | * @param {number} i Start index of quarter length substring within longtext. 1148 | * @return {Array.} Five element Array, containing the prefix of 1149 | * longtext, the suffix of longtext, the prefix of shorttext, the suffix 1150 | * of shorttext and the common middle. Or null if there was no match. 1151 | * @private 1152 | */ 1153 | function diff_halfMatchI_(longtext, shorttext, i) { 1154 | // Start with a 1/4 length substring at position i as a seed. 1155 | var seed = longtext.substring(i, i + Math.floor(longtext.length / 4)); 1156 | var j = -1; 1157 | var best_common = ''; 1158 | var best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b; 1159 | while ((j = shorttext.indexOf(seed, j + 1)) != -1) { 1160 | var prefixLength = diff_commonPrefix(longtext.substring(i), 1161 | shorttext.substring(j)); 1162 | var suffixLength = diff_commonSuffix(longtext.substring(0, i), 1163 | shorttext.substring(0, j)); 1164 | if (best_common.length < suffixLength + prefixLength) { 1165 | best_common = shorttext.substring(j - suffixLength, j) + 1166 | shorttext.substring(j, j + prefixLength); 1167 | best_longtext_a = longtext.substring(0, i - suffixLength); 1168 | best_longtext_b = longtext.substring(i + prefixLength); 1169 | best_shorttext_a = shorttext.substring(0, j - suffixLength); 1170 | best_shorttext_b = shorttext.substring(j + prefixLength); 1171 | } 1172 | } 1173 | if (best_common.length * 2 >= longtext.length) { 1174 | return [best_longtext_a, best_longtext_b, 1175 | best_shorttext_a, best_shorttext_b, best_common]; 1176 | } else { 1177 | return null; 1178 | } 1179 | } 1180 | 1181 | // First check if the second quarter is the seed for a half-match. 1182 | var hm1 = diff_halfMatchI_(longtext, shorttext, 1183 | Math.ceil(longtext.length / 4)); 1184 | // Check again based on the third quarter. 1185 | var hm2 = diff_halfMatchI_(longtext, shorttext, 1186 | Math.ceil(longtext.length / 2)); 1187 | var hm; 1188 | if (!hm1 && !hm2) { 1189 | return null; 1190 | } else if (!hm2) { 1191 | hm = hm1; 1192 | } else if (!hm1) { 1193 | hm = hm2; 1194 | } else { 1195 | // Both matched. Select the longest. 1196 | hm = hm1[4].length > hm2[4].length ? hm1 : hm2; 1197 | } 1198 | 1199 | // A half-match was found, sort out the return data. 1200 | var text1_a, text1_b, text2_a, text2_b; 1201 | if (text1.length > text2.length) { 1202 | text1_a = hm[0]; 1203 | text1_b = hm[1]; 1204 | text2_a = hm[2]; 1205 | text2_b = hm[3]; 1206 | } else { 1207 | text2_a = hm[0]; 1208 | text2_b = hm[1]; 1209 | text1_a = hm[2]; 1210 | text1_b = hm[3]; 1211 | } 1212 | var mid_common = hm[4]; 1213 | return [text1_a, text1_b, text2_a, text2_b, mid_common]; 1214 | }; 1215 | 1216 | 1217 | /** 1218 | * Reorder and merge like edit sections. Merge equalities. 1219 | * Any edit section can move as long as it doesn't cross an equality. 1220 | * @param {Array} diffs Array of diff tuples. 1221 | */ 1222 | function diff_cleanupMerge(diffs) { 1223 | diffs.push([DIFF_EQUAL, '']); // Add a dummy entry at the end. 1224 | var pointer = 0; 1225 | var count_delete = 0; 1226 | var count_insert = 0; 1227 | var text_delete = ''; 1228 | var text_insert = ''; 1229 | var commonlength; 1230 | while (pointer < diffs.length) { 1231 | switch (diffs[pointer][0]) { 1232 | case DIFF_INSERT: 1233 | count_insert++; 1234 | text_insert += diffs[pointer][1]; 1235 | pointer++; 1236 | break; 1237 | case DIFF_DELETE: 1238 | count_delete++; 1239 | text_delete += diffs[pointer][1]; 1240 | pointer++; 1241 | break; 1242 | case DIFF_EQUAL: 1243 | // Upon reaching an equality, check for prior redundancies. 1244 | if (count_delete + count_insert > 1) { 1245 | if (count_delete !== 0 && count_insert !== 0) { 1246 | // Factor out any common prefixies. 1247 | commonlength = diff_commonPrefix(text_insert, text_delete); 1248 | if (commonlength !== 0) { 1249 | if ((pointer - count_delete - count_insert) > 0 && 1250 | diffs[pointer - count_delete - count_insert - 1][0] == 1251 | DIFF_EQUAL) { 1252 | diffs[pointer - count_delete - count_insert - 1][1] += 1253 | text_insert.substring(0, commonlength); 1254 | } else { 1255 | diffs.splice(0, 0, [DIFF_EQUAL, 1256 | text_insert.substring(0, commonlength)]); 1257 | pointer++; 1258 | } 1259 | text_insert = text_insert.substring(commonlength); 1260 | text_delete = text_delete.substring(commonlength); 1261 | } 1262 | // Factor out any common suffixies. 1263 | commonlength = diff_commonSuffix(text_insert, text_delete); 1264 | if (commonlength !== 0) { 1265 | diffs[pointer][1] = text_insert.substring(text_insert.length - 1266 | commonlength) + diffs[pointer][1]; 1267 | text_insert = text_insert.substring(0, text_insert.length - 1268 | commonlength); 1269 | text_delete = text_delete.substring(0, text_delete.length - 1270 | commonlength); 1271 | } 1272 | } 1273 | // Delete the offending records and add the merged ones. 1274 | if (count_delete === 0) { 1275 | diffs.splice(pointer - count_insert, 1276 | count_delete + count_insert, [DIFF_INSERT, text_insert]); 1277 | } else if (count_insert === 0) { 1278 | diffs.splice(pointer - count_delete, 1279 | count_delete + count_insert, [DIFF_DELETE, text_delete]); 1280 | } else { 1281 | diffs.splice(pointer - count_delete - count_insert, 1282 | count_delete + count_insert, [DIFF_DELETE, text_delete], 1283 | [DIFF_INSERT, text_insert]); 1284 | } 1285 | pointer = pointer - count_delete - count_insert + 1286 | (count_delete ? 1 : 0) + (count_insert ? 1 : 0) + 1; 1287 | } else if (pointer !== 0 && diffs[pointer - 1][0] == DIFF_EQUAL) { 1288 | // Merge this equality with the previous one. 1289 | diffs[pointer - 1][1] += diffs[pointer][1]; 1290 | diffs.splice(pointer, 1); 1291 | } else { 1292 | pointer++; 1293 | } 1294 | count_insert = 0; 1295 | count_delete = 0; 1296 | text_delete = ''; 1297 | text_insert = ''; 1298 | break; 1299 | } 1300 | } 1301 | if (diffs[diffs.length - 1][1] === '') { 1302 | diffs.pop(); // Remove the dummy entry at the end. 1303 | } 1304 | 1305 | // Second pass: look for single edits surrounded on both sides by equalities 1306 | // which can be shifted sideways to eliminate an equality. 1307 | // e.g: ABAC -> ABAC 1308 | var changes = false; 1309 | pointer = 1; 1310 | // Intentionally ignore the first and last element (don't need checking). 1311 | while (pointer < diffs.length - 1) { 1312 | if (diffs[pointer - 1][0] == DIFF_EQUAL && 1313 | diffs[pointer + 1][0] == DIFF_EQUAL) { 1314 | // This is a single edit surrounded by equalities. 1315 | if (diffs[pointer][1].substring(diffs[pointer][1].length - 1316 | diffs[pointer - 1][1].length) == diffs[pointer - 1][1]) { 1317 | // Shift the edit over the previous equality. 1318 | diffs[pointer][1] = diffs[pointer - 1][1] + 1319 | diffs[pointer][1].substring(0, diffs[pointer][1].length - 1320 | diffs[pointer - 1][1].length); 1321 | diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1]; 1322 | diffs.splice(pointer - 1, 1); 1323 | changes = true; 1324 | } else if (diffs[pointer][1].substring(0, diffs[pointer + 1][1].length) == 1325 | diffs[pointer + 1][1]) { 1326 | // Shift the edit over the next equality. 1327 | diffs[pointer - 1][1] += diffs[pointer + 1][1]; 1328 | diffs[pointer][1] = 1329 | diffs[pointer][1].substring(diffs[pointer + 1][1].length) + 1330 | diffs[pointer + 1][1]; 1331 | diffs.splice(pointer + 1, 1); 1332 | changes = true; 1333 | } 1334 | } 1335 | pointer++; 1336 | } 1337 | // If shifts were made, the diff needs reordering and another shift sweep. 1338 | if (changes) { 1339 | diff_cleanupMerge(diffs); 1340 | } 1341 | }; 1342 | 1343 | 1344 | /* 1345 | * Modify a diff such that the cursor position points to the start of a change: 1346 | * E.g. 1347 | * cursor_normalize_diff([[DIFF_EQUAL, 'abc']], 1) 1348 | * => [1, [[DIFF_EQUAL, 'a'], [DIFF_EQUAL, 'bc']]] 1349 | * cursor_normalize_diff([[DIFF_INSERT, 'new'], [DIFF_DELETE, 'xyz']], 2) 1350 | * => [2, [[DIFF_INSERT, 'new'], [DIFF_DELETE, 'xy'], [DIFF_DELETE, 'z']]] 1351 | * 1352 | * @param {Array} diffs Array of diff tuples 1353 | * @param {Int} cursor_pos Suggested edit position. Must not be out of bounds! 1354 | * @return {Array} A tuple [cursor location in the modified diff, modified diff] 1355 | */ 1356 | function cursor_normalize_diff (diffs, cursor_pos) { 1357 | if (cursor_pos === 0) { 1358 | return [DIFF_EQUAL, diffs]; 1359 | } 1360 | for (var current_pos = 0, i = 0; i < diffs.length; i++) { 1361 | var d = diffs[i]; 1362 | if (d[0] === DIFF_DELETE || d[0] === DIFF_EQUAL) { 1363 | var next_pos = current_pos + d[1].length; 1364 | if (cursor_pos === next_pos) { 1365 | return [i + 1, diffs]; 1366 | } else if (cursor_pos < next_pos) { 1367 | // copy to prevent side effects 1368 | diffs = diffs.slice(); 1369 | // split d into two diff changes 1370 | var split_pos = cursor_pos - current_pos; 1371 | var d_left = [d[0], d[1].slice(0, split_pos)]; 1372 | var d_right = [d[0], d[1].slice(split_pos)]; 1373 | diffs.splice(i, 1, d_left, d_right); 1374 | return [i + 1, diffs]; 1375 | } else { 1376 | current_pos = next_pos; 1377 | } 1378 | } 1379 | } 1380 | throw new Error('cursor_pos is out of bounds!') 1381 | } 1382 | 1383 | /* 1384 | * Modify a diff such that the edit position is "shifted" to the proposed edit location (cursor_position). 1385 | * 1386 | * Case 1) 1387 | * Check if a naive shift is possible: 1388 | * [0, X], [ 1, Y] -> [ 1, Y], [0, X] (if X + Y === Y + X) 1389 | * [0, X], [-1, Y] -> [-1, Y], [0, X] (if X + Y === Y + X) - holds same result 1390 | * Case 2) 1391 | * Check if the following shifts are possible: 1392 | * [0, 'pre'], [ 1, 'prefix'] -> [ 1, 'pre'], [0, 'pre'], [ 1, 'fix'] 1393 | * [0, 'pre'], [-1, 'prefix'] -> [-1, 'pre'], [0, 'pre'], [-1, 'fix'] 1394 | * ^ ^ 1395 | * d d_next 1396 | * 1397 | * @param {Array} diffs Array of diff tuples 1398 | * @param {Int} cursor_pos Suggested edit position. Must not be out of bounds! 1399 | * @return {Array} Array of diff tuples 1400 | */ 1401 | function fix_cursor (diffs, cursor_pos) { 1402 | var norm = cursor_normalize_diff(diffs, cursor_pos); 1403 | var ndiffs = norm[1]; 1404 | var cursor_pointer = norm[0]; 1405 | var d = ndiffs[cursor_pointer]; 1406 | var d_next = ndiffs[cursor_pointer + 1]; 1407 | 1408 | if (d == null) { 1409 | // Text was deleted from end of original string, 1410 | // cursor is now out of bounds in new string 1411 | return diffs; 1412 | } else if (d[0] !== DIFF_EQUAL) { 1413 | // A modification happened at the cursor location. 1414 | // This is the expected outcome, so we can return the original diff. 1415 | return diffs; 1416 | } else { 1417 | if (d_next != null && d[1] + d_next[1] === d_next[1] + d[1]) { 1418 | // Case 1) 1419 | // It is possible to perform a naive shift 1420 | ndiffs.splice(cursor_pointer, 2, d_next, d) 1421 | return merge_tuples(ndiffs, cursor_pointer, 2) 1422 | } else if (d_next != null && d_next[1].indexOf(d[1]) === 0) { 1423 | // Case 2) 1424 | // d[1] is a prefix of d_next[1] 1425 | // We can assume that d_next[0] !== 0, since d[0] === 0 1426 | // Shift edit locations.. 1427 | ndiffs.splice(cursor_pointer, 2, [d_next[0], d[1]], [0, d[1]]); 1428 | var suffix = d_next[1].slice(d[1].length); 1429 | if (suffix.length > 0) { 1430 | ndiffs.splice(cursor_pointer + 2, 0, [d_next[0], suffix]); 1431 | } 1432 | return merge_tuples(ndiffs, cursor_pointer, 3) 1433 | } else { 1434 | // Not possible to perform any modification 1435 | return diffs; 1436 | } 1437 | } 1438 | 1439 | } 1440 | 1441 | /* 1442 | * Try to merge tuples with their neigbors in a given range. 1443 | * E.g. [0, 'a'], [0, 'b'] -> [0, 'ab'] 1444 | * 1445 | * @param {Array} diffs Array of diff tuples. 1446 | * @param {Int} start Position of the first element to merge (diffs[start] is also merged with diffs[start - 1]). 1447 | * @param {Int} length Number of consecutive elements to check. 1448 | * @return {Array} Array of merged diff tuples. 1449 | */ 1450 | function merge_tuples (diffs, start, length) { 1451 | // Check from (start-1) to (start+length). 1452 | for (var i = start + length - 1; i >= 0 && i >= start - 1; i--) { 1453 | if (i + 1 < diffs.length) { 1454 | var left_d = diffs[i]; 1455 | var right_d = diffs[i+1]; 1456 | if (left_d[0] === right_d[1]) { 1457 | diffs.splice(i, 2, [left_d[0], left_d[1] + right_d[1]]); 1458 | } 1459 | } 1460 | } 1461 | return diffs; 1462 | } 1463 | -------------------------------------------------------------------------------- /extras/getCaretCoordinates.js: -------------------------------------------------------------------------------- 1 | 2 | // from https://github.com/component/textarea-caret-position 3 | 4 | /* jshint browser: true */ 5 | 6 | (function () { 7 | 8 | // We'll copy the properties below into the mirror div. 9 | // Note that some browsers, such as Firefox, do not concatenate properties 10 | // into their shorthand (e.g. padding-top, padding-bottom etc. -> padding), 11 | // so we have to list every single property explicitly. 12 | var properties = [ 13 | 'direction', // RTL support 14 | 'boxSizing', 15 | 'width', // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does 16 | 'height', 17 | 'overflowX', 18 | 'overflowY', // copy the scrollbar for IE 19 | 20 | 'borderTopWidth', 21 | 'borderRightWidth', 22 | 'borderBottomWidth', 23 | 'borderLeftWidth', 24 | 'borderStyle', 25 | 26 | 'paddingTop', 27 | 'paddingRight', 28 | 'paddingBottom', 29 | 'paddingLeft', 30 | 31 | // https://developer.mozilla.org/en-US/docs/Web/CSS/font 32 | 'fontStyle', 33 | 'fontVariant', 34 | 'fontWeight', 35 | 'fontStretch', 36 | 'fontSize', 37 | 'fontSizeAdjust', 38 | 'lineHeight', 39 | 'fontFamily', 40 | 41 | 'textAlign', 42 | 'textTransform', 43 | 'textIndent', 44 | 'textDecoration', // might not make a difference, but better be safe 45 | 46 | 'letterSpacing', 47 | 'wordSpacing', 48 | 49 | 'tabSize', 50 | 'MozTabSize' 51 | 52 | ]; 53 | 54 | var isBrowser = (typeof window !== 'undefined'); 55 | var isFirefox = (isBrowser && window.mozInnerScreenX != null); 56 | 57 | function getCaretCoordinates(element, position, options) { 58 | if (!isBrowser) { 59 | throw new Error('textarea-caret-position#getCaretCoordinates should only be called in a browser'); 60 | } 61 | 62 | var debug = options && options.debug || false; 63 | if (debug) { 64 | var el = document.querySelector('#input-textarea-caret-position-mirror-div'); 65 | if (el) el.parentNode.removeChild(el); 66 | } 67 | 68 | // The mirror div will replicate the textarea's style 69 | var div = document.createElement('div'); 70 | div.id = 'input-textarea-caret-position-mirror-div'; 71 | document.body.appendChild(div); 72 | 73 | var style = div.style; 74 | var computed = window.getComputedStyle ? window.getComputedStyle(element) : element.currentStyle; // currentStyle for IE < 9 75 | var isInput = element.nodeName === 'INPUT'; 76 | 77 | // Default textarea styles 78 | style.whiteSpace = 'pre-wrap'; 79 | if (!isInput) 80 | style.wordWrap = 'break-word'; // only for textarea-s 81 | 82 | // Position off-screen 83 | style.position = 'absolute'; // required to return coordinates properly 84 | if (!debug) 85 | style.visibility = 'hidden'; // not 'display: none' because we want rendering 86 | 87 | // Transfer the element's properties to the div 88 | properties.forEach(function (prop) { 89 | if (isInput && prop === 'lineHeight') { 90 | // Special case for s because text is rendered centered and line height may be != height 91 | style.lineHeight = computed.height; 92 | } else { 93 | style[prop] = computed[prop]; 94 | } 95 | }); 96 | 97 | if (isFirefox) { 98 | // Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275 99 | if (element.scrollHeight > parseInt(computed.height)) 100 | style.overflowY = 'scroll'; 101 | } else { 102 | style.overflow = 'hidden'; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll' 103 | } 104 | 105 | div.textContent = element.value.substring(0, position); 106 | // The second special handling for input type="text" vs textarea: 107 | // spaces need to be replaced with non-breaking spaces - http://stackoverflow.com/a/13402035/1269037 108 | if (isInput) 109 | div.textContent = div.textContent.replace(/\s/g, '\u00a0'); 110 | 111 | var span = document.createElement('span'); 112 | // Wrapping must be replicated *exactly*, including when a long word gets 113 | // onto the next line, with whitespace at the end of the line before (#7). 114 | // The *only* reliable way to do that is to copy the *entire* rest of the 115 | // textarea's content into the created at the caret position. 116 | // For inputs, just '.' would be enough, but no need to bother. 117 | span.textContent = element.value.substring(position) || '.'; // || because a completely empty faux span doesn't render at all 118 | div.appendChild(span); 119 | 120 | var coordinates = { 121 | top: span.offsetTop + parseInt(computed['borderTopWidth']), 122 | left: span.offsetLeft + parseInt(computed['borderLeftWidth']), 123 | height: parseInt(computed['lineHeight']) 124 | }; 125 | 126 | if (debug) { 127 | span.style.backgroundColor = '#aaa'; 128 | } else { 129 | document.body.removeChild(div); 130 | } 131 | 132 | return coordinates; 133 | } 134 | 135 | if (typeof module != 'undefined' && typeof module.exports != 'undefined') { 136 | module.exports = getCaretCoordinates; 137 | } else if(isBrowser) { 138 | window.getCaretCoordinates = getCaretCoordinates; 139 | } 140 | 141 | }()); 142 | -------------------------------------------------------------------------------- /extras/sockjs.js: -------------------------------------------------------------------------------- 1 | /* SockJS client, version 0.3.4, http://sockjs.org, MIT License 2 | 3 | Copyright (c) 2011-2012 VMware, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | */ 23 | 24 | // JSON2 by Douglas Crockford (minified). 25 | var JSON;JSON||(JSON={}),function(){function str(a,b){var c,d,e,f,g=gap,h,i=b[a];i&&typeof i=="object"&&typeof i.toJSON=="function"&&(i=i.toJSON(a)),typeof rep=="function"&&(i=rep.call(b,a,i));switch(typeof i){case"string":return quote(i);case"number":return isFinite(i)?String(i):"null";case"boolean":case"null":return String(i);case"object":if(!i)return"null";gap+=indent,h=[];if(Object.prototype.toString.apply(i)==="[object Array]"){f=i.length;for(c=0;c1?this._listeners[a]=d.slice(0,e).concat(d.slice(e+1)):delete this._listeners[a];return}return},d.prototype.dispatchEvent=function(a){var b=a.type,c=Array.prototype.slice.call(arguments,0);this["on"+b]&&this["on"+b].apply(this,c);if(this._listeners&&b in this._listeners)for(var d=0;d=3e3&&a<=4999},c.countRTO=function(a){var b;return a>100?b=3*a:b=a+200,b},c.log=function(){b.console&&console.log&&console.log.apply&&console.log.apply(console,arguments)},c.bind=function(a,b){return a.bind?a.bind(b):function(){return a.apply(b,arguments)}},c.flatUrl=function(a){return a.indexOf("?")===-1&&a.indexOf("#")===-1},c.amendUrl=function(b){var d=a.location;if(!b)throw new Error("Wrong url for SockJS");if(!c.flatUrl(b))throw new Error("Only basic urls are supported in SockJS");return b.indexOf("//")===0&&(b=d.protocol+b),b.indexOf("/")===0&&(b=d.protocol+"//"+d.host+b),b=b.replace(/[/]+$/,""),b},c.arrIndexOf=function(a,b){for(var c=0;c=0},c.delay=function(a,b){return typeof a=="function"&&(b=a,a=0),setTimeout(b,a)};var i=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,j={"\0":"\\u0000","\x01":"\\u0001","\x02":"\\u0002","\x03":"\\u0003","\x04":"\\u0004","\x05":"\\u0005","\x06":"\\u0006","\x07":"\\u0007","\b":"\\b","\t":"\\t","\n":"\\n","\x0b":"\\u000b","\f":"\\f","\r":"\\r","\x0e":"\\u000e","\x0f":"\\u000f","\x10":"\\u0010","\x11":"\\u0011","\x12":"\\u0012","\x13":"\\u0013","\x14":"\\u0014","\x15":"\\u0015","\x16":"\\u0016","\x17":"\\u0017","\x18":"\\u0018","\x19":"\\u0019","\x1a":"\\u001a","\x1b":"\\u001b","\x1c":"\\u001c","\x1d":"\\u001d","\x1e":"\\u001e","\x1f":"\\u001f",'"':'\\"',"\\":"\\\\","\x7f":"\\u007f","\x80":"\\u0080","\x81":"\\u0081","\x82":"\\u0082","\x83":"\\u0083","\x84":"\\u0084","\x85":"\\u0085","\x86":"\\u0086","\x87":"\\u0087","\x88":"\\u0088","\x89":"\\u0089","\x8a":"\\u008a","\x8b":"\\u008b","\x8c":"\\u008c","\x8d":"\\u008d","\x8e":"\\u008e","\x8f":"\\u008f","\x90":"\\u0090","\x91":"\\u0091","\x92":"\\u0092","\x93":"\\u0093","\x94":"\\u0094","\x95":"\\u0095","\x96":"\\u0096","\x97":"\\u0097","\x98":"\\u0098","\x99":"\\u0099","\x9a":"\\u009a","\x9b":"\\u009b","\x9c":"\\u009c","\x9d":"\\u009d","\x9e":"\\u009e","\x9f":"\\u009f","\xad":"\\u00ad","\u0600":"\\u0600","\u0601":"\\u0601","\u0602":"\\u0602","\u0603":"\\u0603","\u0604":"\\u0604","\u070f":"\\u070f","\u17b4":"\\u17b4","\u17b5":"\\u17b5","\u200c":"\\u200c","\u200d":"\\u200d","\u200e":"\\u200e","\u200f":"\\u200f","\u2028":"\\u2028","\u2029":"\\u2029","\u202a":"\\u202a","\u202b":"\\u202b","\u202c":"\\u202c","\u202d":"\\u202d","\u202e":"\\u202e","\u202f":"\\u202f","\u2060":"\\u2060","\u2061":"\\u2061","\u2062":"\\u2062","\u2063":"\\u2063","\u2064":"\\u2064","\u2065":"\\u2065","\u2066":"\\u2066","\u2067":"\\u2067","\u2068":"\\u2068","\u2069":"\\u2069","\u206a":"\\u206a","\u206b":"\\u206b","\u206c":"\\u206c","\u206d":"\\u206d","\u206e":"\\u206e","\u206f":"\\u206f","\ufeff":"\\ufeff","\ufff0":"\\ufff0","\ufff1":"\\ufff1","\ufff2":"\\ufff2","\ufff3":"\\ufff3","\ufff4":"\\ufff4","\ufff5":"\\ufff5","\ufff6":"\\ufff6","\ufff7":"\\ufff7","\ufff8":"\\ufff8","\ufff9":"\\ufff9","\ufffa":"\\ufffa","\ufffb":"\\ufffb","\ufffc":"\\ufffc","\ufffd":"\\ufffd","\ufffe":"\\ufffe","\uffff":"\\uffff"},k=/[\x00-\x1f\ud800-\udfff\ufffe\uffff\u0300-\u0333\u033d-\u0346\u034a-\u034c\u0350-\u0352\u0357-\u0358\u035c-\u0362\u0374\u037e\u0387\u0591-\u05af\u05c4\u0610-\u0617\u0653-\u0654\u0657-\u065b\u065d-\u065e\u06df-\u06e2\u06eb-\u06ec\u0730\u0732-\u0733\u0735-\u0736\u073a\u073d\u073f-\u0741\u0743\u0745\u0747\u07eb-\u07f1\u0951\u0958-\u095f\u09dc-\u09dd\u09df\u0a33\u0a36\u0a59-\u0a5b\u0a5e\u0b5c-\u0b5d\u0e38-\u0e39\u0f43\u0f4d\u0f52\u0f57\u0f5c\u0f69\u0f72-\u0f76\u0f78\u0f80-\u0f83\u0f93\u0f9d\u0fa2\u0fa7\u0fac\u0fb9\u1939-\u193a\u1a17\u1b6b\u1cda-\u1cdb\u1dc0-\u1dcf\u1dfc\u1dfe\u1f71\u1f73\u1f75\u1f77\u1f79\u1f7b\u1f7d\u1fbb\u1fbe\u1fc9\u1fcb\u1fd3\u1fdb\u1fe3\u1feb\u1fee-\u1fef\u1ff9\u1ffb\u1ffd\u2000-\u2001\u20d0-\u20d1\u20d4-\u20d7\u20e7-\u20e9\u2126\u212a-\u212b\u2329-\u232a\u2adc\u302b-\u302c\uaab2-\uaab3\uf900-\ufa0d\ufa10\ufa12\ufa15-\ufa1e\ufa20\ufa22\ufa25-\ufa26\ufa2a-\ufa2d\ufa30-\ufa6d\ufa70-\ufad9\ufb1d\ufb1f\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufb4e\ufff0-\uffff]/g,l,m=JSON&&JSON.stringify||function(a){return i.lastIndex=0,i.test(a)&&(a=a.replace(i,function(a){return j[a]})),'"'+a+'"'},n=function(a){var b,c={},d=[];for(b=0;b<65536;b++)d.push(String.fromCharCode(b));return a.lastIndex=0,d.join("").replace(a,function(a){return c[a]="\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4),""}),a.lastIndex=0,c};c.quote=function(a){var b=m(a);return k.lastIndex=0,k.test(b)?(l||(l=n(k)),b.replace(k,function(a){return l[a]})):b};var o=["websocket","xdr-streaming","xhr-streaming","iframe-eventsource","iframe-htmlfile","xdr-polling","xhr-polling","iframe-xhr-polling","jsonp-polling"];c.probeProtocols=function(){var a={};for(var b=0;b0&&h(a)};return c.websocket!==!1&&h(["websocket"]),d["xhr-streaming"]&&!c.null_origin?e.push("xhr-streaming"):d["xdr-streaming"]&&!c.cookie_needed&&!c.null_origin?e.push("xdr-streaming"):h(["iframe-eventsource","iframe-htmlfile"]),d["xhr-polling"]&&!c.null_origin?e.push("xhr-polling"):d["xdr-polling"]&&!c.cookie_needed&&!c.null_origin?e.push("xdr-polling"):h(["iframe-xhr-polling","jsonp-polling"]),e};var p="_sockjs_global";c.createHook=function(){var a="a"+c.random_string(8);if(!(p in b)){var d={};b[p]=function(a){return a in d||(d[a]={id:a,del:function(){delete d[a]}}),d[a]}}return b[p](a)},c.attachMessage=function(a){c.attachEvent("message",a)},c.attachEvent=function(c,d){typeof b.addEventListener!="undefined"?b.addEventListener(c,d,!1):(a.attachEvent("on"+c,d),b.attachEvent("on"+c,d))},c.detachMessage=function(a){c.detachEvent("message",a)},c.detachEvent=function(c,d){typeof b.addEventListener!="undefined"?b.removeEventListener(c,d,!1):(a.detachEvent("on"+c,d),b.detachEvent("on"+c,d))};var q={},r=!1,s=function(){for(var a in q)q[a](),delete q[a]},t=function(){if(r)return;r=!0,s()};c.attachEvent("unload",t),c.unload_add=function(a){var b=c.random_string(8);return q[b]=a,r&&c.delay(s),b},c.unload_del=function(a){a in q&&delete q[a]},c.createIframe=function(b,d){var e=a.createElement("iframe"),f,g,h=function(){clearTimeout(f);try{e.onload=null}catch(a){}e.onerror=null},i=function(){e&&(h(),setTimeout(function(){e&&e.parentNode.removeChild(e),e=null},0),c.unload_del(g))},j=function(a){e&&(i(),d(a))},k=function(a,b){try{e&&e.contentWindow&&e.contentWindow.postMessage(a,b)}catch(c){}};return e.src=b,e.style.display="none",e.style.position="absolute",e.onerror=function(){j("onerror")},e.onload=function(){clearTimeout(f),f=setTimeout(function(){j("onload timeout")},2e3)},a.body.appendChild(e),f=setTimeout(function(){j("timeout")},15e3),g=c.unload_add(i),{post:k,cleanup:i,loaded:h}},c.createHtmlfile=function(a,d){var e=new ActiveXObject("htmlfile"),f,g,i,j=function(){clearTimeout(f)},k=function(){e&&(j(),c.unload_del(g),i.parentNode.removeChild(i),i=e=null,CollectGarbage())},l=function(a){e&&(k(),d(a))},m=function(a,b){try{i&&i.contentWindow&&i.contentWindow.postMessage(a,b)}catch(c){}};e.open(),e.write(' --------------------------------------------------------------------------------