浏览代码

overview the different between the latest version and history version

star7th 9 年之前
父节点
当前提交
4199232244

+ 24 - 0
Application/Home/Controller/PageController.class.php

@@ -237,5 +237,29 @@ class PageController extends BaseController {
 
     }
 
+    public function diff(){
+        $login_user = $this->checkLogin();
+        $page_history_id = I("page_history_id/d");
+        $page_id = I("page_id/d");
+
+        $page = D("Page")->where(" page_id = '$page_id' ")->find();
+        $cur_page_content = $page['page_content'];
+
+        $item_id = $page['item_id'] ?$page['item_id'] :$item_id;
+
+        if (!$this->checkItemPermn($login_user['uid'] , $item_id)) {
+            $this->message(L('no_permissions'));
+            return;
+        }
+
+        $page = D("PageHistory")->where(" page_history_id = '$page_history_id' ")->find();
+        $page_content = gzuncompress(base64_decode($page['page_content'])); 
+        $history_page_content = $page_content ? $page_content : $page['page_content'] ;
+        
+        $this->assign("cur_page_content" , $cur_page_content);
+        $this->assign("history_page_content" , $history_page_content);
+        $this->display(); 
+    }
+
 
 }

+ 3 - 1
Application/Home/Lang/en-us.php

@@ -163,7 +163,9 @@ return array(
     'saved_templ_list'=>'Template list you saved',
     'page_comments'=>'Page comments',
     'add_page_comments'=>'Add comments before save',
-
+    'cur_page_content'=>'The latest version ',
+    'history_page_content'=>'History version',
+    'overview'=>'Overview',
     //user
     'login'=>'Login',
     'username'=>'Username',

+ 3 - 0
Application/Home/Lang/zh-cn.php

@@ -163,6 +163,9 @@ return array(
     'saved_templ_list'=>'保存的模板列表',
     'page_comments'=>'页面注释',
     'add_page_comments'=>'保存前添加页面注释',
+    'cur_page_content'=>'当前最新版本',
+    'history_page_content'=>'历史版本',
+    'overview'=>'预览',
 
     //user
     'login'=>'登录',

+ 89 - 0
Application/Home/View/Page/diff.html

@@ -0,0 +1,89 @@
+<include file="Common/header" />
+<link href="__PUBLIC__/diff/diffview.css" rel="stylesheet"> 
+
+<style type="text/css">
+body {
+	font-size: 12px;
+	font-family: Sans-Serif;
+}
+h2 {
+	margin: 0.5em 0 0.1em;
+	text-align: center;
+}
+.top {
+	text-align: center;
+}
+.textInput {
+	display: block;
+	width: 49%;
+	float: left;
+	display: none;
+}
+textarea {
+	width:100%;
+	height:300px;
+}
+label:hover {
+	text-decoration: underline;
+	cursor: pointer;
+}
+.spacer {
+	margin-left: 10px;
+}
+.viewType {
+	font-size: 16px;
+	clear: both;
+	text-align: center;
+	padding: 1em;
+}
+#diffoutput {
+    width: 835px;
+    margin: 0 auto;
+}
+</style>
+
+<!-- <h1 class="top"><a href="http://github.com/cemerick/jsdifflib">jsdifflib</a> demo</h1> -->
+
+<div class="textInput">
+	<h2>{$Think.Lang.cur_page_content}</h2>
+	<textarea id="baseText">{$cur_page_content}</textarea>
+</div>
+<div class="textInput spacer">
+	<h2>{$Think.Lang.history_page_content}</h2>
+	<textarea id="newText">{$history_page_content}</textarea>
+</div>
+
+<div id="diffoutput"> </div>
+
+ <include file="Common/footer" />
+
+ <script src="__PUBLIC__/diff/diffview.js"></script> 
+ <script src="__PUBLIC__/diff/difflib.js"></script> 
+<script type="text/javascript">
+
+$(function(){
+	diffUsingJS(0);
+});
+
+function diffUsingJS(viewType) {
+	"use strict";
+	var byId = function (id) { return document.getElementById(id); },
+		base = difflib.stringAsLines(byId("baseText").value),
+		newtxt = difflib.stringAsLines(byId("newText").value),
+		sm = new difflib.SequenceMatcher(base, newtxt),
+		opcodes = sm.get_opcodes(),
+		diffoutputdiv = byId("diffoutput")
+
+	diffoutputdiv.innerHTML = "";
+
+	diffoutputdiv.appendChild(diffview.buildView({
+		baseTextLines: base,
+		newTextLines: newtxt,
+		opcodes: opcodes,
+		baseTextName: "{$Think.Lang.cur_page_content}",
+		newTextName: "{$Think.Lang.history_page_content}",
+		viewType: viewType
+	}));
+}
+
+</script>

+ 1 - 1
Application/Home/View/Page/history.html

@@ -22,7 +22,7 @@
         <td>{$value.addtime}</td>
         <td>{$value.author_username}</td>
         <td>{$value.page_comments}</td>
-        <td><a href="?s=home/page/edit&page_id={$page_id}&page_history_id={$value.page_history_id}">{$Think.Lang.recover_to_this_version}</a></td>
+        <td> <a href="?s=home/page/diff&page_id={$page_id}&page_history_id={$value.page_history_id}" target="_blank">{$Think.Lang.overview}</a> | <a href="?s=home/page/edit&page_id={$page_id}&page_history_id={$value.page_history_id}">{$Think.Lang.recover_to_this_version}</a></td>
       </TR>
 
       </foreach>

+ 411 - 0
Public/diff/difflib.js

@@ -0,0 +1,411 @@
+/***
+This is part of jsdifflib v1.0. <http://snowtide.com/jsdifflib>
+
+Copyright (c) 2007, Snowtide Informatics Systems, Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+	* Redistributions of source code must retain the above copyright notice, this
+		list of conditions and the following disclaimer.
+	* Redistributions in binary form must reproduce the above copyright notice,
+		this list of conditions and the following disclaimer in the documentation
+		and/or other materials provided with the distribution.
+	* Neither the name of the Snowtide Informatics Systems nor the names of its
+		contributors may be used to endorse or promote products derived from this
+		software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
+SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGE.
+***/
+/* Author: Chas Emerick <cemerick@snowtide.com> */
+__whitespace = {" ":true, "\t":true, "\n":true, "\f":true, "\r":true};
+
+difflib = {
+	defaultJunkFunction: function (c) {
+		return __whitespace.hasOwnProperty(c);
+	},
+	
+	stripLinebreaks: function (str) { return str.replace(/^[\n\r]*|[\n\r]*$/g, ""); },
+	
+	stringAsLines: function (str) {
+		var lfpos = str.indexOf("\n");
+		var crpos = str.indexOf("\r");
+		var linebreak = ((lfpos > -1 && crpos > -1) || crpos < 0) ? "\n" : "\r";
+		
+		var lines = str.split(linebreak);
+		for (var i = 0; i < lines.length; i++) {
+			lines[i] = difflib.stripLinebreaks(lines[i]);
+		}
+		
+		return lines;
+	},
+	
+	// iteration-based reduce implementation
+	__reduce: function (func, list, initial) {
+		if (initial != null) {
+			var value = initial;
+			var idx = 0;
+		} else if (list) {
+			var value = list[0];
+			var idx = 1;
+		} else {
+			return null;
+		}
+		
+		for (; idx < list.length; idx++) {
+			value = func(value, list[idx]);
+		}
+		
+		return value;
+	},
+	
+	// comparison function for sorting lists of numeric tuples
+	__ntuplecomp: function (a, b) {
+		var mlen = Math.max(a.length, b.length);
+		for (var i = 0; i < mlen; i++) {
+			if (a[i] < b[i]) return -1;
+			if (a[i] > b[i]) return 1;
+		}
+		
+		return a.length == b.length ? 0 : (a.length < b.length ? -1 : 1);
+	},
+	
+	__calculate_ratio: function (matches, length) {
+		return length ? 2.0 * matches / length : 1.0;
+	},
+	
+	// returns a function that returns true if a key passed to the returned function
+	// is in the dict (js object) provided to this function; replaces being able to
+	// carry around dict.has_key in python...
+	__isindict: function (dict) {
+		return function (key) { return dict.hasOwnProperty(key); };
+	},
+	
+	// replacement for python's dict.get function -- need easy default values
+	__dictget: function (dict, key, defaultValue) {
+		return dict.hasOwnProperty(key) ? dict[key] : defaultValue;
+	},	
+	
+	SequenceMatcher: function (a, b, isjunk) {
+		this.set_seqs = function (a, b) {
+			this.set_seq1(a);
+			this.set_seq2(b);
+		}
+		
+		this.set_seq1 = function (a) {
+			if (a == this.a) return;
+			this.a = a;
+			this.matching_blocks = this.opcodes = null;
+		}
+		
+		this.set_seq2 = function (b) {
+			if (b == this.b) return;
+			this.b = b;
+			this.matching_blocks = this.opcodes = this.fullbcount = null;
+			this.__chain_b();
+		}
+		
+		this.__chain_b = function () {
+			var b = this.b;
+			var n = b.length;
+			var b2j = this.b2j = {};
+			var populardict = {};
+			for (var i = 0; i < b.length; i++) {
+				var elt = b[i];
+				if (b2j.hasOwnProperty(elt)) {
+					var indices = b2j[elt];
+					if (n >= 200 && indices.length * 100 > n) {
+						populardict[elt] = 1;
+						delete b2j[elt];
+					} else {
+						indices.push(i);
+					}
+				} else {
+					b2j[elt] = [i];
+				}
+			}
+	
+			for (var elt in populardict) {
+				if (populardict.hasOwnProperty(elt)) {
+					delete b2j[elt];
+				}
+			}
+			
+			var isjunk = this.isjunk;
+			var junkdict = {};
+			if (isjunk) {
+				for (var elt in populardict) {
+					if (populardict.hasOwnProperty(elt) && isjunk(elt)) {
+						junkdict[elt] = 1;
+						delete populardict[elt];
+					}
+				}
+				for (var elt in b2j) {
+					if (b2j.hasOwnProperty(elt) && isjunk(elt)) {
+						junkdict[elt] = 1;
+						delete b2j[elt];
+					}
+				}
+			}
+	
+			this.isbjunk = difflib.__isindict(junkdict);
+			this.isbpopular = difflib.__isindict(populardict);
+		}
+		
+		this.find_longest_match = function (alo, ahi, blo, bhi) {
+			var a = this.a;
+			var b = this.b;
+			var b2j = this.b2j;
+			var isbjunk = this.isbjunk;
+			var besti = alo;
+			var bestj = blo;
+			var bestsize = 0;
+			var j = null;
+	
+			var j2len = {};
+			var nothing = [];
+			for (var i = alo; i < ahi; i++) {
+				var newj2len = {};
+				var jdict = difflib.__dictget(b2j, a[i], nothing);
+				for (var jkey in jdict) {
+					if (jdict.hasOwnProperty(jkey)) {
+						j = jdict[jkey];
+						if (j < blo) continue;
+						if (j >= bhi) break;
+						newj2len[j] = k = difflib.__dictget(j2len, j - 1, 0) + 1;
+						if (k > bestsize) {
+							besti = i - k + 1;
+							bestj = j - k + 1;
+							bestsize = k;
+						}
+					}
+				}
+				j2len = newj2len;
+			}
+	
+			while (besti > alo && bestj > blo && !isbjunk(b[bestj - 1]) && a[besti - 1] == b[bestj - 1]) {
+				besti--;
+				bestj--;
+				bestsize++;
+			}
+				
+			while (besti + bestsize < ahi && bestj + bestsize < bhi &&
+					!isbjunk(b[bestj + bestsize]) &&
+					a[besti + bestsize] == b[bestj + bestsize]) {
+				bestsize++;
+			}
+	
+			while (besti > alo && bestj > blo && isbjunk(b[bestj - 1]) && a[besti - 1] == b[bestj - 1]) {
+				besti--;
+				bestj--;
+				bestsize++;
+			}
+			
+			while (besti + bestsize < ahi && bestj + bestsize < bhi && isbjunk(b[bestj + bestsize]) &&
+					a[besti + bestsize] == b[bestj + bestsize]) {
+				bestsize++;
+			}
+	
+			return [besti, bestj, bestsize];
+		}
+		
+		this.get_matching_blocks = function () {
+			if (this.matching_blocks != null) return this.matching_blocks;
+			var la = this.a.length;
+			var lb = this.b.length;
+	
+			var queue = [[0, la, 0, lb]];
+			var matching_blocks = [];
+			var alo, ahi, blo, bhi, qi, i, j, k, x;
+			while (queue.length) {
+				qi = queue.pop();
+				alo = qi[0];
+				ahi = qi[1];
+				blo = qi[2];
+				bhi = qi[3];
+				x = this.find_longest_match(alo, ahi, blo, bhi);
+				i = x[0];
+				j = x[1];
+				k = x[2];
+	
+				if (k) {
+					matching_blocks.push(x);
+					if (alo < i && blo < j)
+						queue.push([alo, i, blo, j]);
+					if (i+k < ahi && j+k < bhi)
+						queue.push([i + k, ahi, j + k, bhi]);
+				}
+			}
+			
+			matching_blocks.sort(difflib.__ntuplecomp);
+	
+			var i1 = j1 = k1 = block = 0;
+			var non_adjacent = [];
+			for (var idx in matching_blocks) {
+				if (matching_blocks.hasOwnProperty(idx)) {
+					block = matching_blocks[idx];
+					i2 = block[0];
+					j2 = block[1];
+					k2 = block[2];
+					if (i1 + k1 == i2 && j1 + k1 == j2) {
+						k1 += k2;
+					} else {
+						if (k1) non_adjacent.push([i1, j1, k1]);
+						i1 = i2;
+						j1 = j2;
+						k1 = k2;
+					}
+				}
+			}
+			
+			if (k1) non_adjacent.push([i1, j1, k1]);
+	
+			non_adjacent.push([la, lb, 0]);
+			this.matching_blocks = non_adjacent;
+			return this.matching_blocks;
+		}
+		
+		this.get_opcodes = function () {
+			if (this.opcodes != null) return this.opcodes;
+			var i = 0;
+			var j = 0;
+			var answer = [];
+			this.opcodes = answer;
+			var block, ai, bj, size, tag;
+			var blocks = this.get_matching_blocks();
+			for (var idx in blocks) {
+				if (blocks.hasOwnProperty(idx)) {
+					block = blocks[idx];
+					ai = block[0];
+					bj = block[1];
+					size = block[2];
+					tag = '';
+					if (i < ai && j < bj) {
+						tag = 'replace';
+					} else if (i < ai) {
+						tag = 'delete';
+					} else if (j < bj) {
+						tag = 'insert';
+					}
+					if (tag) answer.push([tag, i, ai, j, bj]);
+					i = ai + size;
+					j = bj + size;
+					
+					if (size) answer.push(['equal', ai, i, bj, j]);
+				}
+			}
+			
+			return answer;
+		}
+		
+		// this is a generator function in the python lib, which of course is not supported in javascript
+		// the reimplementation builds up the grouped opcodes into a list in their entirety and returns that.
+		this.get_grouped_opcodes = function (n) {
+			if (!n) n = 3;
+			var codes = this.get_opcodes();
+			if (!codes) codes = [["equal", 0, 1, 0, 1]];
+			var code, tag, i1, i2, j1, j2;
+			if (codes[0][0] == 'equal') {
+				code = codes[0];
+				tag = code[0];
+				i1 = code[1];
+				i2 = code[2];
+				j1 = code[3];
+				j2 = code[4];
+				codes[0] = [tag, Math.max(i1, i2 - n), i2, Math.max(j1, j2 - n), j2];
+			}
+			if (codes[codes.length - 1][0] == 'equal') {
+				code = codes[codes.length - 1];
+				tag = code[0];
+				i1 = code[1];
+				i2 = code[2];
+				j1 = code[3];
+				j2 = code[4];
+				codes[codes.length - 1] = [tag, i1, Math.min(i2, i1 + n), j1, Math.min(j2, j1 + n)];
+			}
+	
+			var nn = n + n;
+			var group = [];
+			var groups = [];
+			for (var idx in codes) {
+				if (codes.hasOwnProperty(idx)) {
+					code = codes[idx];
+					tag = code[0];
+					i1 = code[1];
+					i2 = code[2];
+					j1 = code[3];
+					j2 = code[4];
+					if (tag == 'equal' && i2 - i1 > nn) {
+						group.push([tag, i1, Math.min(i2, i1 + n), j1, Math.min(j2, j1 + n)]);
+						groups.push(group);
+						group = [];
+						i1 = Math.max(i1, i2-n);
+						j1 = Math.max(j1, j2-n);
+					}
+					
+					group.push([tag, i1, i2, j1, j2]);
+				}
+			}
+			
+			if (group && !(group.length == 1 && group[0][0] == 'equal')) groups.push(group)
+			
+			return groups;
+		}
+		
+		this.ratio = function () {
+			matches = difflib.__reduce(
+							function (sum, triple) { return sum + triple[triple.length - 1]; },
+							this.get_matching_blocks(), 0);
+			return difflib.__calculate_ratio(matches, this.a.length + this.b.length);
+		}
+		
+		this.quick_ratio = function () {
+			var fullbcount, elt;
+			if (this.fullbcount == null) {
+				this.fullbcount = fullbcount = {};
+				for (var i = 0; i < this.b.length; i++) {
+					elt = this.b[i];
+					fullbcount[elt] = difflib.__dictget(fullbcount, elt, 0) + 1;
+				}
+			}
+			fullbcount = this.fullbcount;
+	
+			var avail = {};
+			var availhas = difflib.__isindict(avail);
+			var matches = numb = 0;
+			for (var i = 0; i < this.a.length; i++) {
+				elt = this.a[i];
+				if (availhas(elt)) {
+					numb = avail[elt];
+				} else {
+					numb = difflib.__dictget(fullbcount, elt, 0);
+				}
+				avail[elt] = numb - 1;
+				if (numb > 0) matches++;
+			}
+			
+			return difflib.__calculate_ratio(matches, this.a.length + this.b.length);
+		}
+		
+		this.real_quick_ratio = function () {
+			var la = this.a.length;
+			var lb = this.b.length;
+			return _calculate_ratio(Math.min(la, lb), la + lb);
+		}
+		
+		this.isjunk = isjunk ? isjunk : difflib.defaultJunkFunction;
+		this.a = this.b = null;
+		this.set_seqs(a, b);
+	}
+};
+

+ 83 - 0
Public/diff/diffview.css

@@ -0,0 +1,83 @@
+/*
+This is part of jsdifflib v1.0. <http://github.com/cemerick/jsdifflib>
+
+Copyright 2007 - 2011 Chas Emerick <cemerick@snowtide.com>. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are
+permitted provided that the following conditions are met:
+
+   1. Redistributions of source code must retain the above copyright notice, this list of
+      conditions and the following disclaimer.
+
+   2. Redistributions in binary form must reproduce the above copyright notice, this list
+      of conditions and the following disclaimer in the documentation and/or other materials
+      provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY Chas Emerick ``AS IS'' AND ANY EXPRESS OR IMPLIED
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Chas Emerick OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+The views and conclusions contained in the software and documentation are those of the
+authors and should not be interpreted as representing official policies, either expressed
+or implied, of Chas Emerick.
+*/
+table.diff {
+	border-collapse:collapse;
+	border:1px solid darkgray;
+	white-space:pre-wrap
+}
+table.diff tbody { 
+	font-family:Courier, monospace
+}
+table.diff tbody th {
+	font-family:verdana,arial,'Bitstream Vera Sans',helvetica,sans-serif;
+	background:#EED;
+	font-size:11px;
+	font-weight:normal;
+	border:1px solid #BBC;
+	color:#886;
+	padding:.3em .5em .1em 2em;
+	text-align:right;
+	vertical-align:top
+}
+table.diff thead {
+	border-bottom:1px solid #BBC;
+	background:#EFEFEF;
+	font-family:Verdana
+}
+table.diff thead th.texttitle {
+	text-align:left
+}
+table.diff tbody td {
+	padding:0px .4em;
+	padding-top:.4em;
+	vertical-align:top;
+}
+table.diff .empty {
+	background-color:#DDD;
+}
+table.diff .replace {
+	background-color:#FD8
+}
+table.diff .delete {
+	background-color:#E99;
+}
+table.diff .skip {
+	background-color:#EFEFEF;
+	border:1px solid #AAA;
+	border-right:1px solid #BBC;
+}
+table.diff .insert {
+	background-color:#9E9
+}
+table.diff th.author {
+	text-align:right;
+	border-top:1px solid #BBC;
+	background:#EFEFEF
+}

+ 198 - 0
Public/diff/diffview.js

@@ -0,0 +1,198 @@
+/*
+This is part of jsdifflib v1.0. <http://github.com/cemerick/jsdifflib>
+
+Copyright 2007 - 2011 Chas Emerick <cemerick@snowtide.com>. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are
+permitted provided that the following conditions are met:
+
+   1. Redistributions of source code must retain the above copyright notice, this list of
+      conditions and the following disclaimer.
+
+   2. Redistributions in binary form must reproduce the above copyright notice, this list
+      of conditions and the following disclaimer in the documentation and/or other materials
+      provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY Chas Emerick ``AS IS'' AND ANY EXPRESS OR IMPLIED
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Chas Emerick OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+The views and conclusions contained in the software and documentation are those of the
+authors and should not be interpreted as representing official policies, either expressed
+or implied, of Chas Emerick.
+*/
+diffview = {
+	/**
+	 * Builds and returns a visual diff view.  The single parameter, `params', should contain
+	 * the following values:
+	 *
+	 * - baseTextLines: the array of strings that was used as the base text input to SequenceMatcher
+	 * - newTextLines: the array of strings that was used as the new text input to SequenceMatcher
+	 * - opcodes: the array of arrays returned by SequenceMatcher.get_opcodes()
+	 * - baseTextName: the title to be displayed above the base text listing in the diff view; defaults
+	 *	   to "Base Text"
+	 * - newTextName: the title to be displayed above the new text listing in the diff view; defaults
+	 *	   to "New Text"
+	 * - contextSize: the number of lines of context to show around differences; by default, all lines
+	 *	   are shown
+	 * - viewType: if 0, a side-by-side diff view is generated (default); if 1, an inline diff view is
+	 *	   generated
+	 */
+	buildView: function (params) {
+		var baseTextLines = params.baseTextLines;
+		var newTextLines = params.newTextLines;
+		var opcodes = params.opcodes;
+		var baseTextName = params.baseTextName ? params.baseTextName : "Base Text";
+		var newTextName = params.newTextName ? params.newTextName : "New Text";
+		var contextSize = params.contextSize;
+		var inline = (params.viewType == 0 || params.viewType == 1) ? params.viewType : 0;
+
+		if (baseTextLines == null)
+			throw "Cannot build diff view; baseTextLines is not defined.";
+		if (newTextLines == null)
+			throw "Cannot build diff view; newTextLines is not defined.";
+		if (!opcodes)
+			throw "Canno build diff view; opcodes is not defined.";
+		
+		function celt (name, clazz) {
+			var e = document.createElement(name);
+			e.className = clazz;
+			return e;
+		}
+		
+		function telt (name, text) {
+			var e = document.createElement(name);
+			e.appendChild(document.createTextNode(text));
+			return e;
+		}
+		
+		function ctelt (name, clazz, text) {
+			var e = document.createElement(name);
+			e.className = clazz;
+			e.appendChild(document.createTextNode(text));
+			return e;
+		}
+	
+		var tdata = document.createElement("thead");
+		var node = document.createElement("tr");
+		tdata.appendChild(node);
+		if (inline) {
+			node.appendChild(document.createElement("th"));
+			node.appendChild(document.createElement("th"));
+			node.appendChild(ctelt("th", "texttitle", baseTextName + " vs. " + newTextName));
+		} else {
+			node.appendChild(document.createElement("th"));
+			node.appendChild(ctelt("th", "texttitle", baseTextName));
+			node.appendChild(document.createElement("th"));
+			node.appendChild(ctelt("th", "texttitle", newTextName));
+		}
+		tdata = [tdata];
+		
+		var rows = [];
+		var node2;
+		
+		/**
+		 * Adds two cells to the given row; if the given row corresponds to a real
+		 * line number (based on the line index tidx and the endpoint of the 
+		 * range in question tend), then the cells will contain the line number
+		 * and the line of text from textLines at position tidx (with the class of
+		 * the second cell set to the name of the change represented), and tidx + 1 will
+		 * be returned.	 Otherwise, tidx is returned, and two empty cells are added
+		 * to the given row.
+		 */
+		function addCells (row, tidx, tend, textLines, change) {
+			if (tidx < tend) {
+				row.appendChild(telt("th", (tidx + 1).toString()));
+				row.appendChild(ctelt("td", change, textLines[tidx].replace(/\t/g, "\u00a0\u00a0\u00a0\u00a0")));
+				return tidx + 1;
+			} else {
+				row.appendChild(document.createElement("th"));
+				row.appendChild(celt("td", "empty"));
+				return tidx;
+			}
+		}
+		
+		function addCellsInline (row, tidx, tidx2, textLines, change) {
+			row.appendChild(telt("th", tidx == null ? "" : (tidx + 1).toString()));
+			row.appendChild(telt("th", tidx2 == null ? "" : (tidx2 + 1).toString()));
+			row.appendChild(ctelt("td", change, textLines[tidx != null ? tidx : tidx2].replace(/\t/g, "\u00a0\u00a0\u00a0\u00a0")));
+		}
+		
+		for (var idx = 0; idx < opcodes.length; idx++) {
+			code = opcodes[idx];
+			change = code[0];
+			var b = code[1];
+			var be = code[2];
+			var n = code[3];
+			var ne = code[4];
+			var rowcnt = Math.max(be - b, ne - n);
+			var toprows = [];
+			var botrows = [];
+			for (var i = 0; i < rowcnt; i++) {
+				// jump ahead if we've alredy provided leading context or if this is the first range
+				if (contextSize && opcodes.length > 1 && ((idx > 0 && i == contextSize) || (idx == 0 && i == 0)) && change=="equal") {
+					var jump = rowcnt - ((idx == 0 ? 1 : 2) * contextSize);
+					if (jump > 1) {
+						toprows.push(node = document.createElement("tr"));
+						
+						b += jump;
+						n += jump;
+						i += jump - 1;
+						node.appendChild(telt("th", "..."));
+						if (!inline) node.appendChild(ctelt("td", "skip", ""));
+						node.appendChild(telt("th", "..."));
+						node.appendChild(ctelt("td", "skip", ""));
+						
+						// skip last lines if they're all equal
+						if (idx + 1 == opcodes.length) {
+							break;
+						} else {
+							continue;
+						}
+					}
+				}
+				
+				toprows.push(node = document.createElement("tr"));
+				if (inline) {
+					if (change == "insert") {
+						addCellsInline(node, null, n++, newTextLines, change);
+					} else if (change == "replace") {
+						botrows.push(node2 = document.createElement("tr"));
+						if (b < be) addCellsInline(node, b++, null, baseTextLines, "delete");
+						if (n < ne) addCellsInline(node2, null, n++, newTextLines, "insert");
+					} else if (change == "delete") {
+						addCellsInline(node, b++, null, baseTextLines, change);
+					} else {
+						// equal
+						addCellsInline(node, b++, n++, baseTextLines, change);
+					}
+				} else {
+					b = addCells(node, b, be, baseTextLines, change);
+					n = addCells(node, n, ne, newTextLines, change);
+				}
+			}
+
+			for (var i = 0; i < toprows.length; i++) rows.push(toprows[i]);
+			for (var i = 0; i < botrows.length; i++) rows.push(botrows[i]);
+		}
+		
+		rows.push(node = ctelt("th", "author", "diff view generated by "));
+		node.setAttribute("colspan", inline ? 3 : 4);
+		node.appendChild(node2 = telt("a", "jsdifflib"));
+		node2.setAttribute("href", "http://github.com/cemerick/jsdifflib");
+		
+		tdata.push(node = document.createElement("tbody"));
+		for (var idx in rows) rows.hasOwnProperty(idx) && node.appendChild(rows[idx]);
+		
+		node = celt("table", "diff" + (inline ? " inlinediff" : ""));
+		for (var idx in tdata) tdata.hasOwnProperty(idx) && node.appendChild(tdata[idx]);
+		return node;
+	}
+};
+