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: "
"
+ }));
+ $.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: "
"
+ $.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