diff --git a/4chan_x.user.js b/4chan_x.user.js index 683f0df3a..159756ddc 100644 --- a/4chan_x.user.js +++ b/4chan_x.user.js @@ -2994,7 +2994,7 @@ Get = { post: function(board, threadID, postID, root, cb) { - var post; + var post, url; if (board === g.BOARD && (post = $.id("pc" + postID))) { $.add(root, Get.cleanPost(post.cloneNode(true))); return; @@ -3004,19 +3004,35 @@ return $.cache("/" + board + "/res/" + threadID, function() { return Get.parsePost(this, board, threadID, postID, root, cb); }); + } else if (url = Redirect.post(board, postID)) { + return $.cache(url, function() { + return Get.parseArchivedPost(this, board, postID, root, cb); + }); } }, parsePost: function(req, board, threadID, postID, root, cb) { - var doc, href, link, pc, quote, status, _i, _len, _ref; + var doc, href, link, pc, quote, status, url, _i, _len, _ref; status = req.status; if (status !== 200) { - root.textContent = status === 404 ? "Thread No." + threadID + " has not been found." : "Error " + req.status + ": " + req.statusText + "."; + if (url = Redirect.post(board, postID)) { + $.cache(url, function() { + return Get.parseArchivedPost(this, board, postID, root, cb); + }); + } else { + root.textContent = status === 404 ? "Thread No." + threadID + " has not been found." : "Error " + req.status + ": " + req.statusText + "."; + } return; } doc = d.implementation.createHTMLDocument(''); doc.documentElement.innerHTML = req.response; if (!(pc = doc.getElementById("pc" + postID))) { - root.textContent = "Post No." + postID + " has not been found."; + if (url = Redirect.post(board, postID)) { + $.cache(url, function() { + return Get.parseArchivedPost(this, board, postID, root, cb); + }); + } else { + root.textContent = "Post No." + postID + " has not been found."; + } return; } pc = Get.cleanPost(d.importNode(pc, true)); @@ -3037,7 +3053,158 @@ return cb(); } }, - parseArchivedPost: function(req, board, postID, root, cb) {}, + parseArchivedPost: function(req, board, postID, root, cb) { + var bq, data, date, email, file, filesize, isOP, nameBlock, p, pc, pi, piM, span, time, unit; + data = JSON.parse(req.response); + if (data.error) { + root.textContent = data.error; + return; + } + isOP = postID === data.thread_num; + pc = $.el('div', { + id: "pc", + className: isOP ? 'postContainer opContainer' : 'postContainer replyContainer' + }); + p = $.el('div', { + id: "p" + postID, + className: isOP ? 'post op' : 'post reply' + }); + $.add(pc, p); + piM = $.el('div', { + id: "pim" + postID, + className: 'postInfoM mobile', + innerHTML: '' + }); + pi = $.el('div', { + id: "pi" + postID, + className: 'postInfo desktop', + innerHTML: " No." + postID + "" + (isOP ? '   ' : '') + " " + }); + time = $('.dateTime', pi); + date = new Date(data.timestamp * 1000); + time.textContent = date.toString(); + $('.subject', pi).textContent = data.title; + nameBlock = $('.nameBlock', pi); + if (data.email) { + email = $.el('a', { + className: 'useremail', + href: "mailto:" + data.email + }); + $.add(nameBlock, email); + nameBlock = email; + } + $.add(nameBlock, $.el('span', { + className: 'name', + textContent: data.name + })); + if (data.trip) { + $.add(nameBlock, [ + $.tn(' '), $.el('span', { + className: 'postertrip', + textContent: data.trip + }) + ]); + } + if (data.capcode === 'A') { + $.add(nameBlock, [ + $.tn(' '), $.el('strong', { + className: 'capcode capcodeAdmin', + textContent: '## Admin' + }) + ]); + nameBlock = $('.nameBlock', pi); + $.addClass(nameBlock, 'capcodeAdmin'); + $.add(nameBlock, [ + $.tn(' '), $.el('img'), { + src: '//static.4chan.org/image/adminicon.gif', + alt: 'This user is the 4chan Administrator.', + title: 'This user is the 4chan Administrator.', + className: 'identityIcon' + } + ]); + } else if (data.capcode === 'M') { + $.add(nameBlock, [ + $.tn(' '), $.el('strong', { + className: 'capcode', + textContent: '## Mod' + }) + ]); + nameBlock = $('.nameBlock', pi); + $.addClass(nameBlock, 'capcodeMod'); + $.add(nameBlock, [ + $.tn(' '), $.el('img'), { + src: '//static.4chan.org/image/adminicon.gif', + alt: 'This user is a 4chan Moderator.', + title: 'This user is a 4chan Moderator.', + className: 'identityIcon' + } + ]); + } + bq = $.el('blockquote', { + id: "m" + postID, + className: 'postMessage', + textContent: data.comment + }); + bq.innerHTML = bq.innerHTML.replace(/\n|\[\/?b\]|\[\/?spoiler\]|\[\/?code\]|\[\/?moot\]|\[\/?banned\]/g, function(text) { + switch (text) { + case '\n': + return '
'; + case '[b]': + return ''; + case '[/b]': + return ''; + case '[spoiler]': + return ''; + case '[/spoiler]': + return ''; + case '[code]': + return '
';
+          case '[/code]':
+            return '
'; + case '[moot]': + return '
'; + case '[/moot]': + return '
'; + case '[banned]': + return ''; + case '[/banned]': + return ''; + } + }); + bq.innerHTML = bq.innerHTML.replace(/(^|>)(>[^<$]+)(<|$)/g, '$1$2$3'); + $.add(p, [piM, pi, bq]); + if (data.media) { + file = $.el('div', { + id: "f" + postID, + className: 'file' + }); + filesize = data.media_size; + unit = 0; + while (filesize >= 1024) { + filesize /= 1024; + unit++; + } + filesize = unit > 1 ? (a * 100).toFixed() / 100 : filesize.toFixed(); + $.add(file, $.el('div', { + className: 'fileInfo', + innerHTML: "File: " + data.media_orig + "-(" + filesize + " " + ['B', 'KB', 'MB', 'GB'][unit] + ", " + data.media_w + "x" + data.media_h + ", )" + })); + span = $('span[title]', file); + span.title = data.media_filename; + span.textContent = data.media_filename.length < 40 ? data.media_filename : "" + data.media_filename.slice(0, 30) + "(...)" + data.media_filename.slice(-4); + $.add(file, $.el('a', { + className: 'fileThumb', + href: data.media_link || data.remote_media_link, + target: '_blank', + innerHTML: "" + (data.spoiler === " + })); + $.after((isOP ? $('.postInfoM', p) : $('.postInfo', p)), file); + } + $.replace(root.firstChild, pc); + if (cb) { + return cb(); + } + }, cleanPost: function(root) { var child, el, now, post, _i, _j, _len, _len1, _ref, _ref1; post = $('.post', root); @@ -3143,7 +3310,7 @@ _ref = post.quotes; for (_i = 0, _len = _ref.length; _i < _len; _i++) { quote = _ref[_i]; - if (!quote.hash) { + if (!(quote.hash || /\bdeadlink\b/.test(quote.className))) { continue; } quote.removeAttribute('onclick'); @@ -3161,7 +3328,7 @@ return; } e.preventDefault(); - id = this.hash.slice(2); + id = this.dataset.id || this.hash.slice(2); if (/\binlined\b/.test(this.className)) { QuoteInline.rm(this, id); } else { @@ -3173,21 +3340,30 @@ return this.classList.toggle('inlined'); }, add: function(q, id) { - var el, i, inline, isBacklink, path, root; + var board, el, i, inline, isBacklink, path, postID, root, threadID; if (!(isBacklink = /\bbacklink\b/.test(q.className))) { root = q; while (root.parentNode.nodeName !== 'BLOCKQUOTE') { root = root.parentNode; } } - path = q.pathname.split('/'); - el = path[1] === g.BOARD ? $.id("p" + id) : false; + if (q.host === 'boards.4chan.org') { + path = q.pathname.split('/'); + board = path[1]; + threadID = path[3]; + postID = id; + } else { + board = q.dataset.board; + threadID = 0; + postID = q.dataset.id; + } + el = board === g.BOARD ? $.id("p" + postID) : false; inline = $.el('div', { - id: "i" + id, + id: "i" + postID, className: el ? 'inline' : 'inline crosspost' }); $.after((isBacklink ? q.parentNode : root), inline); - Get.post(path[1], path[3], id, inline); + Get.post(board, threadID, postID, inline); if (!el) { return; } @@ -3233,7 +3409,7 @@ _ref = post.quotes; for (_i = 0, _len = _ref.length; _i < _len; _i++) { quote = _ref[_i]; - if (quote.hash) { + if (quote.hash || /\bdeadlink\b/.test(quote.className)) { $.on(quote, 'mouseover', QuotePreview.mouseover); } } @@ -3244,7 +3420,7 @@ } }, mouseover: function(e) { - var el, id, parent, path, qp, quote, quoterID, _i, _len, _ref; + var board, el, parent, path, postID, qp, quote, quoterID, threadID, _i, _len, _ref; if (/\binlined\b/.test(this.className)) { return; } @@ -3257,20 +3433,29 @@ if (UI.el) { return; } - path = this.pathname.split('/'); - id = this.hash.slice(2); + if (this.host === 'boards.4chan.org') { + path = this.pathname.split('/'); + board = path[1]; + threadID = path[3]; + postID = this.hash.slice(2); + } else { + board = this.dataset.board; + threadID = 0; + postID = this.dataset.id; + } qp = UI.el = $.el('div', { id: 'qp', className: 'reply dialog' }); UI.hover(e); $.add(d.body, qp); - Get.post(path[1], path[3], id, qp, function() { + Get.post(board, threadID, postID, qp, function() { var bq, fileInfo, img, post; bq = $('blockquote', qp); Main.prettify(bq); post = { - el: qp + el: qp, + blockquote: bq }; if (fileInfo = $('.fileInfo', qp)) { img = fileInfo.nextElementSibling.firstElementChild; @@ -3286,10 +3471,13 @@ Time.node(post); } if (Conf['File Info Formatting']) { - return FileInfo.node(post); + FileInfo.node(post); + } + if (Conf['Resurrect Quotes']) { + return Quotify.node(post); } }); - if (path[1] === g.BOARD && (el = $.id("p" + id))) { + if (board === g.BOARD && (el = $.id("p" + postID))) { if (Conf['Quote Highlighting']) { if (/\bop\b/.test(el.className)) { $.addClass(el.parentNode, 'qphl'); @@ -3403,6 +3591,11 @@ a.href = Redirect.thread(board, id, 'post'); a.className = 'deadlink'; a.target = '_blank'; + if (Redirect.post(board, id)) { + $.addClass(a, 'quotelink'); + a.dataset.board = board; + a.dataset.id = id; + } } data = data.slice(index + quote.length); } @@ -3625,6 +3818,20 @@ return "http://archive.foolz.us/" + board + "/full_image/" + filename; } }, + post: function(board, postID) { + switch (board) { + case 'a': + case 'co': + case 'jp': + case 'm': + case 'tg': + case 'tv': + case 'u': + case 'v': + case 'vg': + return "http://archive.foolz.us/api/chan/post/board/" + board + "/num/" + postID + "/format/json"; + } + }, thread: function(board, id, mode) { if (board == null) { board = g.BOARD; diff --git a/script.coffee b/script.coffee index 13e4c6632..b42b6f01f 100644 --- a/script.coffee +++ b/script.coffee @@ -2292,30 +2292,34 @@ Get = if threadID $.cache "/#{board}/res/#{threadID}", -> Get.parsePost @, board, threadID, postID, root, cb - # else if url = Redirect.??? - # $.cache url, -> - # Get.parseArchivedPost @, board, postID, root, cb + else if url = Redirect.post board, postID + $.cache url, -> + Get.parseArchivedPost @, board, postID, root, cb parsePost: (req, board, threadID, postID, root, cb) -> {status} = req if status isnt 200 - # thread can die by the time we check a post - # try archive if possible - # else - root.textContent = - if status is 404 - "Thread No.#{threadID} has not been found." - else - "Error #{req.status}: #{req.statusText}." + # The thread can die by the time we check a quote. + if url = Redirect.post board, postID + $.cache url, -> + Get.parseArchivedPost @, board, postID, root, cb + else + root.textContent = + if status is 404 + "Thread No.#{threadID} has not been found." + else + "Error #{req.status}: #{req.statusText}." return doc = d.implementation.createHTMLDocument '' doc.documentElement.innerHTML = req.response unless pc = doc.getElementById "pc#{postID}" - # post can be deleted by the time we check for it - # try archive if possible - # else - root.textContent = "Post No.#{postID} has not been found." + # The post can be deleted by the time we check a quote. + if url = Redirect.post board, postID + $.cache url, -> + Get.parseArchivedPost @, board, postID, root, cb + else + root.textContent = "Post No.#{postID} has not been found." return pc = Get.cleanPost d.importNode pc, true @@ -2330,7 +2334,169 @@ Get = $.replace root.firstChild, pc cb() if cb parseArchivedPost: (req, board, postID, root, cb) -> - # $.replace root.firstChild, + # unarchived + # >>>/a/1 + # op + # >>>/a/66734868 + # reply + # >>>/a/66718194 + # reply with image and spoiler text + # >>>/v/142341606 + + data = JSON.parse req.response + + if data.error + root.textContent = data.error + return + + isOP = postID is data.thread_num + + # post containers + pc = $.el 'div', + id: "pc#{}" + className: if isOP then 'postContainer opContainer' else 'postContainer replyContainer' + p = $.el 'div', + id: "p#{postID}" + className: if isOP then 'post op' else 'post reply' + $.add pc, p + + + # post info (mobile) + piM = $.el 'div', + id: "pim#{postID}" + className: 'postInfoM mobile' + innerHTML: '' # XXX + + + # post info + pi = $.el 'div', + id: "pi#{postID}" + className: 'postInfo desktop' + innerHTML: " No.#{postID}#{if isOP then '   ' else ''} " + # time + time = $ '.dateTime', pi + date = new Date data.timestamp * 1000 + time.textContent = date.toString() # XXX needs to be 4chan formatted + # subject + $('.subject', pi).textContent = data.title + + nameBlock = $ '.nameBlock', pi + if data.email + email = $.el 'a', + className: 'useremail' + href: "mailto:#{data.email}" + $.add nameBlock, email + nameBlock = email + $.add nameBlock, $.el 'span', + className: 'name' + textContent: data.name + if data.trip + $.add nameBlock, [$.tn(' '), $.el('span', className: 'postertrip', textContent: data.trip)] + if data.capcode is 'A' # admin + $.add nameBlock, [ + $.tn(' '), + $.el('strong', className: 'capcode capcodeAdmin', textContent: '## Admin') + ] + nameBlock = $ '.nameBlock', pi + $.addClass nameBlock, 'capcodeAdmin' + $.add nameBlock, [ + $.tn(' '), + $.el('img'), src: '//static.4chan.org/image/adminicon.gif', alt: 'This user is the 4chan Administrator.', title: 'This user is the 4chan Administrator.', className: 'identityIcon' + ] + else if data.capcode is 'M' # mod + $.add nameBlock, [ + $.tn(' '), + $.el('strong', className: 'capcode', textContent: '## Mod') + ] + nameBlock = $ '.nameBlock', pi + $.addClass nameBlock, 'capcodeMod' + $.add nameBlock, [ + $.tn(' '), + $.el('img'), src: '//static.4chan.org/image/adminicon.gif', alt: 'This user is a 4chan Moderator.', title: 'This user is a 4chan Moderator.', className: 'identityIcon' + ] + + + # comment + bq = $.el 'blockquote', + id: "m#{postID}" + className: 'postMessage' + textContent: data.comment # set this first to convert text to HTML entities + # https://github.com/eksopl/fuuka/blob/master/Board/Yotsuba.pm#L413-452 + # https://github.com/eksopl/asagi/blob/master/src/main/java/net/easymodo/asagi/Yotsuba.java#L109-138 + bq.innerHTML = bq.innerHTML.replace /// + \n + | \[/?b\] + | \[/?spoiler\] + | \[/?code\] + | \[/?moot\] + | \[/?banned\] + ///g, (text) -> + switch text + when '\n' + '
' + when '[b]' + '' + when '[/b]' + '' + when '[spoiler]' + '' + when '[/spoiler]' + '' + when '[code]' + '
'
+          when '[/code]'
+            '
' + when '[moot]' + '
' + when '[/moot]' + '
' + when '[banned]' + '' + when '[/banned]' + '' + bq.innerHTML = bq.innerHTML.replace /(^|>)(>[^<$]+)(<|$)/g, '$1$2$3' + + + $.add p, [piM, pi, bq] + + + # file + if data.media + file = $.el 'div', + id: "f#{postID}" + className: 'file' + filesize = data.media_size + unit = 0 # Bytes + while filesize >= 1024 + filesize /= 1024 + unit++ + # Keep the filesize as a float if the unit is in MBs. + # Remove trailing 0s. + filesize = + if unit > 1 + (a * 100).toFixed() / 100 + else + filesize.toFixed() + $.add file, $.el 'div', + className: 'fileInfo' + innerHTML: "File: #{data.media_orig}-(#{filesize} #{['B', 'KB', 'MB', 'GB'][unit]}, #{data.media_w}x#{data.media_h}, )" + span = $ 'span[title]', file + span.title = data.media_filename + span.textContent = + if data.media_filename.length < 40 + data.media_filename + else + "#{data.media_filename[...30]}(...)#{data.media_filename[-4...]}" + $.add file, $.el 'a', + className: 'fileThumb' + href: data.media_link or data.remote_media_link + target: '_blank' + innerHTML: "#{if data.spoiler is " + $.after (if isOP then $('.postInfoM', p) else $('.postInfo', p)), file + + + $.replace root.firstChild, pc + cb() if cb cleanPost: (root) -> post = $ '.post', root for child in Array::slice.call root.childNodes @@ -2341,8 +2507,6 @@ Get = for el in $$ '[id]', root el.id = "#{now}_#{el.id}" - # $.rmClass post, 'opContainer' - # $.rmClass post, 'replyContainer' $.rmClass root, 'forwarded' $.rmClass root, 'qphl' # op $.rmClass post, 'highlight' @@ -2404,7 +2568,7 @@ QuoteInline = Main.callbacks.push @node node: (post) -> for quote in post.quotes - continue unless quote.hash + continue unless quote.hash or /\bdeadlink\b/.test quote.className quote.removeAttribute 'onclick' $.on quote, 'click', QuoteInline.toggle for quote in post.backlinks @@ -2413,7 +2577,7 @@ QuoteInline = toggle: (e) -> return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0 e.preventDefault() - id = @hash[2..] + id = @dataset.id or @hash[2..] if /\binlined\b/.test @className QuoteInline.rm @, id else @@ -2429,13 +2593,22 @@ QuoteInline = until root.parentNode.nodeName is 'BLOCKQUOTE' root = root.parentNode - path = q.pathname.split '/' - el = if path[1] is g.BOARD then $.id "p#{id}" else false + if q.host is 'boards.4chan.org' + path = q.pathname.split '/' + board = path[1] + threadID = path[3] + postID = id + else + board = q.dataset.board + threadID = 0 + postID = q.dataset.id + + el = if board is g.BOARD then $.id "p#{postID}" else false inline = $.el 'div', - id: "i#{id}" + id: "i#{postID}" className: if el then 'inline' else 'inline crosspost' $.after (if isBacklink then q.parentNode else root), inline - Get.post path[1], path[3], id, inline + Get.post board, threadID, postID, inline return unless el @@ -2466,7 +2639,7 @@ QuotePreview = Main.callbacks.push @node node: (post) -> for quote in post.quotes - $.on quote, 'mouseover', QuotePreview.mouseover if quote.hash + $.on quote, 'mouseover', QuotePreview.mouseover if quote.hash or /\bdeadlink\b/.test quote.className for quote in post.backlinks $.on quote, 'mouseover', QuotePreview.mouseover return @@ -2483,18 +2656,27 @@ QuotePreview = # Don't stop other elements from dragging return if UI.el - path = @pathname.split '/' - id = @hash[2..] - qp = UI.el = $.el 'div', + if @host is 'boards.4chan.org' + path = @pathname.split '/' + board = path[1] + threadID = path[3] + postID = @hash[2..] + else + board = @dataset.board + threadID = 0 + postID = @dataset.id + + qp = UI.el = $.el 'div', id: 'qp' className: 'reply dialog' UI.hover e $.add d.body, qp - Get.post path[1], path[3], id, qp, -> + Get.post board, threadID, postID, qp, -> bq = $ 'blockquote', qp Main.prettify bq post = el: qp + blockquote: bq if fileInfo = $ '.fileInfo', qp img = fileInfo.nextElementSibling.firstElementChild if img.alt isnt 'File deleted.' @@ -2506,8 +2688,10 @@ QuotePreview = Time.node post if Conf['File Info Formatting'] FileInfo.node post + if Conf['Resurrect Quotes'] + Quotify.node post - if path[1] is g.BOARD and el = $.id "p#{id}" + if board is g.BOARD and el = $.id "p#{postID}" if Conf['Quote Highlighting'] if /\bop\b/.test el.className $.addClass el.parentNode, 'qphl' @@ -2603,8 +2787,11 @@ Quotify = else a.href = Redirect.thread board, id, 'post' a.className = 'deadlink' - # a.className = if JSONable then 'quotelink deadlink' else 'deadlink' a.target = '_blank' + if Redirect.post board, id + $.addClass a, 'quotelink' + a.dataset.board = board + a.dataset.id = id data = data[index + quote.length..] @@ -2793,6 +2980,10 @@ Redirect = # "http://archive.xfiles.to/#{board}/full_image/#{filename}" # when 'e' # "https://md401.homelinux.net/4chan/cgi-board.pl/#{board}/full_image/#{filename}" + post: (board, postID) -> + switch board + when 'a', 'co', 'jp', 'm', 'tg', 'tv', 'u', 'v', 'vg' + "http://archive.foolz.us/api/chan/post/board/#{board}/num/#{postID}/format/json" thread: (board=g.BOARD, id=g.THREAD_ID, mode='thread') -> return unless Conf['404 Redirect'] or mode is 'post' switch board