O'Reilly Hacks
oreilly.comO'Reilly NetworkSafari BookshelfConferences Sign In/My Account | View Cart   
Book List Learning Lab PDFs O'Reilly Gear Newsletters Press Room Jobs  


 
Buy the book!
Greasemonkey Hacks
By Mark Pilgrim
November 2005
More Info

HACK
#98
Animate Wikipedia History
Watch a full-screen timeline of how a Wikipedia page evolved
The Code
[Discuss (0) | Link to this hack]

The Code

This user script runs on all Wikipedia history pages. It adds an "Animate changes" button to the history page that acts as the main entry point for the rest of the script. The animation itself is a series of calls to Wikipedia's revision history interface.

We create an XMLHttpRequest object to retrieve the actual revision text and associated metadata (such as the author and revision date). The script also constructs a slider (really, a styled <div> with appropriate styling and event handlers) that tracks the current status of the animation, from the first version of the page to the current revision.

Save the following user script as wikipedia-animate.user.js:

	// ==UserScript==
	// @name			Wikipedia Animate
	// @namespace		http://phiffer.org/greasemonkey/
	// @description		Animates page modifications between two specific edit points
	// @include			http://*.wikipedia.tld/*action=history*
	// ==/UserScript==

	// based on code by Dan Phiffer
	// and included here with his gracious permission

	function Animation() {

		if (!document.getElementById('bodyContent')) {
			return;
		}

		var url = window.location.href;
		this.base_url = url.substr(0, url.indexOf('&'));
		this.hostname = url.substr(7, url.indexOf('/', 8) - 7);

		this.add_buttons();
		this.add_options();
		this.add_css();

	}
	Animation.prototype.add_buttons = function() {
		// Create the animate buttons
		var button1 = document.createElement('input');
		button1.className = 'historysubmit';
		button1.style.marginLeft = '5px';
		button1.setAttribute('type', 'button');
		button1.value = 'Animate changes';
		button1.addEventListener('click', function() { animate.start(); },
	true); 
		button1.setAttribute('id', 'animate_button1');

		var button2 = button1.cloneNode(true);    
		button2.addEventListener('click', function() { animate.start(); }, 
	true); 
		button2.setAttribute('id', 'animate_button2');

		// Add the buttons to the page
		var history = document.getElementById('pagehistory');
		history.parentNode.insertBefore(button1, history);
		history.parentNode.appendChild(document.createTextNode(' '));
		history.parentNode.appendChild(button2);

	}
	
	Animation.prototype.add_options = function() {
	
		// Create the options box
		var toolbox = document.getElementById('p-tb');
		var options = document.createElement('div');
		options.className = 'portlet';

		options.innerHTML = '<h5>animate options</h5><div class="pBody"><ul>' +

		// Range selection
		'<li>Animate over:' +
		'<div><input type="radio" name="animate_range" id="animate_range_
	selected" value="selected" checked="checked"/> Selected</div>' + 
		'<div><input type="radio" name="animate_range" id="animate_range_all"
	value="all"/> All versions</div>' +
		'<div><input type="checkbox" id="animate_skip_minor"/> Skip minor
	edits</div>' +

		// Diffs
		'</li><li>Highlight diffs:' +
		'<div><input type="radio" name="animate_diff" id="animate_diff_yes"
	value="yes" checked="checked"/> Yes</div>' +
		'<div><input type="radio" name="animate_diff" id="animate_diff_no"
	value="no"/> No</div>' +

		// Speed
		'</li><li>Animate speed:' +
		'<div>Pause <input type="text" id="animate_delay" value="0.5" size="3"
	style="font-size: 10px" onblur="animate.option(this);"/> sec</div>' +
		// Info
		'</li><li>Include info:' +
		'<div><input type="checkbox" id="animate_info_date" checked="checked"/>
	Date/time</div>' +
		'<div><input type="checkbox" id="animate_info_author" checked="checked"/
	> Author</div>' +
		'<div><input type="checkbox" id="animate_info_summary"
	checked="checked"/> Change summary</div>' +
		'</li></ul></div>';

		toolbox.parentNode.appendChild(options);
	}

	Animation.prototype.add_css = function( ) {

		// Add some CSS formatting rules for diffs
		var head = document.getElementsByTagName('head')[0];
		var style = document.createElement('style');
		style.type = 'text/css';
		style.innerHTML = 'ins.diff { display: inline; background-color: #CFC;
	font-weight: bold; } ' +
		'del.diff { background-color: #FFA; display: inline; } ' +
		'#animate_main { width: 100%; position: relative; } ' +
		'#animate_controls { position: absolute; top: 0; left: 0; background:
	transparent url(data:image/
	png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAA8CAYAAACuGnCAAAAABGdBTUEAANbY1E9YM
	gAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAABnSURBVHjaYvz///
	9NBjTAxIAFjAqOCo4KDl7Bf0D8H4rBgAWIvwExIxIGC36G6oBLgAQ/oakEC75HspAJJvgOm/
	a3uAQZkSSY8KrEEES2iAnm+I/
	Y3PkJ6ka4ICOwWOOA+RkmCBBgAIPPFd35TefZAAAAAElFTkSuQmCC) repeat-x; width:
	100%; } ' +
		'#animate_controls span.text { background: #FFF; } ' +
		'#animate_main div.content { position: absolute; top: 60px; display:
	none; } ' +
		'#animate_button { float: left; margin-left: 0; margin-top: 5px; width:
	50px; } ' +
		'#animate_scrubber { position: relative; width: 402px; height: 11px;
	border: 1px solid #AAA; background: #F5F5F5; float: left; margin: 8px; } ' +
		'#animate_load_progress { position: absolute; top: 1px; left: 1px;
	background: #E1E1E1; height: 9px; width: 5px; visibility: hidden; } ' +
		'#animate_playhead { position: absolute; left: 1px; top: 1px; cursor:
	pointer; background: transparent url(data:image/
	gif;base64,R0lGODlhCQAJAIAAAP///
	wAAACH5BAEAAAAALAAAAAAJAAkAAAIRhBGnwYrcDJxvwkplPtchVQAAOw==) no-repeat;
	height: 9px; width: 9px; } ' +
		'#animate_status { font: 10px verdana, sans-serif; float: left; margin-top:
	8px; } ' +
		'#animate_info { font-size: 10px; margin: 0 0 20px 58px; }';
		head.appendChild(style);
	}
	Animation.prototype.start = function( ) {
		// Initialize variables
		this.urls = new Array( );
		this.info = new Array( );
		this.pages = new Array( );
		this.activity = new Array( );
		this.activity_max = 0;
		this.num_loaded = 0;
		this.pos = 0;	
		this.interval = -1;
		this.prev = -1;
		this.status = 1; /* Status codes:
				0: history
				1: loading
				2: playing
				3: paused
				4: playhead scrub */
		var history = document.getElementById('pagehistory');
		var items = history.getElementsByTagName('li');

		// Cache the current history view
		var bodyContent = document.getElementById('bodyContent');
		this.history_content = this.mediawiki_content(bodyContent.innerHTML);

		// Check whether to animate over all article revisions
		if (document.getElementById('animate_range_all').checked) {

			// Check to see if the current history page already contains every
	revision
			var last_row = items[items.length - 1];
			var last_links = last_row.getElementsByTagName('a');
			var first_row = items[0];
			var first_links = first_row.getElementsByTagName('a');

			// The first and last list items each lack a 'last' and 'cur' link,
	respectively
			if (last_links[1].firstChild.nodeValue == 'last' ||
				first_links[0].firstChild.nodeValue == 'cur') {
				this.get_full_history( );
				return;
			} else {
				first_row.getElementsByTagName('input')[1].checked = true;
				last_row.getElementsByTagName('input')[0].checked = true;
			}
		}
		this.parse_history( );
		this.setup_markup( );
		this.start_loading( );
	}
	Animation.prototype.get_full_history = function( ) {

		// Disable the animate buttons while we load
		var button1 = document.getElementById('animate_button1');
		button1.value = 'Loading…';
		button1.setAttribute('disabled', 'disabled');

		var button2 = document.getElementById('animate_button2');
		button2.value = 'Loading…';
		button2.setAttribute('disabled', 'disabled');

		// Load in the full history
		var request = new XMLHttpRequest( );
		request.open('GET', this.base_url +
	'&action=history&limit=5000&offset=0', true);
		request.onreadystatechange = function( ) {
			if (request.readyState == 4) {

				var content = animate.mediawiki_content(request.responseText);
				document.getElementById('bodyContent').innerHTML = content;

				var history = document.getElementById('pagehistory');
				var items = history.getElementsByTagName('li');
				var inputs = items[items.length - 1].
	getElementsByTagName('input');
				inputs[0].checked = true;

				animate.parse_history( );
				animate.setup_markup( );
				animate.start_loading( );
			}
		}
		request.send(null);
	}

	Animation.prototype.parse_history = function( ) {
	
	var history = document.getElementById('pagehistory');
	var items = history.getElementsByTagName('li');
	var skip_minor = document.getElementById('animate_skip_minor').checked;
	var found_start = false;

	for (var i = 0; i < items.length; i++) {

		var radios = items[i].getElementsByTagName('input');
		var skip = false;
		
		// Skip this revision if it's been labeled 'minor'
		if (skip_minor) {
			var spans = items[i].getElementsByTagName('span');
			for (var j = 0; j < spans.length; j++) {
				if (spans[j].className == 'minor') {
				skip = true;
				}	
				}
			}

			if (radios[1] && radios[1].checked) {
				var links = items[i].getElementsByTagName('a');
				if (links[0].firstChild.nodeValue != 'cur' && !skip) {
				this.urls.unshift(links[1].getAttribute('href'));
				this.info.unshift(this.parse_info(items[i], 1));
				} else if (!skip) {
				this.urls.unshift(links[2].getAttribute('href'));
				this.info.unshift(this.parse_info(items[i], 2));
				}
				found_start = true;
			} else if (radios[0] && radios[0].checked) {
				var links = items[i].getElementsByTagName('a');
				if (links[1].firstChild.nodeValue != 'last' && !skip) {
				this.urls.unshift(links[1].getAttribute('href'));
				this.info.unshift(this.parse_info(items[i], 1));
				} else if (!skip) {
				this.urls.unshift(links[2].getAttribute('href'));
				this.info.unshift(this.parse_info(items[i], 2));
				}
				break;
			} else if (found_start && !skip) {
				var links = items[i].getElementsByTagName('a');
				this.urls.unshift(links[2].getAttribute('href'));
				this.info.unshift(this.parse_info(items[i], 2));
			}
		}
	}
	
	Animation.prototype.setup_markup = function( ) {
	
		this.add_nav_link( );
	
		var content = '<div id="animate_main">' +
		'<div id="animate_controls">' +
		'<input type="button" value="Pause" class="historysubmit" id="animate_
	button"/> ' +
		'<div id="animate_scrubber">' +
		'<div id="animate_load_progress"></div>' +
		'<div id="animate_playhead"></div></div>' +
		'<div id="animate_status">Loading…</div>' +
		'<br style="clear: both;"/>' +
		'<div id="animate_info"></div>' +
		'</div></div>';
		document.getElementById('bodyContent').innerHTML = content;
		document.getElementById('bodyContent').style.height = '250px';

		this.content = 1;
		var playhead = document.getElementById('animate_playhead');
		playhead.addEventListener('mousedown', function(e) {
			animate.playhead(e); e.preventDefault( );
		}, true);

		var button = document.getElementById('animate_button');
		button.addEventListener('click', function( ) {
			animate.button( );
		}, true);

		var body = document.getElementsByTagName('body').item(0);
		body.addEventListener('mouseup', function(e) {
			animate.mouseup(e);
		}, true);
		body.addEventListener('mousemove', function(e) {
			animate.mousemove(e);
		}, true);

		var top = 0;
		var curr = document.getElementById('animate_main');
		while (curr.offsetParent) {
			top += curr.offsetTop;
			curr = curr.offsetParent;
		}
		this.scroll_origin = top;

		window.setInterval(function( ) { animate.check_scroll( ); }, 50);
	}

	Animation.prototype.add_nav_link = function( ) {
		var history_nav = document.getElementById('ca-history');
		history_nav.className = '';
		history_nav.getElementsByTagName('a').item(0).addEventListener(
			'click', function(event) {
			animate.status = 0;
			document.getElementById('bodyContent').innerHTML = animate.history_
	content;
			histrowinit( );
			this.parentNode.className = 'selected';

			var animate_nav = document.getElementById('animate_nav');
			this.parentNode.parentNode.removeChild(animate_nav);
			document.getElementById('animate_button1').addEventListener(
				'click', function( ) { animate.start( ); }, true);
			document.getElementById('animate_button2').addEventListener(
				'click', function( ) { animate.start( ); }, true);
			event.preventDefault( );
		}, true);

		var animate_nav = document.createElement('li');
		var link = animate_nav.appendChild(document.createElement('a'));
		animate_nav.id = 'animate_nav';
		link.appendChild(document.createTextNode('animate'));

		link.setAttribute('href', '#');
		link.addEventListener('click', function(event) {
			event.preventDefault( );
		}, true);
		animate_nav.className = 'selected';
		history_nav.parentNode.appendChild(animate_nav);
	}
	Animation.prototype.start_loading = function( ) {
		var url = 'http://' + this.hostname + this.urls[0];

		var request = new XMLHttpRequest( );
		request.open('GET', url, true);
		request.onreadystatechange = function( ) {
			if (request.readyState == 4) {
				animate.loaded(request);
			}
		}
		request.send(null);
	}
	Animation.prototype.parse_info = function(item, l) {
		var info = '';
		var links = item.getElementsByTagName('a');

		if (document.getElementById('animate_info_date').checked) {
			var href = links.item(l).getAttribute('href');
			var text = links.item(l).firstChild.nodeValue;
			info += '<a href="' + href + '">' + text + '</a> ';
		}

		if (document.getElementById('animate_info_author').checked) {
			var href = links.item(l + 1).getAttribute('href');
			var text = links.item(l + 1).firstChild.nodeValue;
			info += 'by <a href="' + href + '">' + text + '</a> ';
		}
		if (document.getElementById('animate_info_summary').checked) {
			var em = item.getElementsByTagName('em');
			if (em.length == 1) {
				info += '&nbsp;&nbsp;&nbsp;' + em.item(0).innerHTML;
			}
		}

		info = '<span class="text">' + info + '</span>';

		return info;
	}

	Animation.prototype.loaded = function(details) {
		var content = this.mediawiki_content(details.responseText);
		this.pages[this.pages.length] = content;

		if (this.num_loaded > 0 && document.getElementById('animate_diff_yes').
		checked) {

			content = diffString(this.pages[this.num_loaded - 1], content);
		}
		var frame = document.createElement('div');
		var main = document.getElementById('animate_main');
		var controls = document.getElementById('animate_controls');

		main.insertBefore(frame, controls);
		frame.innerHTML = content;
		frame.className = 'content';
		frame.setAttribute('id', 'frame' + this.num_loaded);
		var load_progress = document.getElementById('animate_load_progress');
		load_progress.style.width = 5 + (395 * this.num_loaded / (this.urls.
	length - 1)) + 'px';
		load_progress.style.visibility = 'visible';
		if (this.num_loaded > 0 && document.getElementById('animate_diff_yes').
	checked) {

			var activity = 0;
			
			// Check for added content
			var b_list = frame.getElementsByTagName('b');
			for (var i = 0; i < b_list.length; i++) {
				if (b_list[i].className == 'diff') {
				activity++;
				}
			}

			// Check for deleted content
			var s_list = frame.getElementsByTagName('s');
			for (var i = 0; i < s_list.length; i++) {
				if (s_list[i].className == 'diff') {
				activity++;
				}
			}
			var id = this.activity.length;
			this.activity[id] = activity;

			var a = document.createElement('div');
			document.getElementById('animate_load_progress').appendChild(a);
			a.setAttribute('id', 'animate_activity' + id);
			a.style.position = 'absolute';
			a.style.left = (395 * (this.num_loaded - 1) / (this.urls.length -
		1)) + 'px';
			a.style.width = (395 / (this.urls.length - 1)) + 'px';

			if (this.num_loaded == 1) {
				a.style.width = parseFloat(a.style.width) + 5 + 'px';
			} else {
				a.style.left = parseFloat(a.style.left) + 5 + 'px';
			}

			a.style.height = '9px';
			a.style.top = '0px';


			if (this.activity_max == 0) {
				if (activity == 0) {
				var digit = 225;
				} else {
				this.activity_max = activity;
				var digit = 153;
				}
				a.style.background = 'rgb(' + digit + ',' + digit + ',' + digit
	+ ')';
			} else {
				if (activity > this.activity_max) {
				this.activity_max = activity;
				this.normalize_activity( );
				} else {
				var digit = parseInt(225 - 72 * activity / this.activity_
	max);
				a.style.background = 'rgb(' + digit + ',' + digit + ',' +
	digit + ')';
				}	
			}

		}

		if (this.status == 1) {
			this.swap_content(this.num_loaded);
			var playhead = document.getElementById('animate_playhead');
			playhead.style.left = 1 + (390 * this.num_loaded / (this.urls.length
	- 1)) + 'px';
			this.set_info(this.num_loaded);
			this.pos = this.num_loaded;
		
		}

		this.num_loaded++;

		if (this.num_loaded < this.urls.length &&
			this.status != 0) {
			var url = 'http://' + this.hostname + this.urls[this.num_loaded];

			var request = new XMLHttpRequest( );
			request.open('GET', url, true);
			var _this = this;
			request.onreadystatechange = function( ) {
				if (request.readyState == 4) {
				_this.loaded(request);
				}
			}	
			request.send(null);

		} else if (this.num_loaded == this.urls.length) {
			this.pause( );
		}
	}

	Animation.prototype.normalize_activity = function( ) {
		for (var i = 0; i < this.activity.length; i++) {
			var a = document.getElementById('animate_activity' + i);
			var digit = parseInt(225 - 72 * this.activity[i] / this.activity_
	max);
			a.style.background = 'rgb(' + digit + ',' + digit + ',' + digit +
	')';
		}
	}

	Animation.prototype.button = function( ) {
		if (this.status == 3) {
			this.play( );
		} else {
			this.pause( );
		}
	}
	
	Animation.prototype.play = function( ) {
		
		this.status = 2;
		var button = document.getElementById('animate_button').value = 'Pause';

		if (this.pos + 1 == this.urls.length) {
			var playhead = document.getElementById('animate_playhead');
			playhead.style.left = '1px';
			this.pos = 0;
		}

		this.show_frame(this.pos);

		var delay = Math.round(parseFloat(document.getElementById('animate_
	delay').value) * 1000);
		this.interval = window.setInterval(function( ) { animate.show_frame( );
	}, delay);
	}

	Animation.prototype.pause = function( ) {

		this.status = 3;
		var button = document.getElementById('animate_button').value = 'Play';

		if (this.interval != -1) {
			clearInterval(this.interval);
		}
	}

	Animation.prototype.show_frame = function(num) {
	
		if (this.status == 0 || this.status == 3) {
			return;

		}
		
		// If not scrubbing
		if (this.status != 4) {
			var num = this.pos;
			var playhead = document.getElementById('animate_playhead');
			playhead.style.left = 1 + (390 * num / (this.urls.length - 1)) +
	'px';
		}
		this.swap_content(num);
		this.set_info(num);

		if (this.status == 2) {
			if (this.pos + 1 >= this.pages.length) {
				this.pause( );
			} else {
				this.pos++;
			}
		}
	}		

	Animation.prototype.set_info = function(num) {
		
		document.getElementById('animate_info').innerHTML = this.info[num];


		var prev = (num > 0) ? '<a href="#" onclick="animate.prev_frame( );
	return false;" accesskey="p">&larr;</a> ' : '&larr; ';
		var frame = (num + 1) + ' / ' + this.urls.length;
		var next = (num < this.urls.length - 1) ? ' <a href="#"
	onclick="animate.next_frame( ); return false;" accesskey="nw">&rarr; </a>' : '
	&rarr;';
		document.getElementById('animate_status').innerHTML = '<span
	class="text">' + prev + frame + next + '</span>';

	}

	Animation.prototype.playhead = function(e) {
		this.status = 4;
		return false;
	}

	Animation.prototype.prev_frame = function( ) {
		this.pos--;
		this.show_frame(this.pos);
		this.status = 3;
	}
	
	Animation.prototype.next_frame = function( ) {
		if (this.pos + 1 < this.num_loaded) {
			this.pos++;
			this.show_frame(this.pos);

			this.status = 3;
		}
	}

	Animation.prototype.mousemove = function(e) {

		// Make sure the user has clicked on the playhead
		if (animate.status != 4) {
			return;
		}

		var scrubber = document.getElementById('animate_scrubber');
		var left = 0;
		var curr = scrubber;
		while (curr.offsetParent) {
			left += curr.offsetLeft;
			curr = curr.offsetParent;
		}

		var playhead = document.getElementById('animate_playhead');
		var x = e.pageX - left - 5;

		if (x > 391) {
			x = 391;
		} else if (x < 1) {
			x = 1;
		}

		var load_progress = document.getElementById('animate_load_progress');
		if (x > parseInt(load_progress.style.width - 5)) {
			x = parseInt(load_progress.style.width - 5);
		}

		playhead.style.left = x + 'px';

		var snap = Math.floor((x - 1) * (animate.urls.length - 1) / 390);
		if (snap != animate.pos) {
			animate.pos = snap;
			animate.show_frame(snap);
		}
	}
	
	Animation.prototype.mouseup = function(e) {

		if (animate.status != 4) {
			return;
		}

		var scrubber = document.getElementById('animate_scrubber');
		var left = 0;
		var curr = scrubber;
		while (curr.offsetParent) {

			left += curr.offsetLeft;
			curr = curr.offsetParent;
		}

		var playhead = document.getElementById('animate_playhead');
		var x = e.pageX - left - 5;
	
		if (x > 391) {
			x = 391;
		} else if (x < 1) {
			x = 1;
		}

		var load_progress = document.getElementById('animate_load_progress');
		if (x > parseInt(load_progress.style.width)) {
			x = parseInt(load_progress.style.width);
		}

		var snap = Math.floor((x - 1) * (animate.urls.length - 1) / 390);
		if (snap != animate.pos) {
			animate.pos = snap;
			animate.show_frame(snap);
		}
		animate.status = 3;

	}

	Animation.prototype.option = function(input) {

	}

	Animation.prototype.swap_content = function(num) {
	
		var frame = document.getElementById('frame' + num);
		frame.style.display = 'block';

		var height = parseInt(frame.offsetHeight) + 60;
		document.getElementById('bodyContent').style.height = height + 'px';

		if (this.prev != -1) {
			var prev = document.getElementById('frame' + this.prev);
			prev.style.display = 'none';
		}

		this.prev = num;

	}

	Animation.prototype.mediawiki_content = function(text) {
		text = '' + text;
		var start = text.indexOf('<!-- start content -->');
		var end = text.indexOf('<!-- end content -->');
		return text.substr(start, end - start);
	}

	Animation.prototype.check_scroll = function( ) {
		var controls = document.getElementById('animate_controls');
		if (self.pageYOffset > this.scroll_origin) {
			controls.style.top = (self.pageYOffset - this.scroll_origin) + 'px';
		} else {
			controls.style.top = 0;
		}
	}

	var animate = new Animation( );

	// JavaScript diff code thanks to John Resig (http://ejohn.org)
	// http://ejohn.org/files/jsdiff.js
	function diffString( o, n ) {
		var out = diff( o.split(/\s+/), n.split(/\s+/) );
		var str = "";

		for ( var i = 0; i < out.n.length - 1; i++ ) {
			if ( out.n[i].text == null ) {
				if ( out.n[i].indexOf('"') == -1 && out.n[i].indexOf('<') == -1
	&& out.n[i].indexOf('=') == -1 ) {
				str += "<b style='background:#E6FFE6;' class='diff'> " +
	out.n[i] +"</b>";
				} else {
				str += " " + out.n[i];
				}
			} else {
				var pre = "";
				if ( out.n[i].text.indexOf('"') == -1 && out.n[i].text.
	indexOf('<') == -1 && out.n[i].text.indexOf('=') == -1 ) {
				var n = out.n[i].row + 1;
				while ( n < out.o.length && out.o[n].text == null ) {
				if ( out.o[n].indexOf('"') == -1 && out.o[n].
	indexOf('<') == -1 && out.o[n].indexOf(':') == -1 && out.o[n].indexOf(';')
	== -1 && out.o[n].indexOf('=') == -1 ) {
				pre += " <s style='background:#FFE6E6;'
	class='diff'>" + out.o[n] +" </s>";
				}
				n++;
				}
				}
				str += " " + out.n[i].text + pre;
			}
		}

		return str;
	}
	function diff( o, n ) {
		var ns = new Array( );

		var os = new Array( );

		for ( var i = 0; i < n.length; i++ ) {
			if ( ns[ n[i] ] == null ) {
				ns[ n[i] ] = { rows: new Array( ), o: null };
			}
			if (ns[n[i]].rows) {
				ns[ n[i] ].rows.push( i );
			}
		}
		
		for ( var i = 0; i < o.length; i++ ) {
			if ( os[ o[i] ] == null ) {
				os[ o[i] ] = { rows: new Array( ), n: null };
			}
			if (os[o[i]].rows) {
				os[ o[i] ].rows.push( i );
			}
		}
		
		for ( var i in ns ) {
			if ( ns[i].rows.length == 1 && typeof(os[i]) != "undefined" &&
		os[i].rows.length == 1 ) {
				n[ ns[i].rows[0] ] = { text: n[ ns[i].rows[0] ], row: os[i].
		rows[0] };
				o[ os[i].rows[0] ] = { text: o[ os[i].rows[0] ], row: ns[i].
		rows[0] };
				}
			}

			for ( var i = 0; i < n.length - 1; i++ ) {
				if ( n[i].text != null && n[i+1].text == null && o[ n[i].row + 1 ].
		text == null &&
				 n[i+1] == o[ n[i].row + 1 ] ) {
				n[i+1] = { text: n[i+1], row: n[i].row + 1 };
				o[n[i].row+1] = { text: o[n[i].row+1], row: i + 1 };
				}
			}

			for ( var i = n.length - 1; i > 0; i-- ) {
				if ( n[i].text != null && n[i-1].text == null && o[ n[i].row - 1 ].
		text == null &&
				 n[i-1] == o[ n[i].row - 1 ] ) {
				n[i-1] = { text: n[i-1], row: n[i].row - 1 };
				o[n[i].row-1] = { text: o[n[i].row-1], row: i - 1 };
				}
			}

			return { o: o, n: n };
		}


O'Reilly Home | Privacy Policy

© 2007 O'Reilly Media, Inc.
Website: | Customer Service: | Book issues:

All trademarks and registered trademarks appearing on oreilly.com are the property of their respective owners.