Userscripts Forums bbCode

By Mikado Last update Jul 4, 2008 — Installed 97 times.
// ==UserScript==
// @name           Userscripts Forums bbCode
// @namespace      http://userscripts.org/users/31647
// @description    User-friendly bbCode with keyboard shortcuts.
// @include        http://userscripts.org/forums/*/topics/*
// @include        http://userscripts.org/scripts/show/*
// ==/UserScript==

// here you can add/modify CTRL+key shortcuts
// keep in mind that "w" is reserved for hyperlinks ([url=...][/url])
var bbTags = {
	b: ['[b]', '[/b]'],
	i: ['[i]', '[/i]'],
	u: ['[u]', '[/u]'],
	p: ['[img]', '[/img]'],
	q: ['[quote]', '[/quote]'],
	d: ['[code]', '[/code]']
};
// nothing interesting below

function bb2html(text, rec) {
	if (!rec) {
		var chars = {'"': '&quot;', "'": '&apos;', '<': '&lt;', '>': '&gt;', '&': '&amp;'}, replacer = function(s,m){return chars[s]};
		text = text.replace(/^\s+|\s+$/g, '').replace(/[<>&"']/g, replacer);
	}

	var html = rec ? '' : '<p>', tagRE = /\[([^\]]+)\]|(\n)+/, i, append = '', endBlockRE = /<\/(pre|blockquote)>$/;
	while ((i = text.search(tagRE)) != -1) {
		var match = RegExp.$2 || RegExp.$1, tag = match.split('=')[0], endtag = locateClosingTag(text, i, tag, 0);
		switch (tag) {
		case 'quote':
			if (!i) html = html.replace(/(<p>|<br>)+$/, '');
			var post = match.match(/\d+/);
			if (post) {
				var userlink = document.evaluate('td/span[@class="fn"]/a', document.getElementById(post + '-row'), null, 8, null).singleNodeValue;
			}
			if (endtag.success) {
				html += text.substr(0, i) + '<blockquote>' + (post && userlink ? '<p><a href="' + userlink.pathname + '"><b>' + userlink.textContent + '</b></a>&nbsp;<a href="#' +
				(location.pathname.substr(0, 8) == '/forums/' ? 'posts-' : 'comment-') + post + '">wrote</a>:<p>' : '<p>') + bb2html(text.substring(i + match.length + 2, endtag.value), true) + '</blockquote><p>';
				text = text.substr(endtag.value + 8);
			} else {
				html += text.substr(0, i) + '<blockquote><p>';
				text = text.substr(i + match.length + 2);
				while (endtag.value--) append = '</blockquote>' + append;
			}
			break;
		case 'code':
			var content = text.substring(i + 6, endtag.success ? endtag.value : Infinity), pref = !i || text.substr(0, i).match(/\n\s*$/),
				post = !endtag.success || text.length <= endtag.value + 7 || text.substr(endtag.value + 7).match(/^\s*\n/),
				usePre = (content.indexOf('\n') > 0) || (pref && post);
			if (usePre && !i) html = html.replace(/(<p>|<br>)+$/, '');
			if (!endtag.success) while (endtag.value--) append = (usePre ? '</pre>' : '</code>') + append;
			html += text.substr(0, i) + (usePre ? '<pre>' : '<code>') + content + (endtag.success ? (usePre ? '</pre><p>' : '</code>') : '');
			text = endtag.success ? text.substr(endtag.value + 7) : '';
			break;
		case 'url':
			if (!endtag.success) return null;
			var linktext = text.substring(i + match.length + 2, endtag.value), href = match.split('=')[1];
			if (href == linktext) html += text.substr(0, i) + href;
			else html += text.substr(0, i) + '<a href="' + href + '">' + bb2html(linktext, true) + '</a>';
			text = text.substr(endtag.value + 6);
			break;
		case 'img':
			var imgurl;
			if (!endtag.success) {
				endtag.value = text.substr(i).search(/\s/);
				if (endtag.value < 0) endtag.value = Infinity;
				else endtag.value += i;
				imgurl = text.substring(i + 5, endtag.value);
				endtag.value -= 6;
			} else {
				imgurl = text.substring(i + 5, endtag.value);
			}
			var pref = !i || text.substr(0, i).match(/\n\s*$/), post = text.length <= endtag.value + 6 || text.substr(endtag.value + 6).match(/^\s*\n/),
				addBlock = pref && post ? '<p>' : '';
			if (addBlock && !i) html = html.replace(/(<p>|<br>)+$/, '');
			html += text.substr(0, i) + addBlock + '<img src="' + imgurl + '">' + addBlock;
			text = text.substr(endtag.value + 6);
			break;
		case '\n':
			match = RegExp.lastMatch.length;
			if (!i && html.match(/(<p>|<br>)$/)) ;
			else if (match == 1) html += text.substr(0, i) + '<br>';
			else html += text.substr(0, i) + '<p>';
			text = text.substr(i + match);
			break;
		default:
			if (endtag.success) {
				html += text.substr(0, i) + '<' + tag + '>' + bb2html(text.substring(i + match.length + 2, endtag.value), true) + '</' + tag + '>';
				text = text.substr(endtag.value + tag.length + 3);
			} else {
				html += text.substr(0, i) + '<' + tag + '>';
				text = text.substr(i + match.length + 2);
				while (endtag.value--) append = '</' + tag + '>' + append;
			}
		}
	}
	return (html + text + append).replace(/(<p>|<br>)+$/, '');
}

function html2bb(text, rec) {
	if (!rec) text = text.replace(/<!--(.|\n)*-->/g, '').replace(/^\s+|\s+$|<\/p>/g, '');
	text = text.replace(/^(<p>|<br>)+/, '');
	var bbCode = '', tagRE = /<([^>]+)>/, i,
		chars = {quot: '"', apos: "'", lt: '<', gt: '>', amp: '&'}, replacer = function(s,m){return chars[m]};
	while ((i = text.search(tagRE)) != -1) {
		var match = RegExp.$1, tag = match.split(' ')[0], endtag = locateClosingTag(text, i, tag, 1).value;
		switch (tag) {
		case 'blockquote':
			if (text.substr(i + 12).match(/^<p><a.*#posts-(\d+).*wrote<\/a>:<p>/)) {
				bbCode += text.substr(0, i) + '[quote' + ('=#' + RegExp.$1) + ']' + html2bb(text.substring(text.indexOf('<p>', i + 20), endtag), true) + '[/quote]\n';
			} else {
				bbCode += text.substr(0, i) + '[quote]' + html2bb(text.substring(i + match.length + 2, endtag), true) + '[/quote]';
			}
			text = text.substr(endtag + 13);
			break;
		case 'pre':
			var content = text.substring(i + tag.length + 2, endtag);
			bbCode += text.substr(0, i) + (text.substr(0, i).match(/\n\s*$/) ? '' : '\n') + '[code]' + html2bb(content, true) + '[/code]\n';
			text = text.substr(endtag + 6);
			break;
		case 'br':
			var pref = text.charAt(i - 1) == '\n', post = text.charAt(i + 4) == '\n';
			bbCode += text.substr(0, pref ? i - 1 : i) + '\n';
			text = text.substr(i + (post ? 5 : 4));
			break;
		case 'p':
			var pref = text.substr(0, i).match(/\n*$/)[0].length, post = text.substr(i + 3).match(/^\n*/)[0].length;
			bbCode += text.substr(0, i - pref) + '\n\n';
			text = text.substr(i + 3 + post);
			break;
		case 'a':
			var linktext = text.substring(i + match.length + 2, endtag), href = match.match(/href="([^"]+)"/)[1];
			if (href == linktext || href.substr(0, linktext.length - 3) + '...' == linktext) bbCode += text.substr(0, i) + href;
			else bbCode += text.substr(0, i) + '[url=' + href + ']' + html2bb(linktext, true) + '[/url]';
			text = text.substr(endtag + 4);
			break;
		default:
			var content = text.substring(i + tag.length + 2, endtag);
			bbCode += text.substr(0, i) + (content && '[' + tag + ']' + html2bb(content, true) + '[/' + tag + ']');
			text = text.substr(endtag + tag.length + 3);
		}
	}
	return rec ? bbCode + text : (bbCode + text).replace(/&(quot|apos|lt|gt|amp);/g, replacer);
}

function locateClosingTag(str, start, tag, mode) { // mode: 0 = bbCode, 1 = html
	var openTag = (mode ? '<' : '[') + tag, closeTag = (mode ? '</' : '[/') + tag + (mode ? '>' : ']'), level = 0, status = {success: true, value: ''};
	start += openTag.length;
	while (true) {
		var oi = str.indexOf(openTag, start), ci = str.indexOf(closeTag, start);
		if (ci == -1) {
			status.success = false;
			status.value = level;
			return status;
		}
		if (oi != -1 && oi < ci) { level++; start = oi + openTag.length; }
		else if (level) { level--; start = ci + closeTag.length; }
		else {
			status.value = ci;
			return status;
		}
	}
}

function inputHandler(e) {
	if (e.keyCode == 27) {
		if (this.id == 'GM_post_body')
			document.getElementById('reply').style.display = 'none';
		else
			location.assign('javascript:EditForm.cancel();');
		e.preventDefault();
		return;
	}
	var key = String.fromCharCode(e.charCode), cv = this.value, cs = this.selectionStart, ce = this.selectionEnd;
	if (e.ctrlKey) {
		if (key in bbTags) {
			wrapText(this, bbTags[key]);
			e.preventDefault();
		}
		else if (key == 'w') {
			var sel = cv.substring(cs, ce);
			if (sel && sel.match(/^(http|ftp):\/\//)) {
				wrapText(this, '[url=', '][/url]');
				this.selectionStart = this.selectionEnd = this.selectionEnd - 6;
			}
			else wrapText(this, '[url=]', '[/url]');
			e.preventDefault();
		}
	}
	else if (e.keyCode == 9) {
		e.preventDefault();
		wrapText(this, '	', '');
	}
}

function wrapText(field, p1, p2, noInclude) {
	if (typeof p1 == 'object') p2 = p1[1], p1 = p1[0];
	var cv = field.value, cs = field.selectionStart, ce = field.selectionEnd, ct = field.scrollTop,
		insertStr = p1 + cv.substring(cs, ce) + p2, paramPos = p1.indexOf('=]') + 1;
	field.value = cv.substr(0, cs) + insertStr + cv.substr(ce);
	field.selectionStart = field.selectionEnd = cs + (paramPos || (cs == ce ? p1.length : insertStr.length));
	field.scrollTop = ct;
}

function alternateInput(inputArea) {
	inputArea.style.display = 'none';
	var bbArea = document.createElement('textarea');
	bbArea.setAttribute('rows', '8');
	bbArea.id = 'GM_' + inputArea.id;
	bbArea.value = html2bb(inputArea.value);
	inputArea.parentNode.insertBefore(bbArea, inputArea);
	bbArea.addEventListener('keypress', inputHandler, false);
	inputArea.wrappedJSObject.focus = function() { bbArea.focus(); };
	inputArea.form.elements.namedItem('commit').addEventListener('click', function(e) {
		inputArea.value = bb2html(bbArea.value);
//		e.preventDefault();
//		GM_openInTab('data:text/html,' + encodeURI(inputArea.value));
	}, false);
}

//replyarea.parentNode.parentNode.cells[1].innerHTML = ...


// quote links
function quote(e) {
	e.preventDefault();
	var area = document.getElementById('GM_edit_post_body'), edit = document.getElementById('edit'), sel = window.getSelection();
	if (!area || (edit.style.display == 'none')) {
		if (reply.style.display == 'none') location.assign('javascript:ReplyForm.init();');
		area = document.getElementById('GM_post_body');
	}
	var td = this.parentNode.parentNode, name = document.evaluate('span[@class="fn"]/a', td, null, 8, null).singleNodeValue, post = td.parentNode.cells[1];
	if (isChild(sel.anchorNode, post) && isChild(sel.focusNode, post) && sel.toString()) post = sel.toString().replace(/</g, '&lt;').replace(/>/g, '&gt;');
	else post = html2bb(post.innerHTML);
	wrapText(area, '[quote=#' + this.hash.match(/\d+/) + ']' + post + '[/quote]', '');
}

if (location.pathname.match(/\/new$/)) {
	alternateInput(document.getElementById('topic_body'));
	with (document.getElementById('GM_topic_body'))
		rows = 12, cols = 40, style.width = '99%';
} else {
	var posts = document.getElementById('content').getElementsByTagName('table')[0].rows,
		replyarea = document.getElementById('post_body'), reply = document.getElementById('reply');

	for (var i = 0; i < posts.length; i += 2) {
		with (posts[i].cells[0].appendChild(document.createElement('p'))) {
			className = 'edit';
			innerHTML = '<a href="#posts-' + posts[i].id.match(/\d+/) + '" class="utility">Quote</a>';
			firstChild.addEventListener('click', quote, false);
		}
	}

	alternateInput(replyarea);

	document.getElementById('content').addEventListener('DOMNodeInserted', function(e) {
		if (e.originalTarget.id == 'edit') alternateInput(document.getElementById('edit_post_body'));
	}, false);
}

function isChild(c, p) {
	do if (c == p) return true; while (c = c.parentNode);
	return false;
}