Jira Issue Links & Files Tablification

Makes Links and Files sections of Issues into tables that can be sorted

// ==UserScript==
// @name           Jira Issue Links & Files Tablification
// @namespace      http://home.comcast.net/~mailerdaemon
// @description    Makes Links and Files sections of Issues into tables that can be sorted
// @include        *jira*/browse/*-*
// @version        1.9
// ==/UserScript==

//Test URLS
//http://jira.atlassian.com/browse/JRA-16868
//http://jira.activemath.org/browse/IG-370
//http://jira.atlassian.com/browse/JRA-5959
//http://jira.secondlife.com/browse/WEB-856
//http://jira.atlassian.com/browse/JRA-2509

init();

GM_addStyle([
		"table.jilft {}",
		"table.jilft tr.row > td:nth-child(n+2) { border-left:1px dotted rgba(0, 0, 0, 0.133);}",
		"table.jilft tr.header td { font-size:9px; border-bottom:1px solid rgba(0,0,0,0.25);  cursor:pointer; -moz-user-select: none;}",

		"table.links_table { width:100%; }",
		"table.links_table td { padding-left: 1px; padding-right: 1px; }",
		
		"table.files_table { empty-cells:show; width:auto; }",
		"table.files_table td { padding: 1px 3px; }",// text-shadow: 0 0 2px white;
		"#attachment_thumbnails li.latest, table.files_table tr.latest { "+CSSmulti("box-shadow", "0 0 8px rgba(0, 255, 0, 0.20)") +"}", //0.025
		"#attachment_thumbnails li.latest, table.files_table tr.latest td { background-color:rgba(0, 255, 0, 0.08); text-shadow: 0 0 2px white, 0 0 2px white, 0 0 2px white, 0 0 1px white;}",
		"table.files_table tr.stale td { color: rgb(153, 153, 153); }",
		"table.files_table tr.stale td:hover { color: rgb(138, 138, 138); }",
		
		"table.links_table tr.header, table.links_table td.key { white-space: nowrap; }",
		
		"#attachment_thumbnails span.blender {display:none;}",
		
		".t_links tbody td { width: auto; }",

		"#attachment_thumbnails .attachment-content { height: 256px;}",
		"#attachment_thumbnails .attachment-content > td { display:none; }",
		"#attachment_thumbnails .attachment-size { top:auto; float:none;}",
		"#attachment_thumbnails .attachment-title, #attachment_thumbnails dl { height:auto; }",
		"#attachment_thumbnails dt, #attachment_thumbnails dd { line-height:1.1em; font-size:90%; text-align:center; float:none;}",
		//copied from atlassian css
		"a.user-no-hover.user-avatar, span.user-no-hover.user-avatar { background-position: left center; background-repeat: no-repeat; padding: 2px 0 2px 19px; }",
	].join("\n\n"));

//update these!!!
var priorities = ["Showstopper","Critical","Major","Normal","Low","Trivial","Nice to have"];

var statuses = {
	"Open":1,
	"In Progress":50,
	"Reopened":100,
	"Fix Pending":150,
	"Resolved":200,
	"Closed":250,
	"Ready for Translation":-1,
	"Translation In Progress":-1,
	"Translated":-1,
	"In QA":-1,
	"Passed QA":-1,
	"Translation Final":-1,
	"Published":-1,
	"On Hold":-1,
	"Editing In Progress":-1,
	"Edited":-1,
	"Testing":-1,
	"Verified":-1,
	"Verified: Fixed":-1,
	"Verified: Fix Needed":-1,
	"Republished":-1,
	large:10000,
}

if($X("//div[contains(concat(' ', @class, ' '), ' content ') and div[contains(concat(' ', @class, ' '), ' module ')]]"))
{//V4.1.1
	$Z("//div[contains(concat(' ', @class, ' '), ' content ')]/div[@id='attachmentmodule']", function(module){
			$Z(".//ol[@id='file_attachments']", function(r, i){
						var table = document.createElement("table");
						table.cellPadding = 1; 
						table.cellSpacing = 0;
						table.className = "files_table jilft";
						table.setAttribute("order", [-1]);
						tbody = document.createElement("tbody");
						table.appendChild(tbody);
							
						var tr = document.createElement("tr");
						tr.align="center"
						tr.style.whiteSpace="nowrap";
						tr.style.boarderBottom="1px;"
						tr.className="header row";
							
						tr.appendChild(NewHeader(1, "#", "Initial Ordering"));
						tr.appendChild(NewHeader(2, "T", "Type"));
						tr.appendChild(NewHeader(3, "Filename"));
						tr.appendChild(NewHeader(4, "Size"));
						tr.appendChild(NewHeader(5, "Date"));
						tr.appendChild(NewHeader(6, "Uploader"));
						tr.appendChild(NewHeader(7, "Latest"));
						
						tbody.appendChild(tr);
						
						$Z("li[contains(concat(' ', @class, ' '), ' attachment-content ')]", function(li, i){
								tbody.appendChild(File(i + 1, 
										$X(".//div[contains(concat(' ', @class, ' '), ' attachment-thumb ')]/a", li), 
										$X(".//*[contains(concat(' ', @class, ' '), ' attachment-title ')]/descendant-or-self::a", li), 
										$X(".//dd[contains(concat(' ', @class, ' '), ' attachment-size ')]", li)
									))
							}, r);
						
						var click = generic_click(1, FileSort);//FileShifter
						click.apply(tr.cells[0]);//we cheat!
					
						replace(r, table);
						//r.parentNode.appendChild(table);
						
						for(j = 0; j < tr.cells.length; j++)
							tr.cells[j].addEventListener('click', click, false);
					}, module);
			$Z(".//ol[@id='attachment_thumbnails']", function(ol, i){
				$Z("li[contains(concat(' ', @class, ' '), ' attachment-content ')]", function(li, i){
						//just provide the missing info
						var dl = $X("dl", li);
						var thumb = $X(".//div[contains(concat(' ', @class, ' '), ' attachment-thumb ')]/a", li);
						var link = $X(".//*[contains(concat(' ', @class, ' '), ' attachment-title ')]/descendant-or-self::a", li);
						var size = $X(".//dd[contains(concat(' ', @class, ' '), ' attachment-size ')]", li)
						obj = FileObj(i + 1, null, link, size)
						
						FileSetAttributes(li, obj)
						
						if(!(date = $X("dd[contains(concat(' ', @class, ' '), ' attachment-date ')]", dl)))
							dl.insertBefore(date = document.createElement("dd"), size).appendChild(obj.date[0]);
						dl.insertBefore(creator = document.createElement("dd"), size).appendChild(obj.creator[0]);
	//					dl.appendChild(latest = document.createElement("dd")).appendChild($X("td[7]/text()", tr));
						
						//GM_log(tr.className);
						
						li.title = link.title;
					}, ol);
				
				var table = document.createElement("table");
				table.cellPadding =1; 
				table.cellSpacing = 0;
				table.className = "files_table jilft";
				ol.setAttribute("order", [-1]);
				tbody = document.createElement("tbody");
				table.appendChild(tbody);
					
				var tr = document.createElement("tr");
				tr.align="center"
				tr.style.whiteSpace="nowrap";
				tr.style.boarderBottom="1px;"
				tr.className="header row";
					
				tr.appendChild(NewHeader(1, "Initial Ordering"));
				tr.appendChild(NewHeader(2, "Filename"));
				tr.appendChild(NewHeader(3, "Size"));
				tr.appendChild(NewHeader(4, "Date"));
				tr.appendChild(NewHeader(5, "Uploader"));
				tr.appendChild(NewHeader(6, "Latest"));
				
				tbody.appendChild(tr);
				
				var click = generic_list_click(ol, ImageSort);
				click.apply(tr.cells[0]);//we cheat!
			
				ol.insertBefore(table, ol.firstChild);
				
				for(j = 0; j < tr.cells.length; j++)
					tr.cells[j].addEventListener('click', click, false);
				}, module);
		});
	{//Link Tables
		var click = generic_click(2, LinkSort)//, LinkShifter);
		
		$Z("//table[@id = 'outwardLinks_table' or @id = 'inwardLinks_table']", function L1(table){
			table.className = (table.className + " links_table jilft").trim();
			var top = $X("thead/tr/th", table);
			top.colSpan=5;
			table.setAttribute("order", [1]);
			
			$Z("tbody/tr", function(tr, i){
					var summary = $X("td/div[contains(concat(' ', @class, ' '), ' flooded ')]", tr);
					var wrapper = $X(".//div[contains(concat(' ', @class, ' '), ' attribute-wrap ')]", summary);
					var priority = $X(".//span[contains(concat(' ', @class, ' '), ' priority ')]", wrapper);
					var status = $X(".//span[contains(concat(' ', @class, ' '), ' status ')]", wrapper);
					var link = $X(".//a", summary);
					
					tr.className = tr.className.split(" ").concat("row").join(" ");
					tr.setAttribute("number", i + 1);
					replace(summary,  document.createTextNode(i + 1));
					
					tr.appendChild(key = document.createElement("td")).appendChild(link);
					tr.appendChild(sum = document.createElement("td")).appendChild(summary);
					
					if(priority)
					{
						var s = $X(".//img", priority).alt;
						var e = priorities.indexOf(s);
						if(e == null || e == -1) e = priorities.length;
						tr.setAttribute("priority", e + 1);
						tr.appendChild(document.createElement("td")).appendChild(priority);
					}
					else
					{
						tr.appendChild(document.createElement("td"));
						tr.setAttribute("priority", priorities.length + 1);
					}
					tr.appendChild(document.createElement("td")).appendChild(status);
					
					key.className = "key";
					sum.title = link.title && link.title.trim();
					
					var s = link.textContent.trim();
					var e = s.lastIndexOf("-") + 1;
					tr.setAttribute("key", s.slice(0, e).concat(Array(9 + e - s.length).join("0"), s.slice(e)));
					
					var s = $X(".//img", status).alt;
					var e = statuses[s];
					if(e == null || e == -1) e = statuses.large;
					tr.setAttribute("status", e);
					
					if(!$X("*", wrapper))
						remove(wrapper);
					else
						GM_log("unhandled attribute!")
				}, table);
			
			var tr = document.createElement("tr");
			tr.align="center"
			tr.className="header row";
			
			tr.appendChild(number = NewHeader(1, "#", "Initial Ordering", {status:true}));
			tr.appendChild(key = NewHeader(2, "Key", null, {status:true}));
			tr.appendChild(summary = NewHeader(3, "Summary", null, {status:true}));
			tr.appendChild(priority = NewHeader(4, "P", "Priority", {status:true}));
			tr.appendChild(status = NewHeader(5, "S", "Status", {status:true}));

			summary.width="100%";
			
			insertAfter(tr, top.parentNode);
			
			for(j = 0; j < tr.cells.length; j++)
				tr.cells[j].addEventListener('click', click, false);
		});
	}
}
else
{//Old V3 to V4.1.0
	if((files = $X("//td[@id='file_attachments']")) && (files.textContent.trim() != "None")) {
		table = document.createElement("table");
		table.cellPadding =1; 
		table.cellSpacing = 0;
		table.className = "files_table jilft";
		table.setAttribute("order", [-1]);
		tbody = document.createElement("tbody");
		table.appendChild(tbody);
			
		var tr = document.createElement("tr");
		tr.align="center"
		tr.style.whiteSpace="nowrap";
		tr.style.boarderBottom="1px;"
		tr.className="header row";
			
		tr.appendChild(NewHeader(1, "#", "Initial Ordering"));
		tr.appendChild(NewHeader(2, "T", "Type"));
		tr.appendChild(NewHeader(3, "Filename"));
		tr.appendChild(NewHeader(4, "Size"));
		tr.appendChild(NewHeader(5, "Date"));
		tr.appendChild(NewHeader(6, "Uploader"));
		tr.appendChild(NewHeader(7, "Latest"));
		
		tbody.appendChild(tr);

		var items = [];
		var nodes = [files];
		while(nodes.length > 0)
		{
			var node = nodes.shift();
			var offset = 0;
			do
			{
				if(offset + 7 < node.childNodes.length)
				{
					while(e = /^[\d]+\.$/.exec(node.childNodes[offset].nodeValue.trim()))
					{
						tbody.appendChild(File(e[0], node.childNodes[offset + 1], node.childNodes[offset + 3], node.childNodes[offset + 5]));
						offset += 7;
					}
				}
				if((node.childNodes.length > ++offset) && node.childNodes[offset].nodeName == "SPAN")
					nodes.push(node.childNodes[offset++]);
				else
					break;
			}while(true);
		}
		
		var click = generic_click(1, FileSort);//FileShifter
		click.apply(tr.cells[0]);//we cheat!
		
		files.innerHTML="";
		files.appendChild(table);
		
		for(j = 0; j < tr.cells.length; j++)
			tr.cells[j].addEventListener('click', click, false);
	}

	{//Link Tables
		var click = generic_click(2, LinkSort, LinkShifter);
		
		$Z("//table[@id = 'outwardLinks_table' or @id = 'inwardLinks_table']", function L1(r, i, p){
			r.className = (r.className + " links_table jilft").trim();
			top = $X(".//tr[1]/td[@colspan]", r);
			top.colspan=5;
			r.setAttribute("order", [1]);
			
			$Z(".//tr[position()>1 and td[@width]]", L2, r);
			
			var tr = document.createElement("tr");
			tr.align="center"
			tr.className="header row";
			
			tr.appendChild(number = NewHeader(1, "#", "Initial Ordering", {status:true}));
			tr.appendChild(key = NewHeader(2, "Key", null, {status:true}));
			tr.appendChild(summary = NewHeader(3, "Summary", null, {status:true}));
			tr.appendChild(priority = NewHeader(4, "P", "Priority", {status:true}));
			tr.appendChild(status = NewHeader(5, "S", "Status", {status:true}));

			insertAfter(tr, top.parentNode);
			
			for(j = 0; j < tr.cells.length; j++)
				tr.cells[j].addEventListener('click', click, false);
		});
	}
}

/*
{//Sub-Tasks
	//https://jira.secondlife.com/browse/SVC-5165
	$Z("//form[@id='stqcform']/table[@id='issuetable']", function(r, i){
			r.className = (r.className + " links_table jilft").trim();
			r.setAttribute("order", [1]);
			
			$Z("./tbody/tr", function(tr, i){
					tr.setAttribute("sequence", Number(tr.cells[0]).textContent))
					var sum = $X(".//a", tr.cells[1]);
					tr.cells[1].colSpan="2"
					var key = /\/browse\/([A-Z]{3,})-([\d]+)/.match(sum.getAttribute("href"))
					tr.setAttribute("key", key[1] + key[2].leftPad(7))
					tr.setAttribute("summary", sum.textContent)
					tr.setAttribute("issuetype", $X(".//img", tr.cells[2])).alt)
					tr.setAttribute("status", statuses[$X(".//img", tr.cells[3])).alt])
					
					var assignee = $X("./td[contains(concat(concat(' ', @class), ' '), ' assignee ')]//img", tr);
					
					tr.setAttribute("assignee", )
				}, r);
			
			var tr = document.createElement("tr");
			tr.align="center"
			tr.className="header row";
			
			tr.appendChild(number = NewHeader(1, "#", "Initial Ordering", {status:true}));
			tr.appendChild(key = NewHeader(2, "Key", null, {status:true}));
			tr.appendChild(summary = NewHeader(3, "Summary", null, {status:true}));
			tr.appendChild(priority = NewHeader(4, "P", "Priority", {status:true}));
			tr.appendChild(status = NewHeader(5, "S", "Status", {status:true}));

			insertAfter(tr, top.parentNode);
			
			for(j = 0; j < tr.cells.length; j++)
				tr.cells[j].addEventListener('click', click, false);
		});
	var click = generic_click(2, TaskSort);
}
//*/
function TaskSort(by, a, b){
	by = Math.abs(by) - 1;
	var name = ["sequence", "key", "summary", "issuetype", "status", "assignee"][by];
	var left, right, ret = 0;
	if(name)
	{
		left = a.getAttribute(name);
		right = b.getAttribute(name);
	}
	switch(by)
	{
		case 2:
			left = a.cells[2].title;
			right = b.cells[2].title;
		case 1:
			ret = (left==right)?0:(left<right)?-1:1;
			break;
		default:
			ret = left - right;
			break;
	}
	return ret;
	
}

function TaskShifter(row){
	return $X("./td[1]/div", row);
}

function LinkSort(by, a, b){
	by = Math.abs(by) - 1;
	var name = ["number", "key", null, "priority", "status"][by];
	var left, right, ret = 0;
	if(name)
	{
		left = a.getAttribute(name);
		right = b.getAttribute(name);
	}
	switch(by)
	{
		case 2:
			left = a.cells[2].title;
			right = b.cells[2].title;
		case 1:
			ret = (left==right)?0:(left<right)?-1:1;
			break;
		default:
			ret = left - right;
			break;
	}
	//log({by:by, name:name, left:left, right:right, ret:ret});
	return ret;
}

function LinkShifter(row){
	return $X("./td[1]/img", row);
}

function FileShifter(row){
	return row.cells[0];
}

function ImageSort(by, a, b){
	by = Math.abs(by) - 1;
	var name = ["number", "name", "size", "date", "creator", "latest"][by];
	var left = a.getAttribute(name), right = b.getAttribute(name);
	var ret = 0;
	switch(by)
	{
		case 5:
			left = (left=="false");
			right = (right=="false");
		case 0:
		case 2:
		case 3:
			ret = left - right;
			break;
//		case 1:
//			left = left.toLowerCase();
//			right = right.toLowerCase();
		default:
			ret = (left==right)?0:(left<right)?-1:1;
			break;
	}
//	log({by:by, name:name, left:left, right:right});
	return ret;
}

function FileSort(by, a, b){
	by = Math.abs(by) - 1;
	var name = ["number", "type", "name", "size", "date", "creator", "latest"][by];
	var left = a.getAttribute(name), right = b.getAttribute(name);
	var ret = 0;
	switch(by)
	{
		case 0:
		case 3:
		case 4:
			ret = left - right;
			break;
		case 6:
			left = (left=="false");
			right = (right=="false");
		default:
			ret = (left==right)?0:(left<right)?-1:1;
			break;
	}
//	log({by:by, name:name, left:left, right:right});
	return ret;
}

function NewHeader(num, text, name, obj){
	td = document.createElement("td")
	td.appendChild(document.createTextNode(text));
	td.title = "Sort by " + (name?name:text);
	td.setAttribute("type", num);
	if(obj)
		for(var a in obj)
			td.setAttribute(a, obj[a]);
	return td;
}

function FileObj(num, icon, link, size)
{
	var m = /^(?:(.+?) - |)(?:(Latest) |)(([^-]+)|([\d]{4})-([\d]{2})-([\d]{2}) ([\d]{2}):([\d]{2})) - ([^-]+)$/.exec(link.title);
	var latest = m[2] != null;
	var date = m[3];
	var creator = m[10];
	var nice_date = (m[4] && JiraTimeStringToDate(m[4])) || (m[5] && new Date(m[5],m[6],m[7],m[8],m[9]))
	var name = m[1] || link.textContent.trim()
	
	m = /([\d]+(?:\.[\d]+)?) ([kTGM]B)/.exec(size.textContent);
	var bytes = parseFloat(m[1]);
	switch(m[2])
	{
		case "TB":
			bytes *= 1024;
		case "GB":
			bytes *= 1024;
		case "MB":
			bytes *= 1024;
		case "kB":
			bytes *= 1024;
			break;
	}
	
	//GM_log([num,icon,link,size,m[3],m[2]].join(", "));
	var crl = $X("//a[text()=\""+creator+"\" and @style]") || $X("//a[text()=\""+creator+"\"]");
	var cre = document.createElement("a")
	cre.appendChild(document.createTextNode(creator));
	//*
	if(crl)
	{
		cre.href = crl.getAttribute("href");
		//cre.className = crl.className;
		cre.className = (" "+crl.className+" ").replace(" user-hover "," user-no-hover ").trim();//hover doesn't work!
		if(crl.style.cssText)
			cre.style.cssText = crl.style.cssText;
	}
	//*/
	//unsafeWindow.console.log(crl)
	//unsafeWindow.console.log(cre)
	var simple_size = size.cloneNode(false);
		simple_size.appendChild(document.createTextNode(m[0]));
	
	obj = {
			number: [document.createTextNode(num), parseInt(num)],
			name:[link.cloneNode(true), name.toLowerCase()],
			size:[simple_size, bytes],
			date:[document.createTextNode(date), (new Date()) - nice_date],
			creator:[cre, creator.toLowerCase()],
			latest:[document.createTextNode(latest?"Latest":" "), latest],
		}
	if(icon)
		obj.type = [icon.cloneNode(true), $X("./img",icon).alt];
	
	return obj;
}

function FileSetAttributes(file, obj)
{
	for(var a in obj)
		file.setAttribute(a, obj[a][1]);
	
	if(typeof(obj.latest) !== "undefined")
		file.className = (file.className + (obj.latest[1]?" latest":" stale")).trim();
}

function File(num, icon, link, size){
	obj = FileObj(num, icon, link, size);
	
	tr =document.createElement("tr");

	tr.className="row";
	[obj.number, obj.type, obj.name, obj.size, obj.date, obj.creator, obj.latest].forEach(function (i){
			tr.appendChild(td = document.createElement("td"));
			td.appendChild(i[0]);
		})
	
	FileSetAttributes(tr, obj);
	
	return tr;
}

function generic_list_click(ol, sorter){
	return function (event) {
		var type = parseInt(this.getAttribute("type"));
		var a_type = Math.abs(type);

		var order = ol.getAttribute("order");
		if(order == "" || order == null)
			order = [1];
		else
			order = order.split(",").map(function(f){return parseInt(f);});
		var k;
		
	//	GM_log("------------------------------------------------");
		
		if(Math.abs(order[0]) == a_type)
			order[0] = -order[0];
		else if(type == 1)
			order = [1];//numbers are unique a good opertunity to clear the list.
		else
			(order = order.filter( function(r){return (Math.abs(r) != a_type);})).unshift(type);
		
		ol.setAttribute("order", order.join(","));
		
		//we cheated <.<
		var rows = [];
		$Z("li", function(r,i){ rows[i]=r; }, ol);
		
		rows.sort(function(a, b){
			var i = 0, j = 0, neg = (order[0] < 0);
			//we don't bounds check this because every entry is unique.
			//if we overflow order than there is something SERIOUSLY wrong with the data.
			while(!(j = sorter(order[i], a, b)))
			{
				if(order[i] == null)
					break;
				neg ^= (order[++i] < 0);
			}
			return neg ? -j : j;
		});
		
		for(k = 0; k < rows.length; ++k)
			ol.appendChild(remove(rows[k]));
		//	if(n = $X("li["+(k+2)+"]", ol))
		//		if(rows[k].getAttribute("number") != n.getAttribute("number"))
		//			insertAfter(rows[k], n);
		
		//[2,4,1,3] -> [1,2,3,4]
		//if(a != b)
			
		/*
		log({rows:rows.map(function(f){return f.getAttribute("number");}), order:order, 
				"order-names":order.map(function(by){return tbody.rows[RS-1].cells[Math.abs(by) - 1].textContent;})});
		//*/
		delete rows;
	}
}

function generic_click(RS, sorter, deshifter){
	return function (event) {
		var type = parseInt(this.getAttribute("type"));
		var a_type = Math.abs(type);
		var table = $X(".//ancestor::table[1]", this);
		var tbody = $X("tbody", table);
		var order = table.getAttribute("order");
		if(order == "" || order == null)
			order = [1];
		else
			order = order.split(",").map(function(f){return parseInt(f);});
		var k;
		
	//	GM_log("------------------------------------------------");
		
		if(Math.abs(order[0]) == a_type)
			order[0] = -order[0];
		else if(type == 1)
			order = [1];//numbers are unique a good opertunity to clear the list.
		else
			(order = order.filter( function(r){return (Math.abs(r) != a_type);})).unshift(type);
		
		table.setAttribute("order", order.join(","));
		
		//we cheated <.<
		var rows = new Array(table.rows.length - RS);
		for(k = 0; k < rows.length; ++k)
			rows[k] = table.rows[k + RS];
		
		rows.sort(function(a, b){
			var i = 0, j = 0, neg = (order[0] < 0);
			//we don't bounds check this because every entry is unique.
			//if we overflow order than there is something SERIOUSLY wrong with the data.
			while(!(j = sorter(order[i], a, b)))
			{
				if(order[i] == null)
					break;
				neg ^= (order[++i] < 0);
			}
			return neg ? -j : j;
		});
		
		if(deshifter)
		{
			//keep the numbers in place
			if(rows[0] != table.rows[RS])
				swap(deshifter(rows[0]), deshifter(table.rows[RS]), "n[0]<->o[0]");
			if(k = (rows.length - 1))
			{
				if(rows[0] == table.rows[k+RS]) //new[0] == old[last]
				{
					if(rows[k] != table.rows[RS])
						swap(deshifter(rows[k]), deshifter(table.rows[RS]), "n[k]<->o[0]");
				}
				else
				{
					if(rows[k] != table.rows[k+RS])
						swap(deshifter(rows[k]), deshifter(table.rows[k+RS]), "n[k]<->o[k]");
				}
			}
		}
		
		for(k = 0; k < rows.length; ++k)
			if(rows[k] != table.rows[k+1])
				insertAfter(rows[k], table.rows[k+1]);
		/*
		log({rows:rows.map(function(f){return f.getAttribute("number");}), order:order, 
				"order-names":order.map(function(by){return tbody.rows[RS-1].cells[Math.abs(by) - 1].textContent;})});
		//*/
		delete rows;
	}
}

function CSSmulti(name, value){
	var m = name+": "+value+"; ";
	return ["-moz-", "-webkit-", "-ie-", ""].map(function(f){return f + m;}).join("");
}

function L2(r, i, p){
	var summary = r.cells[1];
	var key = document.createElement("td");
	var priority = r.cells[2];
	var status = r.cells[3];
	insertBefore(key, summary);
	r.className = r.className.split(" ").concat("row").join(" ");
	
	r.setAttribute("number", i + 1);
	
	var link = $X(".//font/a", summary);
	var font = link.parentNode.cloneNode(false);
	font.appendChild(link);
	key.appendChild(font);
	key.className = "key"

	summary.title = link.title && link.title.trim();
	
	var s = link.textContent.trim();
	var e = s.lastIndexOf("-") + 1;
	r.setAttribute("key", s.slice(0, e).concat(Array(9 + e - s.length).join("0"), s.slice(e)));
	
	var s = $X(".//img", r.cells[3]).title.split(" - ", 1)[0];
	var e = priorities.indexOf(s);
	if(e == null || e == -1) e = priorities.length;
	r.setAttribute("priority", e + 1);
	
	var s = $X(".//img", r.cells[4]).title.split(" - ", 1)[0];
	var e = statuses[s];
	if(e == null || e == -1) e = statuses.large;
	r.setAttribute("status", e);
}

function log()
{
	var arg;
	switch(arguments.length)
	{
		case 1:
			arg = arguments[0];
			break;
		case 0:
			arg = null;
			break;
		default:
			arg = arguments;
			break;
	}
	
	var f = JSON.stringify(arg);
	if(typeof(unsafeWindow.console) != "undefined" && typeof(unsafeWindow.console.log) != "undefined")
		unsafeWindow.console.log(f);
	else
		GM_log(f);
	return arg;
}

//*/

function $X(_xpath, node){//to search in a frame, you must traverse it's .contentDocument attribute.
    var doc = (node)?(node.ownerDocument || node):(node = document);
    return doc.evaluate(_xpath, node, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null).snapshotItem(0);
}
function $Y(_xpath, node){
    var doc = (node)?(node.ownerDocument || node):(node = document);
    return doc.evaluate(_xpath, node, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
}
function $Z(_xpath, func, node){
    var doc = (node)?(node.ownerDocument || node):(node = document);
    var res = doc.evaluate(_xpath, node, null,	XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
    var args = Array.prototype.slice.call(arguments, 3);
    var i = 0;
    for (; i < res.snapshotLength; ++i)
        func.apply(func, [res.snapshotItem(i), i].concat(args));
    return i;
}

function insertAfter(insert, after){return after.parentNode.insertBefore(insert, after.nextSibling);}
function insertBefore(insert, before){return before.parentNode.insertBefore(insert, before);}
function remove(r){return r.parentNode.removeChild(r);}
function replace(old, New){return old.parentNode.replaceChild(New,old);}
function swap(a, b, comment){
	if(a != b)
	{
		var c = a.nextSibling; 
		var ap = a.parentNode;
		var bp = b.parentNode;
		bp.insertBefore(ap.removeChild(a), b);
		ap.insertBefore(bp.removeChild(b), c);
//		GM_log([comment, a.src?a.src:a, b.src?b.src:b].join("\n"));
	}
}
function findParent(node, parent){
	return $X(".//ancestor::"+parent+"[1]", node);
}

function init(){
	if(typeof(String.prototype.trim) == "undefined")
		String.prototype.trim = function() {return this.replace(/^\s+|\s+$/g, "");}
	if(typeof(String.prototype.leftPad) == "undefined")
		String.prototype.leftPad = function (len, text) { return new Array(len - this.length + 1).join(text || '0') + this; }
}

function JiraTimeStringToDate(str){
	return new Date(str.replace(/^(\d{2})\/([A-Z][a-z]{2})\/(\d{2})/, "$2 $1 20$3"));
}