diff --git a/4chan_x.user.js b/4chan_x.user.js index d03600f20..63a6d7c35 100644 --- a/4chan_x.user.js +++ b/4chan_x.user.js @@ -43,7 +43,7 @@ */ (function() { - var $, $$, Anonymize, ArchiveLink, AutoGIF, Board, Build, Clone, Conf, Config, DeleteLink, DownloadLink, Favicon, FileInfo, Filter, Get, Header, ImageExpand, ImageHover, Main, Menu, Notification, Polyfill, Post, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, Quotify, Recursive, Redirect, RelativeDates, ReplyHiding, ReportLink, RevealSpoilers, Sauce, Settings, Thread, ThreadExcerpt, ThreadHiding, ThreadStats, ThreadUpdater, Time, UI, Unread, d, doc, g, + var $, $$, Anonymize, ArchiveLink, AutoGIF, Board, Build, Clone, Conf, Config, DeleteLink, DownloadLink, ExpandComment, Favicon, FileInfo, Filter, Get, Header, ImageExpand, ImageHover, Main, Menu, Notification, Polyfill, Post, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, Quotify, Recursive, Redirect, RelativeDates, ReplyHiding, ReportLink, RevealSpoilers, Sauce, Settings, Thread, ThreadExcerpt, ThreadHiding, ThreadStats, ThreadUpdater, Time, UI, Unread, d, doc, g, __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }, __hasProp = {}.hasOwnProperty, __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; @@ -2653,7 +2653,7 @@ }); } else { $.addClass(root, 'warning'); - root.textContent = status === 404 ? "Thread No." + threadID + " 404'd." : "Error " + req.status + ": " + req.statusText + "."; + root.textContent = status === 404 ? "Thread No." + threadID + " 404'd." : "Error " + req.statusText + " (" + req.status + ")."; } return; } @@ -3069,7 +3069,7 @@ for (_i = 0, _len = _ref.length; _i < _len; _i++) { quote = _ref[_i]; containers = [QuoteBacklink.getContainer(quote)]; - if (post = g.posts[quote]) { + if ((post = g.posts[quote]) && post.nodes.backlinkContainer) { _ref1 = post.clones; for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { clone = _ref1[_j]; @@ -3091,11 +3091,11 @@ }, secondNode: function() { var container; - if (this.isClone && this.origin.nodes.backlinkContainer) { + if (this.isClone && (this.origin.isReply || Conf['OP Backlinks'])) { this.nodes.backlinkContainer = $('.container', this.nodes.info); return; } - if (!(Conf['OP Backlinks'] || this.isReply)) { + if (!(this.isReply || Conf['OP Backlinks'])) { return; } container = QuoteBacklink.getContainer(this.fullID); @@ -3891,6 +3891,86 @@ } }; + ExpandComment = { + init: function() { + if (g.VIEW !== 'index' || !Conf['Comment Expansion']) { + return; + } + return Post.prototype.callbacks.push({ + name: 'Comment Expansion', + cb: this.node + }); + }, + node: function() { + var a; + if (a = $('.abbr > a', this.nodes.comment)) { + return $.on(a, 'click', ExpandComment.expand); + } + }, + expand: function(e) { + var a, post; + e.preventDefault(); + post = Get.postFromNode(this); + this.textContent = "Post No." + post + " Loading..."; + a = this; + return $.cache("//api.4chan.org" + this.pathname + ".json", function() { + return ExpandComment.parse(this, a, post); + }); + }, + parse: function(req, a, post) { + var comment, href, postObj, posts, prev, quote, spoilerRange, _i, _j, _len, _len1, _ref; + if (req.status !== 200) { + a.textContent = "Error " + req.statusText + " (" + req.status + ")"; + return; + } + posts = JSON.parse(req.response).posts; + if (spoilerRange = posts[0].custom_spoiler) { + Build.spoilerRange[g.BOARD] = spoilerRange; + } + for (_i = 0, _len = posts.length; _i < _len; _i++) { + postObj = posts[_i]; + if (postObj.no === post.ID) { + break; + } + } + if (postObj.no !== post.ID) { + a.textContent = "Post No." + post + " not found."; + return; + } + comment = post.nodes.comment; + comment.innerHTML = postObj.com; + _ref = $$('.quotelink', comment); + for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) { + quote = _ref[_j]; + href = quote.getAttribute('href'); + if (href[0] === '/') { + continue; + } + quote.href = "/" + post.board + "/res/" + href; + } + post.parseComment(); + post.parseQuotes(); + if (Conf['Resurrect Quotes']) { + Quotify.node.call(post); + } + if (Conf['Quote Preview']) { + QuotePreview.node.call(post); + } + if (Conf['Quote Inline']) { + QuoteInline.node.call(post); + } + if (Conf['Mark OP Quotes']) { + QuoteOP.node.call(post); + } + if (Conf['Mark Cross-thread Quotes']) { + QuoteCT.node.call(post); + } + prev = comment.previousSibling; + $.rm(comment); + return $.after(prev, comment); + } + }; + ThreadExcerpt = { init: function() { if (g.VIEW !== 'thread' || !Conf['Thread Excerpt']) { @@ -5373,7 +5453,7 @@ }; function Post(root, thread, board, that) { - var alt, anchor, bq, capcode, data, date, email, file, fileInfo, flag, hash, i, info, name, node, nodes, pathname, post, quotelink, quotes, size, subject, text, thumb, tripcode, uniqueID, unit, _i, _j, _k, _len, _len1, _ref, _ref1, _ref2; + var alt, anchor, capcode, date, email, file, fileInfo, flag, info, name, post, size, subject, thumb, tripcode, uniqueID, unit; this.thread = thread; this.board = board; if (that == null) { @@ -5424,40 +5504,8 @@ this.nodes.date = date; this.info.date = new Date(date.dataset.utc * 1000); } - bq = this.nodes.comment.cloneNode(true); - _ref = $$('.abbr, .capcodeReplies, .exif, b', bq); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - node = _ref[_i]; - $.rm(node); - } - text = []; - nodes = d.evaluate('.//br|.//text()', bq, null, 7, null); - for (i = _j = 0, _ref1 = nodes.snapshotLength; 0 <= _ref1 ? _j < _ref1 : _j > _ref1; i = 0 <= _ref1 ? ++_j : --_j) { - text.push((data = nodes.snapshotItem(i).data) ? data : '\n'); - } - this.info.comment = text.join('').trim().replace(/\s+$/gm, ''); - quotes = {}; - _ref2 = $$('.quotelink', this.nodes.comment); - for (_k = 0, _len1 = _ref2.length; _k < _len1; _k++) { - quotelink = _ref2[_k]; - hash = quotelink.hash; - if (!hash) { - continue; - } - pathname = quotelink.pathname; - if (/catalog$/.test(pathname)) { - continue; - } - if (quotelink.hostname !== 'boards.4chan.org') { - continue; - } - this.nodes.quotelinks.push(quotelink); - if (quotelink.parentNode.parentNode.className === 'capcodeReplies') { - continue; - } - quotes["" + (pathname.split('/')[1]) + "." + hash.slice(2)] = true; - } - this.quotes = Object.keys(quotes); + this.parseComment(); + this.parseQuotes(); if ((file = $('.file', post)) && (thumb = $('img[data-md5]', file))) { alt = thumb.alt; anchor = thumb.parentNode; @@ -5491,6 +5539,51 @@ } } + Post.prototype.parseComment = function() { + var bq, data, i, node, nodes, text, _i, _j, _len, _ref, _ref1; + bq = this.nodes.comment.cloneNode(true); + _ref = $$('.abbr, .capcodeReplies, .exif, b', bq); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + node = _ref[_i]; + $.rm(node); + } + text = []; + nodes = d.evaluate('.//br|.//text()', bq, null, 7, null); + for (i = _j = 0, _ref1 = nodes.snapshotLength; 0 <= _ref1 ? _j < _ref1 : _j > _ref1; i = 0 <= _ref1 ? ++_j : --_j) { + text.push((data = nodes.snapshotItem(i).data) ? data : '\n'); + } + return this.info.comment = text.join('').trim().replace(/\s+$/gm, ''); + }; + + Post.prototype.parseQuotes = function() { + var hash, pathname, quotelink, quotes, _i, _len, _ref; + quotes = {}; + _ref = $$('.quotelink', this.nodes.comment); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + quotelink = _ref[_i]; + hash = quotelink.hash; + if (!hash) { + continue; + } + pathname = quotelink.pathname; + if (/catalog$/.test(pathname)) { + continue; + } + if (quotelink.hostname !== 'boards.4chan.org') { + continue; + } + this.nodes.quotelinks.push(quotelink); + if (quotelink.parentNode.parentNode.className === 'capcodeReplies') { + continue; + } + quotes["" + (pathname.split('/')[1]) + "." + hash.slice(2)] = true; + } + if (this.isClone) { + return; + } + return this.quotes = Object.keys(quotes); + }; + Post.prototype.kill = function(file, now) { var clone, quotelink, strong, _i, _j, _len, _len1, _ref, _ref1; now || (now = new Date()); @@ -5556,7 +5649,7 @@ __extends(Clone, _super); function Clone(origin, context) { - var file, index, info, inline, inlined, key, nodes, post, quotelink, root, val, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1, _ref2, _ref3, _ref4; + var file, index, info, inline, inlined, key, nodes, post, root, val, _i, _j, _k, _len, _len1, _len2, _ref, _ref1, _ref2, _ref3; this.origin = origin; this.context = context; _ref = ['ID', 'fullID', 'board', 'thread', 'info', 'quotes', 'isReply']; @@ -5612,18 +5705,12 @@ if (nodes.date) { this.nodes.date = $('.dateTime', info); } - _ref3 = $$('.quotelink', this.nodes.comment); - for (_l = 0, _len3 = _ref3.length; _l < _len3; _l++) { - quotelink = _ref3[_l]; - if (quotelink.hash || $.hasClass(quotelink, 'deadlink')) { - this.nodes.quotelinks.push(quotelink); - } - } + this.parseQuotes(); if (origin.file) { this.file = {}; - _ref4 = origin.file; - for (key in _ref4) { - val = _ref4[key]; + _ref3 = origin.file; + for (key in _ref3) { + val = _ref3[key]; this.file[key] = val; } file = $('.file', post); @@ -5739,6 +5826,7 @@ initFeature('Reveal Spoilers', RevealSpoilers); initFeature('Auto-GIF', AutoGIF); initFeature('Image Hover', ImageHover); + initFeature('Comment Expansion', ExpandComment); initFeature('Thread Excerpt', ThreadExcerpt); initFeature('Favicon', Favicon); initFeature('Unread', Unread); diff --git a/src/features.coffee b/src/features.coffee index 83129a91a..f0fa8090d 100644 --- a/src/features.coffee +++ b/src/features.coffee @@ -1469,7 +1469,7 @@ Get = if status is 404 "Thread No.#{threadID} 404'd." else - "Error #{req.status}: #{req.statusText}." + "Error #{req.statusText} (#{req.status})." return posts = JSON.parse(req.response).posts @@ -1839,7 +1839,9 @@ QuoteBacklink = textContent: QuoteBacklink.funk @ID for quote in @quotes containers = [QuoteBacklink.getContainer quote] - if post = g.posts[quote] + if (post = g.posts[quote]) and post.nodes.backlinkContainer + # Don't add OP clones when OP Backlinks is disabled, + # as the clones won't have the backlink containers. for clone in post.clones containers.push clone.nodes.backlinkContainer for container in containers @@ -1851,11 +1853,11 @@ QuoteBacklink = $.add container, [$.tn(' '), link] return secondNode: -> - if @isClone and @origin.nodes.backlinkContainer + if @isClone and (@origin.isReply or Conf['OP Backlinks']) @nodes.backlinkContainer = $ '.container', @nodes.info return # Don't backlink the OP. - return unless Conf['OP Backlinks'] or @isReply + return unless @isReply or Conf['OP Backlinks'] container = QuoteBacklink.getContainer @fullID @nodes.backlinkContainer = container $.add @nodes.info, container @@ -2420,6 +2422,62 @@ ImageHover = $.ajax URL, onreadystatechange: (-> clearTimeout timeoutID if @status is 404), type: 'head' +ExpandComment = + init: -> + return if g.VIEW isnt 'index' or !Conf['Comment Expansion'] + + Post::callbacks.push + name: 'Comment Expansion' + cb: @node + node: -> + if a = $ '.abbr > a', @nodes.comment + $.on a, 'click', ExpandComment.expand + expand: (e) -> + e.preventDefault() + post = Get.postFromNode @ + @textContent = "Post No.#{post} Loading..." + a = @ + $.cache "//api.4chan.org#{@pathname}.json", -> ExpandComment.parse @, a, post + parse: (req, a, post) -> + if req.status isnt 200 + a.textContent = "Error #{req.statusText} (#{req.status})" + return + + posts = JSON.parse(req.response).posts + if spoilerRange = posts[0].custom_spoiler + Build.spoilerRange[g.BOARD] = spoilerRange + + for postObj in posts + break if postObj.no is post.ID + if postObj.no isnt post.ID + a.textContent = "Post No.#{post} not found." + return + + {comment} = post.nodes + comment.innerHTML = postObj.com + for quote in $$ '.quotelink', comment + href = quote.getAttribute 'href' + continue if href[0] is '/' # Cross-board quote, or board link + quote.href = "/#{post.board}/res/#{href}" # Fix pathnames + post.parseComment() + post.parseQuotes() + if Conf['Resurrect Quotes'] + Quotify.node.call post + if Conf['Quote Preview'] + QuotePreview.node.call post + if Conf['Quote Inline'] + QuoteInline.node.call post + if Conf['Mark OP Quotes'] + QuoteOP.node.call post + if Conf['Mark Cross-thread Quotes'] + QuoteCT.node.call post + # XXX g code + # XXX sci math + # Fix linkifiers: + prev = comment.previousSibling + $.rm comment + $.after prev, comment + ThreadExcerpt = init: -> return if g.VIEW isnt 'thread' or !Conf['Thread Excerpt'] diff --git a/src/main.coffee b/src/main.coffee index c9593a0a0..eb29dba28 100644 --- a/src/main.coffee +++ b/src/main.coffee @@ -66,47 +66,8 @@ class Post @nodes.date = date @info.date = new Date date.dataset.utc * 1000 - # Get the comment's text. - #
-> \n - # Remove: - # 'Comment too long'... - # Admin/Mod/Dev replies. (/q/) - # EXIF data. (/p/) - # Rolls. (/tg/) - # Preceding and following new lines. - # Trailing spaces. - bq = @nodes.comment.cloneNode true - for node in $$ '.abbr, .capcodeReplies, .exif, b', bq - $.rm node - text = [] - # XPathResult.ORDERED_NODE_SNAPSHOT_TYPE === 7 - nodes = d.evaluate './/br|.//text()', bq, null, 7, null - for i in [0...nodes.snapshotLength] - text.push if data = nodes.snapshotItem(i).data then data else '\n' - @info.comment = text.join('').trim().replace /\s+$/gm, '' - - quotes = {} - for quotelink in $$ '.quotelink', @nodes.comment - # Don't add board links. (>>>/b/) - hash = quotelink.hash - continue unless hash - - # Don't add catalog links. (>>>/b/catalog or >>>/b/search) - pathname = quotelink.pathname - continue if /catalog$/.test pathname - - # Don't add rules links. (>>>/a/rules) - # Don't add text-board quotelinks. (>>>/img/1234) - continue if quotelink.hostname isnt 'boards.4chan.org' - - @nodes.quotelinks.push quotelink - - # Don't count capcode replies as quotes. (Admin/Mod/Dev Replies: ...) - continue if quotelink.parentNode.parentNode.className is 'capcodeReplies' - - # Basically, only add quotes that link to posts on an imageboard. - quotes["#{pathname.split('/')[1]}.#{hash[2..]}"] = true - @quotes = Object.keys quotes + @parseComment() + @parseQuotes() if (file = $ '.file', post) and thumb = $ 'img[data-md5]', file # Supports JPG/PNG/GIF/PDF. @@ -145,6 +106,51 @@ class Post g.posts["#{board}.#{@}"] = thread.posts[@] = board.posts[@] = @ @kill() if that.isArchived + parseComment: -> + # Get the comment's text. + #
-> \n + # Remove: + # 'Comment too long'... + # Admin/Mod/Dev replies. (/q/) + # EXIF data. (/p/) + # Rolls. (/tg/) + # Preceding and following new lines. + # Trailing spaces. + bq = @nodes.comment.cloneNode true + for node in $$ '.abbr, .capcodeReplies, .exif, b', bq + $.rm node + text = [] + # XPathResult.ORDERED_NODE_SNAPSHOT_TYPE === 7 + nodes = d.evaluate './/br|.//text()', bq, null, 7, null + for i in [0...nodes.snapshotLength] + text.push if data = nodes.snapshotItem(i).data then data else '\n' + @info.comment = text.join('').trim().replace /\s+$/gm, '' + + parseQuotes: -> + quotes = {} + for quotelink in $$ '.quotelink', @nodes.comment + # Don't add board links. (>>>/b/) + hash = quotelink.hash + continue unless hash + + # Don't add catalog links. (>>>/b/catalog or >>>/b/search) + pathname = quotelink.pathname + continue if /catalog$/.test pathname + + # Don't add rules links. (>>>/a/rules) + # Don't add text-board quotelinks. (>>>/img/1234) + continue if quotelink.hostname isnt 'boards.4chan.org' + + @nodes.quotelinks.push quotelink + + # Don't count capcode replies as quotes. (Admin/Mod/Dev Replies: ...) + continue if quotelink.parentNode.parentNode.className is 'capcodeReplies' + + # Basically, only add quotes that link to posts on an imageboard. + quotes["#{pathname.split('/')[1]}.#{hash[2..]}"] = true + return if @isClone + @quotes = Object.keys quotes + kill: (file, now) -> now or= new Date() if file @@ -228,10 +234,7 @@ class Clone extends Post if nodes.date @nodes.date = $ '.dateTime', info - for quotelink in $$ '.quotelink', @nodes.comment - # See comments in Post's constructor. - if quotelink.hash or $.hasClass quotelink, 'deadlink' - @nodes.quotelinks.push quotelink + @parseQuotes() if origin.file # Copy values, point to relevant elements. @@ -333,6 +336,7 @@ Main = initFeature 'Reveal Spoilers', RevealSpoilers initFeature 'Auto-GIF', AutoGIF initFeature 'Image Hover', ImageHover + initFeature 'Comment Expansion', ExpandComment initFeature 'Thread Excerpt', ThreadExcerpt initFeature 'Favicon', Favicon initFeature 'Unread', Unread