"use strict"; (function( window, document, jQuery, CodeMirror ){ var Mgly = {}; Mgly.Timer = function(){ var self = this; self.start = function() { self.t0 = new Date().getTime(); }; self.stop = function() { var t1 = new Date().getTime(); var d = t1 - self.t0; self.t0 = t1; return d; }; self.start(); }; Mgly.ChangeExpression = new RegExp(/(^(?![><\-])*\d+(?:,\d+)?)([acd])(\d+(?:,\d+)?)/); Mgly.DiffParser = function(diff) { var changes = []; var change_id = 0; // parse diff var diff_lines = diff.split(/\n/); for (var i = 0; i < diff_lines.length; ++i) { if (diff_lines[i].length == 0) continue; var change = {}; var test = Mgly.ChangeExpression.exec(diff_lines[i]); if (test == null) continue; // lines are zero-based var fr = test[1].split(','); change['lhs-line-from'] = fr[0] - 1; if (fr.length == 1) change['lhs-line-to'] = fr[0] - 1; else change['lhs-line-to'] = fr[1] - 1; var to = test[3].split(','); change['rhs-line-from'] = to[0] - 1; if (to.length == 1) change['rhs-line-to'] = to[0] - 1; else change['rhs-line-to'] = to[1] - 1; change['op'] = test[2]; changes[change_id++] = change; } return changes; }; Mgly.sizeOf = function(obj) { var size = 0, key; for (key in obj) { if (obj.hasOwnProperty(key)) size++; } return size; }; Mgly.LCS = function(x, y) { this.x = x.replace(/[ ]{1}/g, '\n'); this.y = y.replace(/[ ]{1}/g, '\n'); }; jQuery.extend(Mgly.LCS.prototype, { clear: function() { this.ready = 0; }, diff: function(added, removed) { var d = new Mgly.diff(this.x, this.y, {ignorews: false}); var changes = Mgly.DiffParser(d.normal_form()); var li = 0, lj = 0; for (var i = 0; i < changes.length; ++i) { var change = changes[i]; if (change.op != 'a') { // find the starting index of the line li = d.getLines('lhs').slice(0, change['lhs-line-from']).join(' ').length; // get the index of the the span of the change lj = change['lhs-line-to'] + 1; // get the changed text var lchange = d.getLines('lhs').slice(change['lhs-line-from'], lj).join(' '); if (change.op == 'd') lchange += ' ';// include the leading space else if (li > 0 && change.op == 'c') li += 1; // ignore leading space if not first word // output the changed index and text removed(li, li + lchange.length); } if (change.op != 'd') { // find the starting index of the line li = d.getLines('rhs').slice(0, change['rhs-line-from']).join(' ').length; // get the index of the the span of the change lj = change['rhs-line-to'] + 1; // get the changed text var rchange = d.getLines('rhs').slice(change['rhs-line-from'], lj).join(' '); if (change.op == 'a') rchange += ' ';// include the leading space else if (li > 0 && change.op == 'c') li += 1; // ignore leading space if not first word // output the changed index and text added(li, li + rchange.length); } } } }); Mgly.CodeifyText = function(settings) { this._max_code = 0; this._diff_codes = {}; this.ctxs = {}; this.options = {ignorews: false}; jQuery.extend(this, settings); this.lhs = settings.lhs.split('\n'); this.rhs = settings.rhs.split('\n'); }; jQuery.extend(Mgly.CodeifyText.prototype, { getCodes: function(side) { if (!this.ctxs.hasOwnProperty(side)) { var ctx = this._diff_ctx(this[side]); this.ctxs[side] = ctx; ctx.codes.length = Object.keys(ctx.codes).length; } return this.ctxs[side].codes; }, getLines: function(side) { return this.ctxs[side].lines; }, _diff_ctx: function(lines) { var ctx = {i: 0, codes: {}, lines: lines}; this._codeify(lines, ctx); return ctx; }, _codeify: function(lines, ctx) { var code = this._max_code; for (var i = 0; i < lines.length; ++i) { var line = lines[i]; if (this.options.ignorews) { line = line.replace(/\s+/g, ''); } if (this.options.ignorecase) { line = line.toLowerCase(); } var aCode = this._diff_codes[line]; if (aCode != undefined) { ctx.codes[i] = aCode; } else { this._max_code++; this._diff_codes[line] = this._max_code; ctx.codes[i] = this._max_code; } } } }); Mgly.diff = function(lhs, rhs, options) { var opts = jQuery.extend({ignorews: false}, options); this.codeify = new Mgly.CodeifyText({ lhs: lhs, rhs: rhs, options: opts }); var lhs_ctx = { codes: this.codeify.getCodes('lhs'), modified: {} }; var rhs_ctx = { codes: this.codeify.getCodes('rhs'), modified: {} }; var max = (lhs_ctx.codes.length + rhs_ctx.codes.length + 1); var vector_d = []; var vector_u = []; this._lcs(lhs_ctx, 0, lhs_ctx.codes.length, rhs_ctx, 0, rhs_ctx.codes.length, vector_u, vector_d); this._optimize(lhs_ctx); this._optimize(rhs_ctx); this.items = this._create_diffs(lhs_ctx, rhs_ctx); }; jQuery.extend(Mgly.diff.prototype, { changes: function() { return this.items; }, getLines: function(side) { return this.codeify.getLines(side); }, normal_form: function() { var nf = ''; for (var index = 0; index < this.items.length; ++index) { var item = this.items[index]; var lhs_str = ''; var rhs_str = ''; var change = 'c'; if (item.lhs_deleted_count == 0 && item.rhs_inserted_count > 0) change = 'a'; else if (item.lhs_deleted_count > 0 && item.rhs_inserted_count == 0) change = 'd'; if (item.lhs_deleted_count == 1) lhs_str = item.lhs_start + 1; else if (item.lhs_deleted_count == 0) lhs_str = item.lhs_start; else lhs_str = (item.lhs_start + 1) + ',' + (item.lhs_start + item.lhs_deleted_count); if (item.rhs_inserted_count == 1) rhs_str = item.rhs_start + 1; else if (item.rhs_inserted_count == 0) rhs_str = item.rhs_start; else rhs_str = (item.rhs_start + 1) + ',' + (item.rhs_start + item.rhs_inserted_count); nf += lhs_str + change + rhs_str + '\n'; var lhs_lines = this.getLines('lhs'); var rhs_lines = this.getLines('rhs'); if (rhs_lines && lhs_lines) { var i; // if rhs/lhs lines have been retained, output contextual diff for (i = item.lhs_start; i < item.lhs_start + item.lhs_deleted_count; ++i) { nf += '< ' + lhs_lines[i] + '\n'; } if (item.rhs_inserted_count && item.lhs_deleted_count) nf += '---\n'; for (i = item.rhs_start; i < item.rhs_start + item.rhs_inserted_count; ++i) { nf += '> ' + rhs_lines[i] + '\n'; } } } return nf; }, _lcs: function(lhs_ctx, lhs_lower, lhs_upper, rhs_ctx, rhs_lower, rhs_upper, vector_u, vector_d) { while ( (lhs_lower < lhs_upper) && (rhs_lower < rhs_upper) && (lhs_ctx.codes[lhs_lower] == rhs_ctx.codes[rhs_lower]) ) { ++lhs_lower; ++rhs_lower; } while ( (lhs_lower < lhs_upper) && (rhs_lower < rhs_upper) && (lhs_ctx.codes[lhs_upper - 1] == rhs_ctx.codes[rhs_upper - 1]) ) { --lhs_upper; --rhs_upper; } if (lhs_lower == lhs_upper) { while (rhs_lower < rhs_upper) { rhs_ctx.modified[ rhs_lower++ ] = true; } } else if (rhs_lower == rhs_upper) { while (lhs_lower < lhs_upper) { lhs_ctx.modified[ lhs_lower++ ] = true; } } else { var sms = this._sms(lhs_ctx, lhs_lower, lhs_upper, rhs_ctx, rhs_lower, rhs_upper, vector_u, vector_d); this._lcs(lhs_ctx, lhs_lower, sms.x, rhs_ctx, rhs_lower, sms.y, vector_u, vector_d); this._lcs(lhs_ctx, sms.x, lhs_upper, rhs_ctx, sms.y, rhs_upper, vector_u, vector_d); } }, _sms: function(lhs_ctx, lhs_lower, lhs_upper, rhs_ctx, rhs_lower, rhs_upper, vector_u, vector_d) { var max = lhs_ctx.codes.length + rhs_ctx.codes.length + 1; var kdown = lhs_lower - rhs_lower; var kup = lhs_upper - rhs_upper; var delta = (lhs_upper - lhs_lower) - (rhs_upper - rhs_lower); var odd = (delta & 1) != 0; var offset_down = max - kdown; var offset_up = max - kup; var maxd = ((lhs_upper - lhs_lower + rhs_upper - rhs_lower) / 2) + 1; vector_d[ offset_down + kdown + 1 ] = lhs_lower; vector_u[ offset_up + kup - 1 ] = lhs_upper; var ret = {x:0,y:0}, d, k, x, y; for (d = 0; d <= maxd; ++d) { for (k = kdown - d; k <= kdown + d; k += 2) { if (k == kdown - d) { x = vector_d[ offset_down + k + 1 ];//down } else { x = vector_d[ offset_down + k - 1 ] + 1;//right if ((k < (kdown + d)) && (vector_d[ offset_down + k + 1 ] >= x)) { x = vector_d[ offset_down + k + 1 ];//down } } y = x - k; // find the end of the furthest reaching forward D-path in diagonal k. while ((x < lhs_upper) && (y < rhs_upper) && (lhs_ctx.codes[x] == rhs_ctx.codes[y])) { x++; y++; } vector_d[ offset_down + k ] = x; // overlap ? if (odd && (kup - d < k) && (k < kup + d)) { if (vector_u[offset_up + k] <= vector_d[offset_down + k]) { ret.x = vector_d[offset_down + k]; ret.y = vector_d[offset_down + k] - k; return (ret); } } } // Extend the reverse path. for (k = kup - d; k <= kup + d; k += 2) { // find the only or better starting point if (k == kup + d) { x = vector_u[offset_up + k - 1]; // up } else { x = vector_u[offset_up + k + 1] - 1; // left if ((k > kup - d) && (vector_u[offset_up + k - 1] < x)) x = vector_u[offset_up + k - 1]; // up } y = x - k; while ((x > lhs_lower) && (y > rhs_lower) && (lhs_ctx.codes[x - 1] == rhs_ctx.codes[y - 1])) { // diagonal x--; y--; } vector_u[offset_up + k] = x; // overlap ? if (!odd && (kdown - d <= k) && (k <= kdown + d)) { if (vector_u[offset_up + k] <= vector_d[offset_down + k]) { ret.x = vector_d[offset_down + k]; ret.y = vector_d[offset_down + k] - k; return (ret); } } } } throw "the algorithm should never come here."; }, _optimize: function(ctx) { var start = 0, end = 0; while (start < ctx.codes.length) { while ((start < ctx.codes.length) && (ctx.modified[start] == undefined || ctx.modified[start] == false)) { start++; } end = start; while ((end < ctx.codes.length) && (ctx.modified[end] == true)) { end++; } if ((end < ctx.codes.length) && (ctx.codes[start] == ctx.codes[end])) { ctx.modified[start] = false; ctx.modified[end] = true; } else { start = end; } } }, _create_diffs: function(lhs_ctx, rhs_ctx) { var items = []; var lhs_start = 0, rhs_start = 0; var lhs_line = 0, rhs_line = 0; while (lhs_line < lhs_ctx.codes.length || rhs_line < rhs_ctx.codes.length) { if ((lhs_line < lhs_ctx.codes.length) && (!lhs_ctx.modified[lhs_line]) && (rhs_line < rhs_ctx.codes.length) && (!rhs_ctx.modified[rhs_line])) { // equal lines lhs_line++; rhs_line++; } else { // maybe deleted and/or inserted lines lhs_start = lhs_line; rhs_start = rhs_line; while (lhs_line < lhs_ctx.codes.length && (rhs_line >= rhs_ctx.codes.length || lhs_ctx.modified[lhs_line])) lhs_line++; while (rhs_line < rhs_ctx.codes.length && (lhs_line >= lhs_ctx.codes.length || rhs_ctx.modified[rhs_line])) rhs_line++; if ((lhs_start < lhs_line) || (rhs_start < rhs_line)) { // store a new difference-item items.push({ lhs_start: lhs_start, rhs_start: rhs_start, lhs_deleted_count: lhs_line - lhs_start, rhs_inserted_count: rhs_line - rhs_start }); } } } return items; } }); Mgly.mergely = function(el, options) { if (el) { this.init(el, options); } }; jQuery.extend(Mgly.mergely.prototype, { name: 'mergely', //http://jupiterjs.com/news/writing-the-perfect-jquery-plugin init: function(el, options) { this.diffView = new Mgly.CodeMirrorDiffView(el, options); this.bind(el); }, bind: function(el) { this.diffView.bind(el); } }); Mgly.CodeMirrorDiffView = function(el, options) { CodeMirror.defineExtension('centerOnCursor', function() { var coords = this.cursorCoords(null, 'local'); this.scrollTo(null, (coords.y + coords.yBot) / 2 - (this.getScrollerElement().clientHeight / 2)); }); this.init(el, options); }; jQuery.extend(Mgly.CodeMirrorDiffView.prototype, { init: function(el, options) { this.settings = { autoupdate: true, autoresize: true, rhs_margin: 'right', wrap_lines: false, line_numbers: true, lcs: true, sidebar: true, viewport: false, ignorews: false, ignorecase: false, fadein: 'fast', editor_width: '650px', editor_height: '400px', resize_timeout: 500, change_timeout: 150, fgcolor: {a:'#4ba3fa',c:'#a3a3a3',d:'#ff7f7f', // color for differences (soft color) ca:'#4b73ff',cc:'#434343',cd:'#ff4f4f'}, // color for currently active difference (bright color) bgcolor: '#eee', vpcolor: 'rgba(0, 0, 200, 0.5)', lhs: function(setValue) { }, rhs: function(setValue) { }, loaded: function() { }, _auto_width: function(w) { return w; }, resize: function(init) { var scrollbar = init ? 16 : 0; var w = jQuery(el).parent().width() + scrollbar, h = 0; if (this.width == 'auto') { w = this._auto_width(w); } else { w = this.width; this.editor_width = w; } if (this.height == 'auto') { //h = this._auto_height(h); h = jQuery(el).parent().height(); } else { h = this.height; this.editor_height = h; } var content_width = w / 2.0 - 2 * 8 - 8; var content_height = h; var self = jQuery(el); self.find('.mergely-column').css({ width: content_width + 'px' }); self.find('.mergely-column, .mergely-canvas, .mergely-margin, .mergely-column textarea, .CodeMirror-scroll, .cm-s-default').css({ height: content_height + 'px' }); self.find('.mergely-canvas').css({ height: content_height + 'px' }); self.find('.mergely-column textarea').css({ width: content_width + 'px' }); self.css({ width: w, height: h, clear: 'both' }); if (self.css('display') == 'none') { if (this.fadein != false) self.fadeIn(this.fadein); else self.show(); if (this.loaded) this.loaded(); } if (this.resized) this.resized(); }, _debug: '', //scroll,draw,calc,diff,markup,change resized: function() { } }; var cmsettings = { mode: 'text/plain', readOnly: false, lineWrapping: this.settings.wrap_lines, lineNumbers: this.settings.line_numbers, gutters: ['merge', 'CodeMirror-linenumbers'] }; this.lhs_cmsettings = {}; this.rhs_cmsettings = {}; // save this element for faster queries this.element = jQuery(el); // save options if there are any if (options && options.cmsettings) jQuery.extend(this.lhs_cmsettings, cmsettings, options.cmsettings, options.lhs_cmsettings); if (options && options.cmsettings) jQuery.extend(this.rhs_cmsettings, cmsettings, options.cmsettings, options.rhs_cmsettings); //if (options) jQuery.extend(this.settings, options); // bind if the element is destroyed this.element.bind('destroyed', jQuery.proxy(this.teardown, this)); // save this instance in jQuery data, binding this view to the node jQuery.data(el, 'mergely', this); this._setOptions(options); }, unbind: function() { if (this.changed_timeout != null) clearTimeout(this.changed_timeout); this.editor[this.id + '-lhs'].toTextArea(); this.editor[this.id + '-rhs'].toTextArea(); jQuery(window).off('.mergely'); }, destroy: function() { this.element.unbind('destroyed', this.teardown); this.teardown(); }, teardown: function() { this.unbind(); }, lhs: function(text) { this.editor[this.id + '-lhs'].setValue(text); }, rhs: function(text) { this.editor[this.id + '-rhs'].setValue(text); }, update: function() { this._changing(this.id + '-lhs', this.id + '-rhs'); }, unmarkup: function() { this._clear(); }, scrollToDiff: function(direction) { if (!this.changes.length) return; if (direction == 'next') { if (this._current_diff == this.changes.length -1) { this._current_diff = 0; } else { this._current_diff = Math.min(++this._current_diff, this.changes.length - 1); } } else if (direction == 'prev') { if (this._current_diff == 0) { this._current_diff = this.changes.length - 1; } else { this._current_diff = Math.max(--this._current_diff, 0); } } this._scroll_to_change(this.changes[this._current_diff]); this._changed(this.id + '-lhs', this.id + '-rhs'); }, mergeCurrentChange: function(side) { if (!this.changes.length) return; if (side == 'lhs' && !this.lhs_cmsettings.readOnly) { this._merge_change(this.changes[this._current_diff], 'rhs', 'lhs'); } else if (side == 'rhs' && !this.rhs_cmsettings.readOnly) { this._merge_change(this.changes[this._current_diff], 'lhs', 'rhs'); } }, scrollTo: function(side, num) { var le = this.editor[this.id + '-lhs']; var re = this.editor[this.id + '-rhs']; if (side == 'lhs') { le.setCursor(num); le.centerOnCursor(); } else { re.setCursor(num); re.centerOnCursor(); } }, _setOptions: function(opts) { jQuery.extend(this.settings, opts); if (this.settings.hasOwnProperty('rhs_margin')) { // dynamically swap the margin if (this.settings.rhs_margin == 'left') { this.element.find('.mergely-margin:last-child').insertAfter( this.element.find('.mergely-canvas')); } else { var target = this.element.find('.mergely-margin').last(); target.appendTo(target.parent()); } } if (this.settings.hasOwnProperty('sidebar')) { // dynamically enable sidebars if (this.settings.sidebar) { this.element.find('.mergely-margin').css({display: 'block'}); } else { this.element.find('.mergely-margin').css({display: 'none'}); } } var le, re; if (this.settings.hasOwnProperty('wrap_lines')) { if (this.editor) { le = this.editor[this.id + '-lhs']; re = this.editor[this.id + '-rhs']; le.setOption('lineWrapping', this.settings.wrap_lines); re.setOption('lineWrapping', this.settings.wrap_lines); } } if (this.settings.hasOwnProperty('line_numbers')) { if (this.editor) { le = this.editor[this.id + '-lhs']; re = this.editor[this.id + '-rhs']; le.setOption('lineNumbers', this.settings.line_numbers); re.setOption('lineNumbers', this.settings.line_numbers); } } }, options: function(opts) { if (opts) { this._setOptions(opts); if (this.settings.autoresize) this.resize(); if (this.settings.autoupdate) this.update(); } else { return this.settings; } }, swap: function() { if (this.lhs_cmsettings.readOnly || this.rhs_cmsettings.readOnly) return; var le = this.editor[this.id + '-lhs']; var re = this.editor[this.id + '-rhs']; var tmp = re.getValue(); re.setValue(le.getValue()); le.setValue(tmp); }, merge: function(side) { var le = this.editor[this.id + '-lhs']; var re = this.editor[this.id + '-rhs']; if (side == 'lhs' && !this.lhs_cmsettings.readOnly) le.setValue(re.getValue()); else if (!this.rhs_cmsettings.readOnly) re.setValue(le.getValue()); }, get: function(side) { var ed = this.editor[this.id + '-' + side]; var t = ed.getValue(); if (t == undefined) return ''; return t; }, clear: function(side) { if (side == 'lhs' && this.lhs_cmsettings.readOnly) return; if (side == 'rhs' && this.rhs_cmsettings.readOnly) return; var ed = this.editor[this.id + '-' + side]; ed.setValue(''); }, cm: function(side) { return this.editor[this.id + '-' + side]; }, search: function(side, query, direction) { var le = this.editor[this.id + '-lhs']; var re = this.editor[this.id + '-rhs']; var editor; if (side == 'lhs') editor = le; else editor = re; direction = (direction == 'prev') ? 'findPrevious' : 'findNext'; if ((editor.getSelection().length == 0) || (this.prev_query[side] != query)) { this.cursor[this.id] = editor.getSearchCursor(query, { line: 0, ch: 0 }, false); this.prev_query[side] = query; } var cursor = this.cursor[this.id]; if (cursor[direction]()) { editor.setSelection(cursor.from(), cursor.to()); } else { cursor = editor.getSearchCursor(query, { line: 0, ch: 0 }, false); } }, resize: function() { this.settings.resize(); this._changing(this.id + '-lhs', this.id + '-rhs'); this._set_top_offset(this.id + '-lhs'); }, diff: function() { var lhs = this.editor[this.id + '-lhs'].getValue(); var rhs = this.editor[this.id + '-rhs'].getValue(); var d = new Mgly.diff(lhs, rhs, this.settings); return d.normal_form(); }, bind: function(el) { this.element.hide();//hide this.id = jQuery(el).attr('id'); this.changed_timeout = null; this.chfns = {}; this.chfns[this.id + '-lhs'] = []; this.chfns[this.id + '-rhs'] = []; this.prev_query = []; this.cursor = []; this._skipscroll = {}; this.change_exp = new RegExp(/(\d+(?:,\d+)?)([acd])(\d+(?:,\d+)?)/); var merge_lhs_button; var merge_rhs_button; if (jQuery.button != undefined) { //jquery ui merge_lhs_button = ''; merge_rhs_button = ''; } else { // homebrew var style = 'opacity:0.4;width:10px;height:15px;background-color:#888;cursor:pointer;text-align:center;color:#eee;border:1px solid: #222;margin-right:5px;margin-top: -2px;'; merge_lhs_button = '
<
'; merge_rhs_button = '
>
'; } this.merge_rhs_button = jQuery(merge_rhs_button); this.merge_lhs_button = jQuery(merge_lhs_button); // create the textarea and canvas elements var height = this.settings.editor_height; var width = this.settings.editor_width; this.element.append(jQuery('
')); this.element.append(jQuery('
')); this.element.append(jQuery('
')); var rmargin = jQuery('
'); if (!this.settings.sidebar) { this.element.find('.mergely-margin').css({display: 'none'}); } if (this.settings.rhs_margin == 'left') { this.element.append(rmargin); } this.element.append(jQuery('
')); if (this.settings.rhs_margin != 'left') { this.element.append(rmargin); } // get current diff border color var color = jQuery('