diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bbdbb486..7a9b09c5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +- **New feature**: `Linkify` and `Clean Links`, enabled by default + - Linkify will turn text URLs into working links. + - Clean Links will get rid of spoiler and code tags in linkified URLs used to bypass spam blocks. + ## 3.9.0 - *2013-08-18* - **New feature**: `Desktop Notifications` diff --git a/Gruntfile.coffee b/Gruntfile.coffee index 2c0ff150b..753ac3df2 100644 --- a/Gruntfile.coffee +++ b/Gruntfile.coffee @@ -31,6 +31,7 @@ module.exports = (grunt) -> 'src/Quotelinks/**/*' 'src/Posting/**/*' 'src/Images/**/*' + 'src/Linkification/**/*' 'src/Menu/**/*' 'src/Monitoring/**/*' 'src/Archive/**/*' diff --git a/src/General/Config.coffee b/src/General/Config.coffee index fc174dcfb..24fa40faf 100644 --- a/src/General/Config.coffee +++ b/src/General/Config.coffee @@ -27,6 +27,9 @@ Config = 'Image Hover': [false, 'Show a floating expanded image on hover.'] 'Sauce': [true, 'Add sauce links to images.'] 'Reveal Spoilers': [false, 'Reveal spoiler thumbnails.'] + 'Linkification': + 'Linkify': [true, 'Convert text links into hyperlinks.'] + 'Clean Links': [true, 'Remove spoiler and code tags commonly used to bypass blocked links.'] 'Menu': 'Menu': [true, 'Add a drop-down menu to posts.'] 'Report Link': [true, 'Add a report link to the menu.'] diff --git a/src/General/Main.coffee b/src/General/Main.coffee index 9a26ad43b..23f35d9b5 100644 --- a/src/General/Main.coffee +++ b/src/General/Main.coffee @@ -128,6 +128,7 @@ Main = initFeature 'Index Navigation', Nav initFeature 'Keybinds', Keybinds initFeature 'Show Dice Roll', Dice + initFeature 'Linkify', Linkify # c.timeEnd 'All initializations' $.on d, 'AddCallback', Main.addCallback diff --git a/src/General/Post.coffee b/src/General/Post.coffee index 1b890c2ec..7319235fd 100644 --- a/src/General/Post.coffee +++ b/src/General/Post.coffee @@ -56,6 +56,8 @@ class Post @kill() if that.isArchived parseComment: -> + # Merge text nodes and remove empty ones. + @nodes.comment.normalize() # Get the comment's text. #
-> \n # Remove: diff --git a/src/Linkification/Linkify.coffee b/src/Linkification/Linkify.coffee new file mode 100644 index 000000000..99f667fbe --- /dev/null +++ b/src/Linkification/Linkify.coffee @@ -0,0 +1,104 @@ +Linkify = + init: -> + return if g.VIEW is 'catalog' or !Conf['Linkify'] + + # gruber revised + magnet support + # http://rodneyrehm.de/t/url-regex.html + @catchAll = /\b([a-z][\w-]+:(\/{1,3}|[a-z0-9%]|\?(dn|x[lts]|as|kt|mt|tr)=)|www\d{0,3}\.|[a-z0-9.\-]+\.[a-z]{2,4}\/)([^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’])/g + + Post::callbacks.push + name: 'Linkify' + cb: @node + + node: -> + return if @isClone or !links = @info.comment.match Linkify.catchAll + walker = d.createTreeWalker @nodes.comment, 4 + range = d.createRange() + for link in links + boundaries = Linkify.find link, walker + # break unless boundaries + anchor = Linkify.createLink link + if Linkify.surround anchor, range, boundaries + if (parent = anchor.parentNode).href is anchor.href + # Replace already-linkified links, + # f.e.: https://boards.4chan.org/b/% + $.replace parent, anchor + Linkify.cleanLink anchor, link if Conf['Clean Links'] + walker.currentNode = anchor.lastChild + else + walker.currentNode = boundaries.endNode + range.detach() + + find: (link, walker) -> + # Walk through the nodes until we find the entire link. + text = '' + while node = walker.nextNode() + text += node.data + break if text.indexOf(link) > -1 + # return unless node + startNode = endNode = node + + # Walk backwards to find the startNode. + text = node.data + until (index = text.indexOf link) > -1 + startNode = walker.previousNode() + text = "#{startNode.data}#{text}" + + return { + startNode, endNode + startOffset: index + endOffset: endNode.length - (text.length - index - link.length) + } + + createLink: (link) -> + unless /^[a-z][\w-]+:/.test link + link = "http://#{link}" + $.el 'a', + href: link + className: 'linkified' + target: '_blank' + + surround: (anchor, range, boundaries) -> + {startOffset, endOffset, startNode, endNode} = boundaries + range.setStart startNode, startOffset + range.setEnd endNode, endOffset + try + range.surroundContents anchor + true + catch + <% if (type === 'crx') { %> + # Chrome bug: crbug.com/275848 + return true if anchor.parentNode + <% } %> + # Attempt to handle cases such as: + # [spoiler]www.[/spoiler]example.com # + # www.example[spoiler].com[/spoiler] # + return false if boundaries.areRelocated + Linkify.relocate boundaries + Linkify.surround anchor, range, boundaries + + relocate: (boundaries) -> + # What do you mean, "silly"? + boundaries.areRelocated = true + + if boundaries.startOffset is 0 + parentNode = boundaries.startNode + until parentNode.previousSibling + {parentNode} = parentNode + parent = parentNode.parentNode + boundaries.startNode = parent + boundaries.startOffset = [parent.childNodes...].indexOf parentNode + + if boundaries.endOffset is boundaries.endNode.length + parentNode = boundaries.endNode + until parentNode.nextSibling + {parentNode} = parentNode + parent = parentNode.parentNode + boundaries.endNode = parent + boundaries.endOffset = [parent.childNodes...].indexOf(parentNode) + 1 + + cleanLink: (anchor, link) -> + {length} = link + for node in $$ 's, .prettyprint', anchor + $.replace node, [node.childNodes...] if length > node.textContent.length + return diff --git a/src/Miscellaneous/ExpandComment.coffee b/src/Miscellaneous/ExpandComment.coffee index 8d87de91e..3567e9f84 100644 --- a/src/Miscellaneous/ExpandComment.coffee +++ b/src/Miscellaneous/ExpandComment.coffee @@ -72,3 +72,5 @@ ExpandComment = Fourchan.code.call post if g.BOARD.ID is 'sci' Fourchan.math.call post + if Conf['Linkify'] + Linkify.node.call post