BBCode in Text

Adds BBCode support to an antique forum.

// ==UserScript==
// @name           BBCode in Text
// @namespace      http://mailerdaemon.home.comcast.net/
// @include        http://forums.secondlife.com/*
// @version        0.2
// @description    Adds BBCode support to an antique forum.
// ==/UserScript==

//configures how urls are displayed.
const ERRORS = false;
const url_start = 40;
const url_end = 25;
const url_len = url_start + url_end;

GM_addStyle = function(css){
    style = document.createElement("style");
    style.type = "text/css";
    style.innerHTML = css;
	document.getElementsByTagName('head')[0].appendChild(style);
};
//GM_log = function(text){};

GM_addStyle(".GMCode {border: 1px inset ; margin: 0px; padding: 6px; overflow: auto; width: auto; height: auto; text-align: left; font-family:monospace; } .GMCode br {display:none;}")

const chars = String.fromCharCode(38, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 248, 249, 250, 251, 252, 253, 254, 255, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 216, 217, 218, 219, 220, 221, 222, 8364, 34, 223, 60, 62, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 8364, 8226, 160, 161);
const entities = new Array ('amp','agrave','aacute','acirc','atilde','auml','aring',
						'aelig','ccedil','egrave','eacute','ecirc','euml','igrave',
						'iacute','icirc','iuml','eth','ntilde','ograve','oacute',
						'ocirc','otilde','ouml','oslash','ugrave','uacute','ucirc',
						'uuml','yacute','thorn','yuml','Agrave','Aacute','Acirc',
						'Atilde','Auml','Aring','AElig','Ccedil','Egrave','Eacute',
						'Ecirc','Euml','Igrave','Iacute','Icirc','Iuml','ETH','Ntilde',
						'Ograve','Oacute','Ocirc','Otilde','Ouml','Oslash','Ugrave',
						'Uacute','Ucirc','Uuml','Yacute','THORN','euro','quot','szlig',
						'lt','gt','cent','pound','curren','yen','brvbar','sect','uml',
						'copy','ordf','laquo','not','shy','reg','macr','deg','plusmn',
						'sup2','sup3','acute','micro','para','middot','cedil','sup1',
						'ordm','raquo','frac14','frac12','frac34',
						'euro','bull','nbsp','iexcl');

if(!String.prototype.trim) String.prototype.trim = function() { return this.replace(/^\s*/,'').replace(/\s*$/, ''); }
if(!String.prototype.htmlUnescape) String.prototype.htmlUnescape = function(){
	var r = /&(#[0-9]*|[A-Za-z0-9]*);/;
	var i = this;
	var o = ""
	var p;
	do
	{
		if(!(p = r.exec(i)))
			return o + i;
		if(p.index > 0)
			o += i.slice(0, p.index);
		i = i.slice(p.index + p[0].length);
		if(p[1].charAt(0) == '#')
			o += String.fromCharCode(p[1].slice(1))
		else
		{
			var w = entities.indexOf(p[1]);
			o += (w != -1)?chars[w]:("&"+p[1]+";");
		}
	}while(1);
}
if(!String.prototype.htmlEscape) String.prototype.htmlEscape = function(){
	var i = this;
	for(var k = 0;k < chars.length; k++)
		i = i.replace(new RegExp(chars[k], "g"), "&"+entities[k]+";");
	return i;
}
if(!Array.prototype.insert) Array.prototype.insert = function( i, v ) {
	var b = this.splice( i );
	this.push(v);
	return this.concat( b );
};

var count = 0;
var base = document.URL;
count = base.indexOf("?");
if(count >= 0) base = base.substring(0, count);
base = base.substring(0, base.lastIndexOf("/")+1);
var pattern = /(?:\[(?:[\/\\]?)(url|email|thread|post|indent|code|php|font|size|color|b|u|i|right|left|center|highlight|list|img|[*])(?:|=[^\]]*)\]|((?:https?|mms|rstp|secondlife|ftp|mailto):[^\s\[]*)|((?:[a-z0-9.\-]{3,}\.(?:com|net|org|co\.uk)|(?:(?:0x[0-9a-f]{1,8}|[0-9]{1,3})\.){3}(?:0x[0-9a-f]{1,8}|[0-9]{1,3}))(?:(?:\:[0-9]+|)\/[^\s\[]*|(?=[^0-9a-z\-.]|$))))/im;
var split_pattern = /\[([\/\\]?)([^=\]]*?)(?:\s*=\s*"?([^\]"]*)"?\s*|)\]/im;
count = 0;
var calls = 100000;//stops a race condition that doesn't happen anymore.

start();

function start()
{
	var i, j, divs;
	var xpath = "//td[@class='thead']/../../tr[2]/td[position()=2 and @class='alt1']"+"|"+//thread
						"//tr[contains(td, 'Preview')]/td[@class='tcat']/../../tr[2]"+"|"+//edit - preview
						"//td[@class='thead']/../../tr/td[position()=2 and @class='alt2']/..";//topic review
	var res = document.evaluate(xpath, document, null,	XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); 
	for (i = 0; divs = res.snapshotItem(i); ++i) {
		var t = divs.childNodes.length;
//		GM_log(divs.nodeName + "\nChild Count = " + t+ "\n"+divs.innerHTML);
		for (t=0; t<divs.childNodes.length; t++){
			parse(divs.childNodes[t]);
		}
	}
	if(i > 0 || count > 0)
	{
//		GM_log(document.URL.concat("\nHad "+count+" broken tag"+((count != 1)?"s":"") + " in "+i +" messages. (debug = " + calls + " )"));
//		GM_log(calls);
	}
}

function parse(element, only, nourl)
{
	var table = new Array(0);
	var urls = new Array(0);
	var badurls = new Array(0);
	var t = element.firstChild;
	for (;t != null; t = t.nextSibling){
		if(--calls < 0)
			return;
//		GM_log("nodeName = "+t.nodeName+"\nnodeType = "+t.nodeType );
		if(t.nodeType == 3)
		{
			var nodes = pattern;
			var p = pattern.exec(t.nodeValue);
			if(p != null)
			{
//				GM_log(p.length+"\n"+p[0]+"\n"+p[1]+"\n"+p[2]+"\n"+p[3]);
//				if(p[1] != null || p[2] != null || p[3] != null)
				{
					var m=document.createTextNode(p[0]);
					var n=document.createTextNode(t.nodeValue.substring(p.index + p[0].length));
					t.nodeValue = t.nodeValue.substring(0,p.index);
					insertAfter(m, t);
					insertAfter(n, m);
					++count;
				}
				if(p[1] != null)
					table.push(m);
				else if(p[2] != null && !nourl)
					urls.push(m);
				else if(p[3] != null && !nourl)
					badurls.push(m);
				t = t.nextSibling;
			}
		}
		else if(t.nodeType == 1)
		{
			if(t.nodeName != "TEXTAREA" && t.nodeName != "INPUT" && t.nodeName != "PRE")
				parse(t, only, nourl);
		}
//		else
//			GM_log("nodeName = "+t.nodeName+"\nnodeType = "+t.nodeType );
	}
//	if(element.innerHTML)
//		GM_log("Count = "+table.length+"\n"+table+"\n" + element.innerHTML);
	if(table.length > 0)
		tag(table, only);
	var i;
	for(i = 0; i < urls.length; i++)
	{
		var t = urls[i];
		var v = t;
		while(v = v.parentNode)
		{
			if(v.nodeName == "A" || v.nodeName == "PRE")//keep it honest
				break;
		}
		if(v == null && t.parentNode)
		{
//			GM_log(t.nodeValue);
			var m=document.createElement("A");
			m.href = (m.innerHTML = t.nodeValue).htmlUnescape();
			if(m.innerHTML.length > url_len)
				m.innerHTML = m.innerHTML.slice(0,url_start)+"..."+m.innerHTML.slice(m.innerHTML.length-url_end);
			t.parentNode.replaceChild(m, t);
		}
	}
	for(i = 0; i < badurls.length; i++)
	{
		var t = badurls[i];
		var v = t;
		while(v = v.parentNode)
		{
			if(v.nodeName == "A" || v.nodeName == "PRE")//keep it honest
				break;
		}
		if(v == null && t.parentNode)
		{
//			GM_log(t.nodeValue);
			var m=document.createElement("A");
			m.href = "http://"+(m.innerHTML = t.nodeValue).htmlUnescape();
			if(m.innerHTML.length > url_len)
				m.innerHTML = m.innerHTML.slice(0,url_start)+"..."+m.innerHTML.slice(m.innerHTML.length-url_end);
			t.parentNode.replaceChild(m, t);
		}
	}
	badurls = urls = table = null;
}

/*
function juggle(table)
{
	var t;
	var map = new Object();
	for (t=0; t<table.length; t++){
		var type = split_pattern.exec(table[t].nodeValue);
		if(type[1] == "")
		{
			if(map[type[2].toLowerCase()])
				++map[type[2].toLowerCase()];
			else
				map[type[2].toLowerCase()] = 1;
		}
		else
		{
			if(map[type[2].toLowerCase()])
				--map[type[2].toLowerCase()];
			else
				map[type[2].toLowerCase()] = -1;
		}
	}
	map = null;
	return table;
}
*/
function log(table, sep, msg_a, msg_b)
{
	var kw = "";
	Array.forEach(table, function(item) {if(kw !="") kw +=sep;kw += item.nodeValue;});
//	GM_log(msg_a+kw+msg_b);
}

function oc(a)
{
	if(a && a.length)
	{
		var o = {};
		for(var i=0;i<a.length;i++)
		{
			o[a[i]]='';
		}
		return o;
	}
	return null;
}

function tag(table, only)
{
//	log(table, "\n", "tags = [\n","\n]");
	var t;
	var z = oc(only);
	for (t=0; t<table.length; t++){
		if(--calls < 0)
			return;
		var type = split_pattern.exec(table[t].nodeValue);
		if(type)//make sure it is infact a valid tag.
		{
			if(type[1] == "")
			{
				var c = 1;
				var m = t;
				var r = type[2].toLowerCase();
				var dtype;
				var k = table.length - 1;
				if(z && !(r in z));//silently ignore missing tags
				else if(r == "*")
				{
					while(t < k && c != 0)
					{
						if(--calls < 0)
							return;
						dtype = split_pattern.exec(table[++t].nodeValue);
						if(dtype)
						{
							var dt = dtype[2].toLowerCase();
							if(dt == "list")
							{
								if(dtype[1] == "")
									c++;
								else
									c--;
							}
							else if(dt == "*" && c == 1)
								c = 0;
						}
					}
					if(c > 0)
						t++;
				}
				else
				{
					while(t < k && c != 0)
					{
						if(--calls < 0)
							return;
						dtype = split_pattern.exec(table[++t].nodeValue);
						if(dtype)
						{
							if(dtype[2].toLowerCase() == r)
							{
								if(dtype[1] == "")
									c++;
								else
									c--;
							}
						}
					}
					if(c > 0 && (r == "url" || r =="img") && type[3] && type[3] != "")
					{//some idiot didn't close thier URL/IMG tag, *rolls eyes*
//						log(table, ", ", "before: " + table.length+"\n")
						table.splice(t = m+1, 0, insertAfter(document.createTextNode("[/"+r+"]"), table[m]));
//						log(table, ", ", "after: " + table.length+"\n")
						insertAfter(document.createTextNode(type[3]), table[m]);
						k+=1;//adjust the end...
						type[3]="";//makes it work harder
						c = 0;
					}
				}
				
	//			if(table.length > t)
	//				GM_log("( "+(t - m)+", " + c + " ) = " + type[0] +" && " + split_pattern.exec(table[t].nodeValue)[0]);
	//			else
	//				GM_log("( "+(t - m)+", " + c + " ) = " + type[0] +" && last");
				if(c == 0 || r == "*")
				{
					var nodes = new Array(0);
					var p = null;
					var q = null;
					if(r == "url" || r == "thread" || r == "post" || r =="email")
					{
						p = document.createElement("a");
						var d = "";
						if(type[3] != null)
							d = type[3].replace(/  /g,"").htmlUnescape();
						if(r == "thread")
							p.href = (base + "showthread.php?t=" + d);
						else if(r == "post")
							p.href = (base + "showthread.php?p=" + d);
						else if(d != "")
						{
							if(r == "email")
								p.href = (d.trim().toLowerCase().indexOf("mailto:") != 0)?"mailto:"+d:d;
							else if(r == "url")
							{
								var d1 = d.trim();
								var d3 = d1.indexOf("/");
								var d4 = d1.indexOf(":");
								var d5 = d1.indexOf("?");
								if(d4 == -1 || (d4 > d3 && d3 != -1))
								{
									if(d5 == -1 || d3 != -1)
										p.href = "http://"+d;
									else
										p.href = base + d;
								}
								else
									p.href = d;
							}
						}
					}
					else if(r == "color" ||  r == "size" || r == "font")
					{
						p = document.createElement("font");
						if(r == "color")
							p.color = type[3];
						else if(r == "size")
							p.size = type[3];
						else if(r == "font")
							p.face = type[3];
					}
					else if(r == "right" || r == "left" || r == "center")
					{
						p = document.createElement("div");
						if(r == "right")
							p.align = "right";
						else if(r == "left")
							p.align = "left";
						else if(r == "center")
							p.align = "center";
					}
					else if(r == "u" || r == "b" ||  r == "i")
					{
						p = document.createElement(r);
					}
					else if(r == "highlight")
					{
						p = document.createElement("span");
						p.className = "highlight";
					}
					else if(r == "indent")
					{
						p = document.createElement("blockquote");
						q = document.createElement("div");
						p.appendChild(q);
					}
					else if(r == "code" || r == "php")
					{
						p = document.createElement("div");
						p.style.margin="5px 20px 20px";
						q = document.createElement("div");
						q.className = "smallfont";
						q.style.marginBottom="2px";
						q.appendChild(document.createTextNode("Code:"));
						p.appendChild(q);
						q = document.createElement("pre");
						q.className = "alt2 GMCode";
						q.dir = "ltr";
						p.appendChild(q);
					}
					else if(r == "list")
					{//<ol type="1"><li>list item 1</li><li>list item 2</li></ol>
						if(type[3] != "" && type[3] != null)
						{
							p = document.createElement("ol");
							p.type=type[3];
						}
						else
						{
							p = document.createElement("ul");
						}
					}
					else if(r == "img")
					{
						p = document.createElement("img");
						q = document.createElement("span");
					}
					else if(r == "*")
					{
						p = document.createElement("li");
					}
					if(q == null)
						q = p;
					if(p != null)
					{
						var h = table[m];
						var w = h.parentNode;
						var n = h.nextSibling;
						var s = null;
						if(table.length > t)
							s = table[t];
						while(n != s && n != null)
						{
							y = n.nextSibling;
							child = w.removeChild(n);
							if(child.nodeName != "BR" || (r != "code" && r != "php"))
								q.appendChild(child);
							if(child.nodeName == "#text")
								child.nodeValue = child.nodeValue.replace(/([\S]{50,52})  /gm,"$1");
							n = y;
							if(--calls < 0)
								return;
						}
						if(s != null)
						{
							var rtype = split_pattern.exec(s.nodeValue);
							if(rtype[1] != "" && rtype[2].toLowerCase() == r /*/&& w/**/)//catch for lists ~_~
								w.removeChild(s);
						}
						if(r == "code" || r == "php")//keeps it from trying to parse code and php stuff.
						{
						//FIXME: make the replacement smarter, i'm too lazy.
							q.innerHTML = q.innerHTML.replace(/\t/g,"    ");
							//if(r != "code")//oddly enabling this test borks things, not exactly sure what causesit.
								m = t;//no internal parsing
							if(r == "code")//this is the wrong solutions but it works
								parse(q, ["i", "b", "u", "color", "highlight", "url", "thread", "post", "email"], true);
						}
						else if(r == "img")
						{
							if("" == (p.src = q.textContent.trim().htmlUnescape()))
								p = q;
							m = t;//no internal parsing
						}
						else if((r == "url" || r == "email" || r == "post" || r == "thread") && (type[3]=="" || type[3] == null))
						{
							s = q.textContent.trim().replace(/  /g,"").htmlUnescape();
							if(r == "email")
								q.href = (s.toLowerCase().indexOf("mailto:") != 0)?"mailto:"+s:s;
							else if(r == "url")
								q.href = ((s+"/:").indexOf(":") > s.indexOf("/"))?"http://"+s:s;
							else
								p.href += s;
							if(s.length > url_len)
								q.innerHTML = (s.slice(0,url_start)+"..."+s.slice(s.length-url_end)).htmlEscape();
							else
								q.innerHTML = q.textContent;//strip the html from it, it should be unformated.
							m = t;//no internal parsing
						}
						/*/if(w)/**/
							w.replaceChild(p,h);
					}
				}
				else //if(c > 0 && r != "*")
				{
					++t;
					if(ERRORS)
					{
//						GM_log("tag error = " + type[0]);
//						log(table, "\n", "tags = [\n","\n]");
					}
				}
				if(m+1 < t)
					tag(table.slice(m+1, t));
				if((r == "*" && c == 0))// || (c == 1 && r != "*"))
					--t;
			}
			else if(ERRORS)
			{
//				GM_log("tag error = " + type[0]);
//				log(table, "\n", "tags = [\n","\n]");
			}
		}
	}
}

function insertAfter(insert, after)
{
	return after.parentNode.insertBefore(insert, after.nextSibling);
}