Jira Comment Highlighter

By BlindWanderer Last update Oct 4, 2009 — Installed 469 times. Daily Installs: 4, 2, 4, 0, 0, 0, 0, 3, 1, 7, 0, 3, 0, 0, 1, 1, 0, 2, 0, 0, 2, 0, 1, 0, 2, 0, 0, 0, 3, 0, 1, 0

There are 16 previous versions of this script.

// ==UserScript==
// @name           Jira Comment Highlighter
// @namespace      http://home.comcast.net/~mailerdaemon
// @include        *jira*/browse/*-*
// @version        2.0
// ==/UserScript==

const CollapsedAtStart = true;

const AssigneeColor = "#FFCCFF";
const ReporterColor = "#CCFFFF";
const AdminColor = "#FFFFCC";
const ReporterIsAssigneeColor = "#E4E4FF";
const MeColor = "#E4FFE4";
const CriticalColor = "rgba(255, 0, 0, 0.6)";
const InterestingColor = "rgba(96, 96, 256, 0.5)";
const CollapsableColor = "rgba(255, 140, 64, 0.45)";

const CommentColor = "orange";
const ChangeColor = "violet";

const domains = {
	"jira.secondlife.com": {
			admins: "contains(text(), ' Linden') or contains(text(), ' linden') or text()='lindenrobot'",
			interest: ["Linden Lab Issue ID"],
		},
	"jira.atlassian.com": {
			admins: "text()='Support Count Updater' or contains(text(), '[Atlassian]') or contains(text(), '[JIRA Product Manager]')",
		},
	get: function(name, default_value){
			if((a = this[document.location.host]) && (b = a[name]))
				return b;
			return default_value;
		},
	getArrayWith: function(name, array){
			if((a = this[document.location.host]) && (b = a[name]))
				return (array === null)?b:Array.concat(b, array);
			return array;
		},
	};
	
const interest = ["Priority", "Key", "Assignee", "Issue Type", "Type"];
const critical = ["Status", "Resolution", "Parent"];
const comment = ["Comment"]
const collapse = ["Description", "Comment"]; 
const CombinedDiff = ["Description"]
const SideBySideDiff = ["Summary", "Environment"];

const Debug_diffString = 0;

//collapses the extra whitespace
GM_addStyle([
		"div#issue_actions_container > br{ line-height:0px;}",
		"div.action-body, div.action-body p, div.action-body ul, div.action-body ol, div.action-body .panel { margin-bottom: 0;}",
		"div.action-links { background-color: transparent; padding-bottom: 4px;}",
		"div.action-details, div[id^=\"comment-\"][id$=\"-closed\"] > div.actionContainer > div.action-links { background-color: rgba(0, 0, 0, 0.05);}",
		"div#changehistory td { background-color: transparent;} div#changehistory td[bgcolor='#dddddd'] { background-color: rgba(0, 0, 0, 0.13);}",
		
		".collapsable .collapse {width:100%; overflow-y:auto; min-height:1.3em;} .collapsable .collapse[style*=\"height: 0px;\"]:hover { height:auto!important; }",
		".collapsable .sizer {cursor: s-resize; height: 4px; width:100%; background-color: rgba(255, 140, 64, 0.25); -moz-border-radius-topleft:3px;}",//rgba(0, 0, 0, 0.1)
		//".collapsable {background-color: "+CollapsableColor+";}",
		//must overwride div#changehistory td
		".collapsable > td:first-child {padding:0 5px 5px 3px; }",
		".collapsable > td:first-child b {background-color: "+CollapsableColor+" !important; -moz-user-select: none; padding: 0 2px; -moz-border-radius: 6px; margin-right:0.4ch; }",
		
		".diffed { padding:0!important; }",
		".diffed del.diff, .diffed-left del.diff { background-color: rgba(255, 0, 0, 0.3); text-decoration:none;}",
		".diffed ins.diff, .diffed-right ins.diff { background-color: rgba(0, 255, 0, 0.3); text-decoration:none;}",
		".diffed-right del.diff, .diffed-left ins.diff {display:none;}",
		
		".diffed .diff span.beforebr:before { content: \"\u21B2\"; opacity: 0.33;} ",//\u21B2 ↲
		
		"div#issue_actions_container .admin { background-color:"+AdminColor+"!important; }",
		"div#issue_actions_container .reporter { background-color:"+ReporterColor+"!important; }",
		"div#issue_actions_container .assignee { background-color:"+AssigneeColor+"!important; }",
		"div#issue_actions_container .reporter.assignee { background-color:"+ReporterIsAssigneeColor+"!important; }",
		"div#issue_actions_container .me { background-color:"+MeColor+"!important; }",
		"div#issue_actions_container div#changehistory tr.critical { background-color:"+CriticalColor+"; }",
		"div#issue_actions_container div#changehistory tr.interest { background-color:"+InterestingColor+"; }",
/*
		"div#issue_actions_container .actionContainer { border-width: 0px 2px; border-style: solid; }",
		"div#issue_actions_container .actionContainer { border-color: "+ChangeColor+"; }",
		"div#issue_actions_container > div > .actionContainer { border-color: "+CommentColor+"; }",
*/
	].join("\n\n"));

init();

const before = 'text()="';
const after = '"';

var Reporter = find("Reporter","/a");
var base = $X("//div[@id='issue_actions_container']");
if(Reporter && base)
{
//*
	if(admins = domains.get("admins"))
		run(admins, "admin", base);
		
	var Assignee = find("Assignee","/a");
	var Me = $X("//a[starts-with(@id,'user_nav_bar_')]");
	if(Me)	Me = Me.textContent;
	
	if(Assignee && Reporter.textContent == Assignee.textContent)
	{
		if((!Me || Me != Assignee.textContent) && 0 < run(before+Assignee.textContent+after, "reporter assignee", base))
			Assignee.style.backgroundColor = Reporter.style.backgroundColor = ReporterIsAssigneeColor;
	}
	else
	{
		if((!Me || Me.textContent != Reporter.textContent) && 0 < run(before+Reporter.textContent+after, "reporter", base))
			Reporter.style.backgroundColor = ReporterColor;
		if(Assignee && (!Me || Me != Assignee.textContent) && 0 < run(before+Assignee.textContent+after, "assignee", base))
			Assignee.style.backgroundColor = AssigneeColor;
	}
	if(Me && 0 < run(before+Me+after, "me" , base))
	{
		if(Me == Reporter.textContent)
			Reporter.style.backgroundColor = MeColor;
		if(Assignee && Me == Assignee.textContent)
			Assignee.style.backgroundColor = MeColor;
	}
	
	go(".//table[starts-with(@id,'changehistory_')]//tr[td[position()=1 and b[", "contains(text(),'", domains.getArrayWith("interest", interest), "')", "]]]", 
			function(r,i,p){r.className = (r.className + " interest").trim(); return 1;}, base);
	go(".//table[starts-with(@id,'changehistory_')]//tr[td[position()=1 and b[", "contains(text(),'", domains.getArrayWith("critical", critical), "')", "]]]", 
			function(r,i,p){r.className = (r.className + " critical").trim(); return 1;}, base);//*/
	go(".//table[starts-with(@id,'changehistory_')]//tr[td[position()=1 and b[", "contains(text(),'", domains.getArrayWith("comment", comment), "')", "]]]", 
			function(r,i,p){
				r.className = (r.className + " comment").trim();
				var td = r.cells[1];
				td.innerHTML=td.textContent.replace(/^\s*\[\s*/g,"").replace(/\s*\]\s*$/g,"");
			});//*/
	go(".//table[starts-with(@id,'changehistory_')]//tr[td[position()=1 and b[", "contains(text(),'", domains.getArrayWith("CombinedDiff", CombinedDiff).concat(domains.getArrayWith("comment", comment)), "')", "]]]", 
			function(r,i,p){
//				debug("CombinedDiff", r);
				r.className = (r.className + " diff-combined").trim();
				var c = diffString(r.cells[1], r.cells[2]);
				td = r.cells[2];
				td.className="diffed";
				td.colSpan="2";
				td.width="80%";
				remove(r.cells[1]);
				td.innerHTML = c;
				return diffCleanup(td);
			}, base);
	go(".//table[starts-with(@id,'changehistory_')]//tr[td[position()=1 and b[", "contains(text(),'", domains.getArrayWith("SideBySideDiff", SideBySideDiff), "')", "]]]", 
			function(r,i,p){
//				debug("SideBySideDiff", r);
				r.className = (r.className + " diff-sidebyside").trim();
				var c = diffString(r.cells[1], r.cells[2]);
				r.cells[1].className="diffed-left";
				r.cells[2].className="diffed-right";
				r.cells[1].innerHTML = r.cells[2].innerHTML = c;
				return diffCleanup(r.cells[1]);
			}, base);
	//*
	go(".//table[starts-with(@id,'changehistory_')]//tr[td[position()=1 and b[", "contains(text(),'", domains.getArrayWith("collapse", collapse), "')", "]]]", 
			function(r,i,p){
			//TODO disable when initial.scrollHeight < window.getComputedStyle(.collapse).height
				r.className = (r.className + " collapsable").trim();
				var td = document.createElement("td"); td.colSpan="2"; td.width="80%"; td.style.paddingBottom=td.style.paddingRight="0";
				var div = document.createElement("div"); div.className="collapse"; 
				
				td.appendChild(div);
				while(r.cells[1])
					div.appendChild(r.cells[1]);
				
				sizer = document.createElement("div"); sizer.className="sizer";
				td.appendChild(sizer);
				r.appendChild(td);
				
				addEvent(sizer, "mousedown", down);
				addEvent(sizer, "click", stop);
				addEvent(sizer, "dblclick", auto);
				addEvent(r.cells[0], "dblclick", auto);
				
				var b = $X("./b", r.cells[0]);
				b.innerHTML = b.innerHTML.trim();
				
				//addEvent(div, "dblclick", auto);
				
				if(CollapsedAtStart)
					auto({originalTarget:sizer}, true);
				return 1;
			}, base);//*/
}

function debug(t, r)
{
	var u = $X("./div[@class='action-details']/a[@id and @href]",GetParentNodeBy(r, function(n){return n.className.split(" ").indexOf("actionContainer") != -1;})).textContent.trim();
	var n = $X("./b", r.cells[0]).textContent.trim();
	return log({type:t, user:u, name:n});
}

//function GetNextSiblingNode(child, bad) {while((child = child.nextSibling) && child.nodeType != 1);return child?child:bad;}

function find(feild, extra){return $X("//table[@id='issuedetails']/tbody/tr[td[position()=1 and b[text()='"+feild+":']]]/td[2]" + extra );}

function $X(_xpath, node){return document.evaluate(_xpath, node?node:document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null).snapshotItem(0);}
function $Y(_xpath, node){return document.evaluate(_xpath, node?node:document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);}
function $Z(_xpath, func, node, payload){
//	log(_xpath);
	var res = document.evaluate(_xpath, node?node:document, null,	XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
	var i, j;
	for (i = j = 0; link = res.snapshotItem(i); ++i)
		j += func(link, i, payload);
	return j;
}

function merge(group, b, a) {
	return b+group.join(a+" or "+b)+a;
}

function diffCleanup(td) {
	if(document.evaluate("./del or ./ins", td, null, XPathResult.BOOLEAN_TYPE , null).booleanValue)
		return 1;
	var tr = GetParentNodeByTag(td, "tr");
	var tbody = GetParentNodeByTag(td, "tbody");
	tr.parentNode.removeChild(tr);
	if(tbody.rows.length <= 0)
		if(d = GetParentNodeBy(tbody, function(r){ return r.tagName == "div" && r.className == "actionContainer"; }))
			d.parentNode.removeChild(d);
	return 0;
}

function run(group, payload, base) {
	if(group && typeof(group) == "object" && group.length)
	{
		if(group.length > 0)
			group = merge(group, before, after);
		else
			group = null;
	}
	return group?$Z('.//div[starts-with(@class,"actionContainer") and div[starts-with(@class,"action-details") and a['+ group +']]]',
		function(r,i,p){r.className = (r.className + " "+payload).trim(); return 1;}, base):0;
}

function go(left, mleft, array, mright, right, func, base) {
//*
	if(array && array.length > 0)
		$Z(left+merge(array, mleft, mright)+right, func);
/*/ // I don't remember what the purpouse of this choice.
	if(array && array.length && array.length > 0)
		$Z(left+merge(array, mleft, mright)+right, function(r,i,p){ if(i == 1) return func(r,i,p); return 0;});
//*/
}

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 remove(node) {
	return node.parentNode.removeChild(node);
}

function outerHTML(node) {
   var attrs = node.attributes;
   var str = "<" + node.tagName;
   for (var i = 0; i < attrs.length; i++)
      str += " " + attrs[i].name + "=\"" + attrs[i].value.escapeHTML() + "\"";
   return str + (node.innerHTML?">" + node.innerHTML + "</" + node.tagName + ">":"/>");
}

function pairs(str) {
	str = str.replace(/&nbsp;/g, "\u00A0").escapeHTML();
	var resplit = /([\\\/!=.,{}]+|[^\s\u2060(){}@%\^\*~|+\-:;\[\]\\\/!=.,]*(?:[()\[\]]+|[@%\^\*~|+\-]+|[:;]+)?)([\s\u2060]*)/y;
	resplit.lastIndex = 0;
	var text = [], whitespace = [], m;
	while((m = resplit.exec(str)) && m[0] != "")
	{
		text = text.concat(m[1]);
		whitespace = whitespace.concat(m[2]);
	}
	var out = { characters: text, spaces: whitespace, string:str, lastmatch:m};
	if(str != "" && text == []) unsafeWindow.console.log(out);
	return text.length > 0 ?out:null;
}

function split(node, p) {
	if(typeof(node) == "string"){
		return pairs(node);
	}
	else
	{
		var i = 0;
		var os = { type: p, characters: [], spaces: [] };
		for(e = node.lastChild, d = c = node.firstChild; c; c = c.nextSibling)
		{
			if(c.nodeType == c.TEXT_NODE)
			{
				var a = c.nodeValue;
				var f = (c == d);
				var g = (c == e);
				if(f && g)
					a = a.replace(/^\s*|\s*$/g,"");
				else if(f)
					a = a.replace(/^\s*/,"");
				else if(g)
					a = a.replace(/\s*$/,"");
				var t = pairs(a);
				if(t)
				{
					os.characters = os.characters.concat(t.characters);
					os.spaces = os.spaces.concat(t.spaces);
				}
			}
			else if(c.nodeType == c.ELEMENT_NODE)
			{
				if(c.nodeName == "BR")
				{
					os.characters = os.characters.concat("");
					os.spaces = os.spaces.concat("<span class='beforebr'></span><br/>");
				}
				else
				{
					os.characters = os.characters.concat(outerHTML(c));
					os.spaces = os.spaces.concat("");
				}
			}
			else
				unsafeWindow.console.log("Unknown node type: " + c.nodeType);
		}
		return os;
	}
}

function init(){
	String.prototype.escapeHTML = function () { return this.replace(/&/g,'&amp;').replace(/>/g,'&gt;').replace(/</g,'&lt;').replace(/"/g,'&quot;'); };
	if(!String.prototype.trim)
		String.prototype.trim = function trim(){return this.replace(/^\s*|\s*$/g,"");}
		
	Buffer.prototype.toString = function (){return this.running?this.str + "</"+this.running+">":this.str;};
	Buffer.prototype.append = function (tag, text) {
		if(text != "" || (Debug_diffString & 1))
		{
			var tagging = "";
			if(this.running != tag || (Debug_diffString & 2))
			{
				if(this.running) tagging += "</"+this.running+">";
				if(this.running = tag) tagging += "<"+this.running+" class='diff'>";
			}
			this.str += tagging + text;
		}
		return this;
	};
}

function Buffer() { this.running = ""; this.str=""; }

//{
/*
 * Javascript Diff Algorithm
 *  By John Resig (http://ejohn.org/)
 *  Modified by Chu Alan "sprite"
 *
 * Released under the MIT license.
 *
 * More Info:
 *  http://ejohn.org/projects/javascript-diff-algorithm/
 */

//The diff alg is ok but the diffString is a pain in the butt.
//these functions have been tweaked and rewritten for this context.

function diffString( o, n ) {
	var buffer = new Buffer();
	
	var os = split(o, "old");
	var ns = split(n, "new");
		
	var out = diff(os.characters, ns.characters);
	
	if (out.n.length == 0) {
		for(i = 0; i < out.o.length; i++)
			buffer.append("del", out.o[i] + os.spaces[i]);
	} else {
		if (out.n[0].text == null)
			for (n = 0; n < out.o.length && out.o[n].text == null; n++)
				buffer.append("del", out.o[n] + os.spaces[n]);

		for ( var i = 0; i < out.n.length; i++ ) {
			if(out.n[i].text == null)
				buffer.append("ins", out.n[i] + ns.spaces[i]);
			else
			{
				buffer.append("", out.n[i].text);
				
				n = out.n[i].row;
				if(ns.spaces[i] != os.spaces[n])
					buffer.append("ins", ns.spaces[i]).append("del", os.spaces[n]);
				else
					buffer.append("", ns.spaces[i]);
				
				for(n++; n < out.o.length && out.o[n].text == null; n++)
					buffer.append("del", out.o[n] + os.spaces[n]);
			}
		}
	}
	return buffer.toString();
}

function diff( o, n ) {
  var ns = new Object();
  var os = new Object();
  
  for ( var i = 0; i < n.length; i++ ) {
	var name = n[i];
	var t = ns[ name ];
    if ( t == null || t.rows == null )
      ns[ name ] = t = { rows: new Array(), o: null };
    t.rows.push( i );
  }
  
  for ( var i = 0; i < o.length; i++ ) {
	var name = o[i];
	var t = os[ name ];
    if ( t == null || t.rows == null )
      os[ name ] = t = { rows: new Array(), n: null };
    t.rows.push( i );
  }
  
  for ( var i in ns ) {
    if ( ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows != null && 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 && n[i].row + 1 < o.length && 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 && n[i].row > 0 && 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 };
}
//}
//{
	function stop(event){
		if (event)
		{
			if (typeof event.stopPropagation == "function")
				event.stopPropagation();
			if (typeof event.preventDefault == "function")
				event.preventDefault();
		}
		return false;
	}
	
	function down(event){
		addEvent(document, "mouseup", up);
		addEvent(document, "mousemove", movemouse);
		var e = event.originalTarget;
		var t = $X("./../div[@class='collapse']", e);
		var h = parseInt(unsafeWindow.getComputedStyle(t, null).height.replace(/^(\d+)(?:\.\d*)?px$/, "$1"));
		startdrag = {y: event.pageY, h: h, t: t};
		return stop(event);
	}

	function up(event){
		removeEvent(document, "mousemove", movemouse);
		removeEvent(document, "mouseup", up);
		if(startdrag)
		{
			startdrag=null;
			return stop(event);
		}
	}

	function movemouse(event){
		if (startdrag)
		{
			var p = event.pageY + startdrag.h - startdrag.y;
			startdrag.t.style.height = (p<0?0:p) + "px";
			return stop(event);
		}
	}
	
	function auto(event, trick){
		var link = $X(".//div[@class='collapse']", tr = GetParentNodeByTag(event.originalTarget, "TR"));
		if(link.style.height != "")
		{
			link.style.height = "";
		}
		else
		{
			var pt = link;
			addEvent(pt, "click", function poke() { removeEvent(pt, "click", poke); if(link.style.height == "1px" || link.style.height == "0px") link.style.height = "";});
			if(trick == true)
			{
				link.style.height = "0px";
			}
			else
			{
				link.style.height = "1px";
				addEvent(link, "mouseout", function out(){ removeEvent(link, "mouseout", out); if(link.style.height == "1px") link.style.height = "0px"; });
			}
		}
		if(trick != true)
			return stop(event);
	}
	
	function addEvent( obj, type, fn, capture ) {
		obj.addEventListener( type, fn, capture?true:false );
	}
	
	function removeEvent( obj, type, fn, capture ) {
		obj.removeEventListener( type, fn, capture?true:false );
	}
	
	function GetParentNodeByTag(child, tag, bad) {
		tag = tag.toUpperCase();
		while((child = child.parentNode) && child.tagName != tag);
		return child?child:bad;
	}
	
	function GetParentNodeBy(child, func, bad) {
		var i = 0;
		while((child = child.parentNode) && !func(child, i)) i++;
		return child?child:bad;
	}
//}