Greasy Fork 还支持 简体中文。

Xkcd Forums Tables

Adds bbcode tables to the xkcd forums

  1. // ==UserScript==
  2. // @name Xkcd Forums Tables
  3. // @version 1.0.1
  4. // @description Adds bbcode tables to the xkcd forums
  5. // @author faubiguy
  6. // @match http://forums.xkcd.com/*
  7. // @match http://fora.xkcd.com/*
  8. // @match http://forums2.xkcd.com/*
  9. // @match http://echochamber.me/*
  10. // @namespace FaubiScripts
  11. // @grant none
  12. // ==/UserScript==
  13.  
  14. tableVersion = '1'
  15.  
  16. debug_on = false;
  17.  
  18. function debug(str){
  19. if (debug_on){
  20. console.log(str);
  21. }
  22. }
  23.  
  24. //parseCSV attribution: https://stackoverflow.com/a/14991797/3893398
  25. function parseCSV(str) {
  26. var arr = [];
  27. var quote = false; // true means we're inside a quoted field
  28.  
  29. // iterate over each character, keep track of current row and column (of the returned array)
  30. var row, col, c;
  31. for (row = col = c = 0; c < str.length; c++) {
  32. var cc = str[c], nc = str[c+1]; // current character, next character
  33. arr[row] = arr[row] || []; // create a new row if necessary
  34. arr[row][col] = arr[row][col] || ''; // create a new column (start with empty string) if necessary
  35.  
  36. // If the current character is a quotation mark, and we're inside a
  37. // quoted field, and the next character is also a quotation mark,
  38. // add a quotation mark to the current column and skip the next character
  39. if (cc == '"' && quote && nc == '"') { arr[row][col] += cc; ++c; continue; }
  40.  
  41. // If it's just one quotation mark, begin/end quoted field
  42. if (cc == '"') { quote = !quote; continue; }
  43.  
  44. // If it's a comma and we're not in a quoted field, move on to the next column
  45. if (cc == ',' && !quote) { ++col; continue; }
  46.  
  47. // If it's a newline and we're not in a quoted field, move on to the next
  48. // row and move to column 0 of that new row
  49. if (cc == '\n' && !quote) { ++row; col = 0; continue; }
  50.  
  51. // Otherwise, append the current character to the current column
  52. arr[row][col] += cc;
  53. }
  54. return arr;
  55. }
  56.  
  57. function arrayToCSV(arr) {
  58. var lines = [];
  59. for (var i = 0; i < arr.length; i++){
  60. row = arr[i];
  61. line = [];
  62. for (var j = 0; j < row.length; j++){
  63. cell = row[j];
  64. if (cell.indexOf(',') != -1 || cell.indexOf('\n') != -1 || cell.indexOf('"') != -1){ //comma or newline or quote in cell
  65. cell = '"' + cell.replace('"', '""') + '"';
  66. }
  67. line[j] = cell;
  68. }
  69. lines[i] = line.join(',');
  70. }
  71. return lines.join('\n');
  72. }
  73.  
  74. //attribution: http://stackoverflow.com/a/13419367/3893398
  75. function parseQueryString(qstr)
  76. {
  77. var query = {};
  78. var a = qstr.split('&');
  79. for (var i = 0; i < a.length; i++)
  80. {
  81. var b = a[i].split('=');
  82. var value = decodeURIComponent(b[1])
  83. if (value == 'true') {
  84. value = true
  85. } else if (value == 'false') {
  86. value = false
  87. }
  88. query[decodeURIComponent(b[0])] = value;
  89. }
  90.  
  91. return query;
  92. }
  93.  
  94. //attribution: http://stackoverflow.com/a/15096979/3893398
  95. function objectToQueryString(obj) {
  96. var str = [];
  97. for(var p in obj){
  98. if (obj.hasOwnProperty(p)) {
  99. str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
  100. }
  101. }
  102. return str.join("&");
  103. }
  104.  
  105. var defaultOptions = {'header': true};
  106. var defaultKeys = Object.keys(defaultOptions);
  107.  
  108. function tableToOutputBBCode(table) {
  109. debug('toOutput: ' + JSON.stringify(table));
  110. var rows = table.array.length;
  111. var cols = table.array.reduce(function(b,c){return Math.max(b, c.length);}, 0);
  112. var widths = [];
  113. for (var col = 0; col < cols; col++){
  114. var width = 0;
  115. for (var row = 0; row < rows; row++){
  116. width = Math.max(width, (table.array[row][col] || '').length);
  117. }
  118. widths[col] = width;
  119. }
  120. var lines = [];
  121. for (var row = 0; row < rows; row++){
  122. var line = [];
  123. for (var col = 0; col < cols; col++){
  124. var value = table.array[row][col] || '';
  125. line[col] = value + ' '.repeat(widths[col]-value.length);
  126. }
  127. lines[row] = line.join(' ');
  128. }
  129. if (getOption(table.options,'header')){
  130. lines = lines.slice(0,1).concat([''],lines.slice(1));
  131. }
  132. table.options.widths = widths;
  133. 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]';
  134. debug(result);
  135. return result;
  136. }
  137.  
  138. function getOption(options, option){
  139. var result = options[option];
  140. if (result === undefined){
  141. result = defaultOptions[option];
  142. }
  143. return result;
  144. }
  145.  
  146. function tableToInputBBCode(table){
  147. debug('toInput: ' + JSON.stringify(table));
  148. var bbcode = '[' + (table.csv ? 'csvtable' : 'table') + (Object.getOwnPropertyNames(table.options).length > 0 ? '=' + JSON.stringify(table.options) : '') + ']';
  149. if (table.csv) {
  150. bbcode += arrayToCSV(table.array);
  151. } else {
  152. for (var i = 0; i < table.array.length; i++){
  153. var row = table.array[i];
  154. bbcode += '[tr]';
  155. for (var j = 0; j < row.length; j++){
  156. bbcode += '[td]' + row[j].replace(/\[\/tr]/g, '[\\tr]').replace(/\[\/td]/g, '[\\td]') + '[/td]';
  157. }
  158. bbcode += '[/tr]';
  159. }
  160. }
  161. bbcode += '[/' + (table.csv ? 'csvtable' : 'table') + ']';
  162. return bbcode;
  163. }
  164.  
  165. inputBBCodeRegex = /\[(table|csvtable)(?:=(.*?))?]([\s\S]*?)\[\/\1]/g; //groups: type, options, contents
  166. outputBBCodeRegex = /\[url=http:\/\/faubi\/(table|csvtable)(?:.v([\0-9]+))?\/(.*?)]\[s]\[\/s]\[\/url]\[code]([\s\S]*?)\[\/code]/g; //groups: type, version, options, contents
  167. hrefRegex = /http:\/\/faubi\/(table|csvtable)(?:.v([\0-9]+))?\/(.*)/; //groups: type, version, options
  168. tableRowRegex = /\s*\[tr]([\s\S]*?)\[\/tr]\s*/g; //groups: cells
  169. tableCellRegex = /\s*\[td]([\s\S]*?)\[\/td]\s*/g; //groups: cells
  170. codeRegex = /\[code].*?\[\/code]/g; //groups: cells
  171.  
  172. function outputBBCodeToTable(match){
  173. debug('outputBBCodeToTable: ' + [match[1], match[2], match[3], match[4]].join(', '))
  174. return toTable(match[1], getOptionsByVersion(match[3], match[2]), match[4], 'output');
  175. }
  176.  
  177. function inputBBCodeToTable(match){
  178. debug('inputBBCodeToTable: ' + [match[1], match[2], match[3]].join(', '))
  179. return toTable(match[1], getOptionsFromJSON(match[2]), match[3], 'input');
  180. }
  181.  
  182. function toTable(type, options, contents, mode){
  183. debug('toTable: '+JSON.stringify({'type':type,'options':options,'contents':contents,'mode':mode}));
  184. var table = {};
  185. table.csv = type == 'csvtable';
  186. table.options = options
  187. if (mode=='input' && table.csv){
  188. if (contents[0] === '\n') {
  189. contents = contents.substr(1);
  190. }
  191. table.array = parseCSV(contents);
  192. } else if (mode=='input'){
  193. table.array = [];
  194. var rowMatch = tableRowRegex.exec(contents);
  195. while(rowMatch) {
  196. row = [];
  197. var cellMatch = tableCellRegex.exec(rowMatch[1]);
  198. while(cellMatch){
  199. row.push(cellMatch[1]);
  200. cellMatch = tableCellRegex.exec(rowMatch[1]);
  201. }
  202. table.array.push(row);
  203. rowMatch = tableRowRegex.exec(contents);
  204. }
  205. } else { //mode=='output'
  206. table.array = textTableToArray(contents, table.options);
  207. }
  208. delete table.options.widths;
  209. return table;
  210. }
  211.  
  212. function textTableToArray(text, options) {
  213. if (!options){
  214. return [['Broken Table!']];
  215. }
  216. lines = text.split('\n');
  217. array = [];
  218. for (var row = 0; row < lines.length; row++){
  219. if (row == 1 && getOption(options,'header')){
  220. continue;
  221. }
  222. var rowArray = [];
  223. var startpos = 0;
  224. for (var col = 0; col < options.widths.length; col++){
  225. rowArray[col] = lines[row].substr(startpos, options.widths[col]).trim();
  226. startpos += options.widths[col] + 2;
  227. }
  228. if (rowArray.length === 0){
  229. rowArray.push('');
  230. }
  231. array.push(rowArray);
  232. }
  233. return array;
  234. }
  235.  
  236. function getOptionsFromJSON(jsonString) {
  237. try {
  238. var options = JSON.parse(unescape(jsonString));
  239. if (typeof(options) == 'object') {
  240. return options;
  241. } else {
  242. return {};
  243. }
  244. } catch (e) {
  245. return {};
  246. }
  247. }
  248.  
  249. function getOptionsByVersion(optionString, version) {
  250. if (typeof(version) == 'string') {
  251. version = parseInt(version)
  252. }
  253. debug('getOptionsByVersion: ' + optionString + ', ' + version)
  254. switch (version) {
  255. case 0:
  256. return getOptionsFromJSON(optionString);
  257. break;
  258. case 1:
  259. var options = parseQueryString(optionString.substr(1));
  260. if (options.widths) {
  261. options.widths = options.widths.split(',').map(function(n){return parseInt(n,10)})
  262. }
  263. return options
  264. break;
  265. default:
  266. return {};
  267. }
  268. }
  269.  
  270. function replaceTable(string, regex, func, nmfunc){
  271. func = func || function(x){return x}
  272. nmfunc = nmfunc || function(x){return x}
  273. var sections = [];
  274. var lastEnd = 0;
  275. var tableMatch = regex.exec(string);
  276. while (tableMatch){
  277. sections.push(nmfunc(string.substring(lastEnd, tableMatch.index)));
  278. sections.push(func(tableMatch));
  279. lastEnd = tableMatch.index + tableMatch[0].length;
  280. tableMatch = regex.exec(string);
  281. }
  282. return sections.join('') + nmfunc(string.substring(lastEnd));
  283. }
  284.  
  285. if (window.location.pathname.indexOf('posting.php') != -1){ //On posting page
  286. var messagebox = document.getElementById('message');
  287. messagebox.value = replaceTable(messagebox.value, outputBBCodeRegex, function(x){return tableToInputBBCode(outputBBCodeToTable(x));});
  288. 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));});});});
  289. }
  290.  
  291. links = document.getElementsByTagName('a');
  292. linksList = [];
  293. for (var i = 0; i < links.length; i++){
  294. if(links[i].href.startsWith('http://faubi/')){
  295. linksList.push(links[i]);
  296. }
  297. }
  298. debug('# Links: ' + linksList.length);
  299. for (var i = 0; i < linksList.length; i++){
  300. var link = linksList[i];
  301. debug('Handling link: ' + link.href);
  302. var codebox = link.nextSibling;
  303. if (!(codebox && codebox.tagName == 'DL')){
  304. continue;
  305. }
  306. var text = codebox.children[1].firstChild.innerHTML.replace(/&nbsp;/g, ' ').replace(/<br>/g, '\n');
  307. var hrefMatch = hrefRegex.exec(link.href);
  308. if (!hrefMatch){
  309. continue;
  310. }
  311. var version = hrefMatch[2] ? parseInt(hrefMatch[2]) : 0;
  312. var options = getOptionsByVersion(hrefMatch[3], version)
  313. var table = toTable(hrefMatch[1], options, text, 'output');
  314. var htmlTable = document.createElement('table');
  315. htmlTable.classList.add('faubi-table');
  316. for (var rowNum = 0; rowNum < table.array.length; rowNum++){
  317. var row = table.array[rowNum];
  318. var tr = document.createElement('tr');
  319. for (var j = 0; j < row.length; j++){
  320. var td = document.createElement(rowNum === 0 && getOption(table.options, 'header') ? 'th' : 'td');
  321. var cellText = row[j];
  322. if (cellText === ''){
  323. cellText = '\u00A0';
  324. }
  325. td.textContent = cellText;
  326. tr.appendChild(td);
  327. }
  328. htmlTable.appendChild(tr);
  329. }
  330. link.parentNode.insertBefore(htmlTable, link);
  331. codebox.style.display = 'none';
  332. link.style.display = 'none';
  333. }
  334. var style = document.createElement('style');
  335. 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%;}';
  336. document.head.appendChild(style);