Skip to content

Instantly share code, notes, and snippets.

@max-mapper
Created March 29, 2011 21:36
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save max-mapper/893372 to your computer and use it in GitHub Desktop.
Save max-mapper/893372 to your computer and use it in GitHub Desktop.
js diffing UI

Uses jsdiff, ace editor and github's css to do 100% client side line diffing. You should fork the code and build a UI for word and character diffing (it's supported by the jsdiff library, I just haven't hooked it up yet).

<!DOCTYPE html>
<html>
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js" type="text/javascript"></script>
<script type="text/javascript">
/*
Shameless port of a shameless port
@defunkt => @janl => @aq
See http://github.com/defunkt/mustache for more info.
*/(function(a){var b=function(){var a=function(){};return a.prototype={otag:"{{",ctag:"}}",pragmas:{},buffer:[],pragmas_implemented:{"IMPLICIT-ITERATOR":!0},context:{},render:function(a,b,c,d){d||(this.context=b,this.buffer=[]);if(!this.includes("",a)){if(d)return a;this.send(a);return}a=this.render_pragmas(a);var e=this.render_section(a,b,c);if(d)return this.render_tags(e,b,c,d);this.render_tags(e,b,c,d)},send:function(a){a!=""&&this.buffer.push(a)},render_pragmas:function(a){if(!this.includes("%",a))return a;var b=this,c=new RegExp(this.otag+"%([\\w-]+) ?([\\w]+=[\\w]+)?"+this.ctag);return a.replace(c,function(a,c,d){if(!b.pragmas_implemented[c])throw{message:"This implementation of mustache doesn't understand the '"+c+"' pragma"};b.pragmas[c]={};if(d){var e=d.split("=");b.pragmas[c][e[0]]=e[1]}return""})},render_partial:function(a,b,c){a=this.trim(a);if(!c||c[a]===undefined)throw{message:"unknown_partial '"+a+"'"};return typeof b[a]!="object"?this.render(c[a],b,c,!0):this.render(c[a],b[a],c,!0)},render_section:function(a,b,c){if(!this.includes("#",a)&&!this.includes("^",a))return a;var d=this,e=new RegExp(this.otag+"(\\^|\\#)\\s*(.+)\\s*"+this.ctag+"\n*([\\s\\S]+?)"+this.otag+"\\/\\s*\\2\\s*"+this.ctag+"\\s*","mg");return a.replace(e,function(a,e,f,g){var h=d.find(f,b);if(e=="^")return!h||d.is_array(h)&&h.length===0?d.render(g,b,c,!0):"";if(e=="#")return d.is_array(h)?d.map(h,function(a){return d.render(g,d.create_context(a),c,!0)}).join(""):d.is_object(h)?d.render(g,d.create_context(h),c,!0):typeof h=="function"?h.call(b,g,function(a){return d.render(a,b,c,!0)}):h?d.render(g,b,c,!0):""})},render_tags:function(a,b,c,d){var e=this,f=function(){return new RegExp(e.otag+"(=|!|>|\\{|%)?([^\\/#\\^]+?)\\1?"+e.ctag+"+","g")},g=f(),h=function(a,d,h){switch(d){case"!":return"";case"=":return e.set_delimiters(h),g=f(),"";case">":return e.render_partial(h,b,c);case"{":return e.find(h,b);default:return e.escape(e.find(h,b))}},i=a.split("\n");for(var j=0;j<i.length;j++)i[j]=i[j].replace(g,h,this),d||this.send(i[j]);if(d)return i.join("\n")},set_delimiters:function(a){var b=a.split(" ");this.otag=this.escape_regex(b[0]),this.ctag=this.escape_regex(b[1])},escape_regex:function(a){if(!arguments.callee.sRE){var b=["/",".","*","+","?","|","(",")","[","]","{","}","\\"];arguments.callee.sRE=new RegExp("(\\"+b.join("|\\")+")","g")}return a.replace(arguments.callee.sRE,"\\$1")},find:function(a,b){function c(a){return a===!1||a===0||a}a=this.trim(a);var d;return c(b[a])?d=b[a]:c(this.context[a])&&(d=this.context[a]),typeof d=="function"?d.apply(b):d!==undefined?d:""},includes:function(a,b){return b.indexOf(this.otag+a)!=-1},escape:function(a){return a=String(a===null?"":a),a.replace(/&(?!\w+;)|["<>\\]/g,function(a){switch(a){case"&":return"&amp;";case"\\":return"\\\\";case'"':return'"';case"<":return"&lt;";case">":return"&gt;";default:return a}})},create_context:function(a){if(this.is_object(a))return a;var b=".";this.pragmas["IMPLICIT-ITERATOR"]&&(b=this.pragmas["IMPLICIT-ITERATOR"].iterator);var c={};return c[b]=a,c},is_object:function(a){return a&&typeof a=="object"},is_array:function(a){return Object.prototype.toString.call(a)==="[object Array]"},trim:function(a){return a.replace(/^\s*|\s*$/g,"")},map:function(a,b){if(typeof a.map=="function")return a.map(b);var c=[],d=a.length;for(var e=0;e<d;e++)c.push(b(a[e]));return c}},{name:"mustache.js",version:"0.3.1-dev",to_html:function(b,c,d,e){var f=new a;e&&(f.send=e),f.render(b,c,d);if(!e)return f.buffer.join("\n")},escape:function(b){return(new a).escape(b)}}}();a.mustache=function(a,c,d){return b.to_html(a,c,d)},a.mustache.escape=function(a){return b.escape(a)}})(jQuery);
</script>
<script src="https://github.com/ajaxorg/ace/raw/master/build/src/ace.js" type="text/javascript" charset="utf-8"></script>
<script src="https://github.com/ajaxorg/ace/raw/master/build/src/mode-javascript.js" type="text/javascript" charset="utf-8"></script>
<script src="https://github.com/kpdecker/jsdiff/raw/master/diff.js" type="text/javascript"></script>
<script type="text/diff" id="old"></script>
<script type="text/diff" id="new"></script>
<script type="text/mustache" id="diffTemplate">
<tr data-position="8">
<td id="L0L241" class="line_numbers linkable-line-number"></td>
<td id="L0R241" class="line_numbers linkable-line-number"></td>
<td width="100%">
<b class="add-bubble" remote=""></b>
<pre><div class="{{#added}}gi{{/added}}{{#removed}}gd{{/removed}}">{{value}}</div></pre>
</td>
</tr>
</script>
<script type="text/javascript">
$(function() {
function renderDiff() {
$('#diffLines').html("");
var oldFile = $( '#old' ).text(),
newFile = $( '#new' ).text(),
diffTemplate = $( '#diffTemplate' ).text();
$( JsDiff.diffLines( oldFile, newFile ) ).map( function( i, diff ) {
$('#diffLines').append( $.mustache(diffTemplate, diff) );
} )
}
var editor;
$('#old').text($('#editor').html());
function jsonParty() {
$('#new').text(editor.getSession().toString());
renderDiff();
}
editor = ace.edit("editor");
jsonParty();
$('textarea').keydown(function() {
window.setTimeout(jsonParty, 100, true);
});
var JavaScriptMode = require("ace/mode/javascript").Mode;
editor.getSession().setMode(new JavaScriptMode());
})
</script>
<style type="text/css" media="screen">
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #666;
overflow: hidden;
}
h1 {
margin-top: 0;
}
ul {
list-style: none;
padding: 0;
margin: 0;
}
li {
margin-bottom: 20px;
clear: both;
}
label {
font-size: 10px;
font-weight: bold;
text-transform: uppercase;
display: block;
margin-bottom: 3px;
clear: both;
}
#editor {
width: 100%;
height: 300px;
margin: 0;
position: absolute;
bottom: 0;
left: 0;
right: 0;
}
.formOutput {
float: left;
clear: both;
}
.htmlOutput {
padding: 25px 0px 0px 0px;
font-size: 12px;
clear: both;
}
</style>
<link href="https://a248.e.akamai.net/assets.github.com/stylesheets/bundles/github-25d3866eecbdad76dacc91ca80d2434cc8f22c04.css" media="screen" rel="stylesheet" type="text/css" />
<link href="https://a248.e.akamai.net/assets.github.com/stylesheets/bundles/github2-0362aac7f9b6a26a7d1f5bcb8c3d429a39498c73.css" media="screen" rel="stylesheet" type="text/css" />
</head>
<body>
<div id="files" class="diff-view commentable">
<div id="diff-0" class="file">
<div class="data highlight">
<table cellpadding="0" cellspacing="0" width="100%">
<tbody id="diffLines">
</tbody>
</table>
</div>
<div class="file-comments-place-holder" data-path="_attachments/pages/monocles.html"></div>
</div>
</div>
<pre id="editor">{
"edit" : "this text",
"remove" : "and add lines",
"to see" : "diffs!"
}
</pre>
</body>
</html>
@thorst
Copy link

thorst commented Sep 18, 2012

This didnt seem to work on first try. Ill see if i can dig in and figure it out.

@coderaiser
Copy link

Tried to see at work but had a problem. This is log.

Failed to load resource: the server responded with a status of 404 (Not Found) https://a248.e.akamai.net/assets.github.com/stylesheets/bundles/github-25d3866eecbdad76dacc91ca80d2434cc8f22c04.css
Failed to load resource: the server responded with a status of 404 (Not Found) https://a248.e.akamai.net/assets.github.com/stylesheets/bundles/github2-0362aac7f9b6a26a7d1f5bcb8c3d429a39498c73.css
Failed to load resource: the server responded with a status of 404 (Not Found) https://raw.github.com/ajaxorg/ace/master/build/src/mode-javascript.js
Failed to load resource: the server responded with a status of 404 (Not Found) https://raw.github.com/ajaxorg/ace/master/build/src/ace.js
Refused to execute script from 'https://github.com/kpdecker/jsdiff/raw/master/diff.js' because its MIME type ('text/plain') is not executable, and strict MIME type checking is enabled. diff.html:1
Uncaught ReferenceError: ace is not defined diff.html:53

I think jsdiff a good library, but I have one problem with it, maybe you could help me.

@tjmehta
Copy link

tjmehta commented Jan 16, 2014

update to rawgithub.com links and ace -> acebuilds
looks like css is just github.com stylesheet... so this will likely break again.
but here's a working version. if it breaks just pull the css from github.com head.

LIVE EXAMPLE: http://runnable.com/UtdEd7G1CSk6AAAb/diff-files-in-javascript-with-jsdiff-and-ace

<!DOCTYPE html> 
<html> 
  <head>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js" type="text/javascript"></script>
    <script type="text/javascript">
/*
Shameless port of a shameless port
@defunkt => @janl => @aq

See http://github.com/defunkt/mustache for more info.
*/(function(a){var b=function(){var a=function(){};return a.prototype={otag:"{{",ctag:"}}",pragmas:{},buffer:[],pragmas_implemented:{"IMPLICIT-ITERATOR":!0},context:{},render:function(a,b,c,d){d||(this.context=b,this.buffer=[]);if(!this.includes("",a)){if(d)return a;this.send(a);return}a=this.render_pragmas(a);var e=this.render_section(a,b,c);if(d)return this.render_tags(e,b,c,d);this.render_tags(e,b,c,d)},send:function(a){a!=""&&this.buffer.push(a)},render_pragmas:function(a){if(!this.includes("%",a))return a;var b=this,c=new RegExp(this.otag+"%([\\w-]+) ?([\\w]+=[\\w]+)?"+this.ctag);return a.replace(c,function(a,c,d){if(!b.pragmas_implemented[c])throw{message:"This implementation of mustache doesn't understand the '"+c+"' pragma"};b.pragmas[c]={};if(d){var e=d.split("=");b.pragmas[c][e[0]]=e[1]}return""})},render_partial:function(a,b,c){a=this.trim(a);if(!c||c[a]===undefined)throw{message:"unknown_partial '"+a+"'"};return typeof b[a]!="object"?this.render(c[a],b,c,!0):this.render(c[a],b[a],c,!0)},render_section:function(a,b,c){if(!this.includes("#",a)&&!this.includes("^",a))return a;var d=this,e=new RegExp(this.otag+"(\\^|\\#)\\s*(.+)\\s*"+this.ctag+"\n*([\\s\\S]+?)"+this.otag+"\\/\\s*\\2\\s*"+this.ctag+"\\s*","mg");return a.replace(e,function(a,e,f,g){var h=d.find(f,b);if(e=="^")return!h||d.is_array(h)&&h.length===0?d.render(g,b,c,!0):"";if(e=="#")return d.is_array(h)?d.map(h,function(a){return d.render(g,d.create_context(a),c,!0)}).join(""):D.is_object(h)?d.render(g,d.create_context(h),c,!0):typeof h=="function"?h.call(b,g,function(a){return d.render(a,b,c,!0)}):h?d.render(g,b,c,!0):""})},render_tags:function(a,b,c,d){var e=this,f=function(){return new RegExp(e.otag+"(=|!|>|\\{|%)?([^\\/#\\^]+?)\\1?"+e.ctag+"+","g")},g=f(),h=function(a,d,h){switch(d){case"!":return"";case"=":return e.set_delimiters(h),g=f(),"";case">":return e.render_partial(h,b,c);case"{":return e.find(h,b);default:return e.escape(e.find(h,b))}},i=a.split("\n");for(var j=0;j<i.length;j++)i[j]=i[j].replace(g,h,this),d||this.send(i[j]);if(d)return i.join("\n")},set_delimiters:function(a){var b=a.split(" ");this.otag=this.escape_regex(b[0]),this.ctag=this.escape_regex(b[1])},escape_regex:function(a){if(!arguments.callee.sRE){var b=["/",".","*","+","?","|","(",")","[","]","{","}","\\"];arguments.callee.sRE=new RegExp("(\\"+b.join("|\\")+")","g")}return a.replace(arguments.callee.sRE,"\\$1")},find:function(a,b){function c(a){return a===!1||a===0||a}a=this.trim(a);var d;return c(b[a])?d=b[a]:c(this.context[a])&&(d=this.context[a]),typeof d=="function"?d.apply(b):D!==undefined?d:""},includes:function(a,b){return b.indexOf(this.otag+a)!=-1},escape:function(a){return a=String(a===null?"":a),a.replace(/&(?!\w+;)|["<>\\]/g,function(a){switch(a){case"&":return"&amp;";case"\\":return"\\\\";case'"':return'"';case"<":return"&lt;";case">":return"&gt;";default:return a}})},create_context:function(a){if(this.is_object(a))return a;var b=".";this.pragmas["IMPLICIT-ITERATOR"]&&(b=this.pragmas["IMPLICIT-ITERATOR"].iterator);var c={};return c[b]=a,c},is_object:function(a){return a&&typeof a=="object"},is_array:function(a){return Object.prototype.toString.call(a)==="[object Array]"},trim:function(a){return a.replace(/^\s*|\s*$/g,"")},map:function(a,b){if(typeof a.map=="function")return a.map(b);var c=[],d=a.length;for(var e=0;e<d;e++)c.push(b(a[e]));return c}},{name:"mustache.js",version:"0.3.1-dev",to_html:function(b,c,d,e){var f=new a;e&&(f.send=e),f.render(b,c,d);if(!e)return f.buffer.join("\n")},escape:function(b){return(new a).escape(b)}}}();a.mustache=function(a,c,d){return b.to_html(a,c,d)},a.mustache.escape=function(a){return b.escape(a)}})(jQuery);
</script>
    <script src="http://rawgithub.com/ajaxorg/ace-builds/master/src/ace.js" type="text/javascript" charset="utf-8"></script>
    <script src="http://rawgithub.com/ajaxorg/ace-builds/master/src/mode-javascript.js" type="text/javascript" charset="utf-8"></script>
    <script src="http://rawgithub.com/kpdecker/jsdiff/master/diff.js" type="text/javascript"></script>
    <script type="text/diff" id="old"></script>
    <script type="text/diff" id="new"></script>
    <script type="text/mustache" id="diffTemplate">
      <tr data-position="8">
        <td id="L0L241" class="line_numbers linkable-line-number"></td>
        <td id="L0R241" class="line_numbers linkable-line-number"></td>
        <td width="100%">
            <b class="add-bubble" remote=""></b>
          <pre><div class="{{#added}}gi{{/added}}{{#removed}}gd{{/removed}}">{{value}}</div></pre>
        </td>
      </tr>
    </script>
    <script type="text/javascript"> 

      $(function() {

        function renderDiff() {
          $('#diffLines').html("");

          var oldFile = $( '#old' ).text(),
              newFile = $( '#new' ).text(),
          diffTemplate = $( '#diffTemplate' ).text();

          $( JsDiff.diffLines( oldFile, newFile ) ).map( function( i, diff ) {
            $('#diffLines').append( $.mustache(diffTemplate, diff) );
          } )
        }

      var editor;

      $('#old').text($('#editor').html());

      function jsonParty() {
        $('#new').text(editor.getSession().toString());
        renderDiff();
      }  

      editor = ace.edit("editor");
      jsonParty();

      $('textarea').keydown(function() {
        window.setTimeout(jsonParty, 100, true);
      });

      var JavaScriptMode = require("ace/mode/javascript").Mode;
      editor.getSession().setMode(new JavaScriptMode());

      })

    </script>
    <style type="text/css" media="screen">
      body {
        font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
        color: #666;
        overflow: hidden;
      }
      h1 {
        margin-top: 0;
      }
      ul {
        list-style: none;
        padding: 0;
        margin: 0;
      }
      li {
        margin-bottom: 20px;
        clear: both;
      }
      label {
        font-size: 10px;
        font-weight: bold;
        text-transform: uppercase;
        display: block;
        margin-bottom: 3px;
        clear: both;
      }

      #editor { 
          width: 100%;
          height: 300px;
          margin: 0;
          position: absolute;
          bottom: 0;
          left: 0;
          right: 0;
      }

      .formOutput {
        float: left;
        clear: both;
      }

      .htmlOutput {
        padding: 25px 0px 0px 0px;
        font-size: 12px;
        clear: both;
      }
    </style>
    <link href="https://github.global.ssl.fastly.net/assets/github2-29e8f0829ba588f8a69c82f1a068a1e6a46701fe.css" media="screen" rel="stylesheet" type="text/css" /> 
    <link href="https://github.global.ssl.fastly.net/assets/github-ea281967a8390ec8a707e53c711e8ad3d8033bd8.css" media="screen" rel="stylesheet" type="text/css" />
  </head>
  <body>
    <div id="files" class="diff-view commentable">
      <div id="diff-0" class="file">
        <div class="data highlight">
          <table cellpadding="0" cellspacing="0" width="100%">
            <tbody id="diffLines">

            </tbody>
          </table>
        </div>
        <div class="file-comments-place-holder" data-path="_attachments/pages/monocles.html"></div>
      </div>
    </div>
    <pre id="editor">{
  "edit" : "this text",
  "remove" : "and add lines",
  "to see" : "diffs!"
}
</pre>
  </body>
</html>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment