Greasy Fork is available in English.

Xkcd Forums Tables

Adds bbcode tables to the xkcd forums

// ==UserScript==
// @name         Xkcd Forums Tables
// @version      1.0.1
// @description  Adds bbcode tables to the xkcd forums
// @author       faubiguy
// @match        http://forums.xkcd.com/*
// @match        http://fora.xkcd.com/*
// @match        http://forums2.xkcd.com/*
// @match        http://echochamber.me/*
// @namespace    FaubiScripts
// @grant        none
// ==/UserScript==

tableVersion = '1'

debug_on = false;

function debug(str){
    if (debug_on){
        console.log(str);
    }
}

//parseCSV attribution: https://stackoverflow.com/a/14991797/3893398
function parseCSV(str) {
    var arr = [];
    var quote = false;  // true means we're inside a quoted field

    // iterate over each character, keep track of current row and column (of the returned array)
    var row, col, c;
    for (row = col = c = 0; c < str.length; c++) {
        var cc = str[c], nc = str[c+1];        // current character, next character
        arr[row] = arr[row] || [];             // create a new row if necessary
        arr[row][col] = arr[row][col] || '';   // create a new column (start with empty string) if necessary

        // If the current character is a quotation mark, and we're inside a
        // quoted field, and the next character is also a quotation mark,
        // add a quotation mark to the current column and skip the next character
        if (cc == '"' && quote && nc == '"') { arr[row][col] += cc; ++c; continue; }  

        // If it's just one quotation mark, begin/end quoted field
        if (cc == '"') { quote = !quote; continue; }

        // If it's a comma and we're not in a quoted field, move on to the next column
        if (cc == ',' && !quote) { ++col; continue; }

        // If it's a newline and we're not in a quoted field, move on to the next
        // row and move to column 0 of that new row
        if (cc == '\n' && !quote) { ++row; col = 0; continue; }

        // Otherwise, append the current character to the current column
        arr[row][col] += cc;
    }
    return arr;
}

function arrayToCSV(arr) {
    var lines = [];
    for (var i = 0; i < arr.length; i++){
        row = arr[i];
        line = [];
        for (var j = 0; j < row.length; j++){
            cell = row[j];
            if (cell.indexOf(',') != -1 || cell.indexOf('\n') != -1 || cell.indexOf('"') != -1){ //comma or newline or quote in cell
                cell = '"' + cell.replace('"', '""') + '"';
            }
            line[j] = cell;
        }
        lines[i] = line.join(',');
    }
    return lines.join('\n');
}

//attribution: http://stackoverflow.com/a/13419367/3893398
function parseQueryString(qstr)
{
  var query = {};
  var a = qstr.split('&');
  for (var i = 0; i < a.length; i++)
  {
    var b = a[i].split('=');
	var value = decodeURIComponent(b[1])
	if (value == 'true') {
		value = true
	} else if (value == 'false') {
		value = false
	}
    query[decodeURIComponent(b[0])] = value;
  }

  return query;
}

//attribution: http://stackoverflow.com/a/15096979/3893398
function objectToQueryString(obj) {
   var str = [];
   for(var p in obj){
       if (obj.hasOwnProperty(p)) {
           str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
       }
   }
   return str.join("&");
}

var defaultOptions = {'header': true};
var defaultKeys = Object.keys(defaultOptions);

function tableToOutputBBCode(table) {
    debug('toOutput: ' + JSON.stringify(table));
    var rows = table.array.length;
    var cols = table.array.reduce(function(b,c){return Math.max(b, c.length);}, 0);
    var widths = [];
    for (var col = 0; col < cols; col++){
        var width = 0;
        for (var row = 0; row < rows; row++){
            width = Math.max(width, (table.array[row][col] || '').length);
        }
        widths[col] = width;
    }
    var lines = [];
    for (var row = 0; row < rows; row++){
        var line = [];
        for (var col = 0; col < cols; col++){
            var value = table.array[row][col] || '';
            line[col] = value + ' '.repeat(widths[col]-value.length);
        }
        lines[row] = line.join('  ');
    }
    if (getOption(table.options,'header')){
        lines = lines.slice(0,1).concat([''],lines.slice(1));
    }
    table.options.widths = widths;
    var result = '[url=http://faubi/' + (table.csv ? 'csvtable' : 'table') + '.v' + tableVersion + '/?' + objectToQueryString(table.options) + '][s][/s][/url][code]' +  lines.join('\n').replace(/\[\/code]/g,'[\\code]') + '[/code]';
    debug(result);
    return result;
}

function getOption(options, option){
    var result = options[option];
    if (result === undefined){
        result = defaultOptions[option];
    }
    return result;
}

function tableToInputBBCode(table){
    debug('toInput: ' + JSON.stringify(table));
    var bbcode =  '[' + (table.csv ? 'csvtable' : 'table') + (Object.getOwnPropertyNames(table.options).length > 0 ? '=' + JSON.stringify(table.options) : '') + ']';
    if (table.csv) {
        bbcode += arrayToCSV(table.array);
    } else {
        for (var i = 0; i < table.array.length; i++){
            var row = table.array[i];
            bbcode += '[tr]';
            for (var j = 0; j < row.length; j++){
                bbcode += '[td]' + row[j].replace(/\[\/tr]/g, '[\\tr]').replace(/\[\/td]/g, '[\\td]') + '[/td]';
            }
            bbcode += '[/tr]';
        }
    }
    bbcode += '[/' + (table.csv ? 'csvtable' : 'table') + ']';
    return bbcode;
}

inputBBCodeRegex = /\[(table|csvtable)(?:=(.*?))?]([\s\S]*?)\[\/\1]/g; //groups: type, options, contents
outputBBCodeRegex = /\[url=http:\/\/faubi\/(table|csvtable)(?:.v([\0-9]+))?\/(.*?)]\[s]\[\/s]\[\/url]\[code]([\s\S]*?)\[\/code]/g; //groups: type, version, options, contents
hrefRegex = /http:\/\/faubi\/(table|csvtable)(?:.v([\0-9]+))?\/(.*)/; //groups: type, version, options
tableRowRegex = /\s*\[tr]([\s\S]*?)\[\/tr]\s*/g; //groups: cells
tableCellRegex = /\s*\[td]([\s\S]*?)\[\/td]\s*/g; //groups: cells
codeRegex = /\[code].*?\[\/code]/g; //groups: cells

function outputBBCodeToTable(match){
	debug('outputBBCodeToTable: ' + [match[1], match[2], match[3], match[4]].join(', '))
    return toTable(match[1], getOptionsByVersion(match[3], match[2]), match[4], 'output');
}

function inputBBCodeToTable(match){
	debug('inputBBCodeToTable: ' + [match[1], match[2], match[3]].join(', '))
    return toTable(match[1], getOptionsFromJSON(match[2]), match[3], 'input');
}

function toTable(type, options, contents, mode){
    debug('toTable: '+JSON.stringify({'type':type,'options':options,'contents':contents,'mode':mode}));
    var table = {};
    table.csv = type == 'csvtable';
	table.options = options
    if (mode=='input' && table.csv){
        if (contents[0] === '\n') {
            contents = contents.substr(1);
        }
        table.array = parseCSV(contents);
    } else if (mode=='input'){
        table.array = [];
        var rowMatch = tableRowRegex.exec(contents);
        while(rowMatch) {
            row = [];
            var cellMatch = tableCellRegex.exec(rowMatch[1]);
            while(cellMatch){
                row.push(cellMatch[1]);
                cellMatch = tableCellRegex.exec(rowMatch[1]);
            }
            table.array.push(row);
            rowMatch = tableRowRegex.exec(contents);
        }
    } else { //mode=='output'
        table.array = textTableToArray(contents, table.options);
    }
    delete table.options.widths;
    return table;
}

function textTableToArray(text, options) {
    if (!options){
        return [['Broken Table!']];
    }
    lines = text.split('\n');
    array = [];
    for (var row = 0; row < lines.length; row++){
        if (row == 1 && getOption(options,'header')){
            continue;
        }
        var rowArray = [];
        var startpos = 0;
        for (var col = 0; col < options.widths.length; col++){
            rowArray[col] = lines[row].substr(startpos, options.widths[col]).trim();
            startpos += options.widths[col] + 2;
        }
        if (rowArray.length === 0){
            rowArray.push('');
        }
        array.push(rowArray);
    }
    return array;
}

function getOptionsFromJSON(jsonString) {
	try {
		var options = JSON.parse(unescape(jsonString));
		if (typeof(options) == 'object') {
			return options;
		} else {
			return {};
		}
	} catch (e) {
		return {};
	}
}

function getOptionsByVersion(optionString, version) {
	if (typeof(version) == 'string') {
		version = parseInt(version)
	}
	debug('getOptionsByVersion: ' + optionString + ', ' + version)
    switch (version) {
		case 0:
			return getOptionsFromJSON(optionString);
			break;
		case 1:
			var options = parseQueryString(optionString.substr(1));
			if (options.widths) {
				options.widths = options.widths.split(',').map(function(n){return parseInt(n,10)})
			}
			return options
			break;
		default:
			return {};
	}
}
	

function replaceTable(string, regex, func, nmfunc){
    func = func || function(x){return x}
    nmfunc = nmfunc || function(x){return x}
    var sections = [];
    var lastEnd = 0;
    var tableMatch = regex.exec(string);
    while (tableMatch){
        sections.push(nmfunc(string.substring(lastEnd, tableMatch.index)));
        sections.push(func(tableMatch));
        lastEnd = tableMatch.index + tableMatch[0].length;
        tableMatch = regex.exec(string);
    }
    return sections.join('') + nmfunc(string.substring(lastEnd));
}

if (window.location.pathname.indexOf('posting.php') != -1){ //On posting page
    var messagebox = document.getElementById('message');
    messagebox.value = replaceTable(messagebox.value, outputBBCodeRegex, function(x){return tableToInputBBCode(outputBBCodeToTable(x));});
    document.getElementById('postform').addEventListener('submit', function(){messagebox.value = replaceTable(messagebox.value, codeRegex, null, function(noncode){return replaceTable(noncode, inputBBCodeRegex, function(x){return tableToOutputBBCode(inputBBCodeToTable(x));});});});
}

links = document.getElementsByTagName('a');
linksList = [];
for (var i = 0; i < links.length; i++){
    if(links[i].href.startsWith('http://faubi/')){
        linksList.push(links[i]);
    }
}
debug('# Links: ' + linksList.length);
for (var i = 0; i < linksList.length; i++){
    var link = linksList[i];
    debug('Handling link: ' + link.href);
    var codebox = link.nextSibling;
    if (!(codebox && codebox.tagName == 'DL')){
        continue;
    }
    var text = codebox.children[1].firstChild.innerHTML.replace(/&nbsp;/g, ' ').replace(/<br>/g, '\n');
    var hrefMatch = hrefRegex.exec(link.href);
    if (!hrefMatch){
        continue;
    }
    var version = hrefMatch[2] ? parseInt(hrefMatch[2]) : 0;
    var options = getOptionsByVersion(hrefMatch[3], version)
    var table = toTable(hrefMatch[1], options, text, 'output');
    var htmlTable = document.createElement('table');
    htmlTable.classList.add('faubi-table');
    for (var rowNum = 0; rowNum < table.array.length; rowNum++){
        var row = table.array[rowNum];
        var tr = document.createElement('tr');
        for (var j = 0; j < row.length; j++){
            var td = document.createElement(rowNum === 0 && getOption(table.options, 'header') ? 'th' : 'td');
            var cellText = row[j];
            if (cellText === ''){
                cellText = '\u00A0';
            }
            td.textContent = cellText;
            tr.appendChild(td);
        }
        htmlTable.appendChild(tr);
    }
    link.parentNode.insertBefore(htmlTable, link);
    codebox.style.display = 'none';
    link.style.display = 'none';
}
var style = document.createElement('style');
style.textContent = '.faubi-table{border: 1px solid gray; border-collapse: collapse} .faubi-table td, .faubi-table th{border: 1px solid gray; padding-left: 2px; padding-right: 2px; height: 100%;}';
document.head.appendChild(style);