// ==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(/ /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);