This commit is contained in:
Zixaphir 2013-03-15 19:34:16 -07:00
parent 0ba9e6027b
commit 69763f87e9
5 changed files with 536 additions and 7 deletions

View File

@ -43,7 +43,7 @@
*/ */
(function() { (function() {
var $, $$, Anonymize, ArchiveLink, Board, Build, Clone, Conf, Config, CustomCSS, DeleteLink, DownloadLink, ExpandComment, ExpandThread, Favicon, FileInfo, Filter, Fourchan, Get, Header, ImageExpand, ImageHover, ImageReplace, Keybinds, Main, Menu, Misc, Nav, Notification, Polyfill, Post, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, QuoteStrikeThrough, QuoteYou, Quotify, Recursive, Redirect, RelativeDates, ReplyHiding, Report, ReportLink, RevealSpoilers, Sauce, Settings, Thread, ThreadExcerpt, ThreadHiding, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, c, d, doc, g, var $, $$, Anonymize, ArchiveLink, Board, Build, Clone, Conf, Config, CustomCSS, DeleteLink, DownloadLink, ExpandComment, ExpandThread, Favicon, FileInfo, Filter, Fourchan, Get, Header, ImageExpand, ImageHover, ImageReplace, Keybinds, Linkify, Main, Menu, Misc, Nav, Notification, Polyfill, Post, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, QuoteStrikeThrough, QuoteYou, Quotify, Recursive, Redirect, RelativeDates, ReplyHiding, Report, ReportLink, RevealSpoilers, Sauce, Settings, Thread, ThreadExcerpt, ThreadHiding, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, c, d, doc, g,
__slice = [].slice, __slice = [].slice,
__hasProp = {}.hasOwnProperty, __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; }; __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; };
@ -64,6 +64,11 @@
'Custom CSS': [false, 'Apply custom CSS to 4chan.'], 'Custom CSS': [false, 'Apply custom CSS to 4chan.'],
'Check for Updates': [true, 'Check for updated versions of 4chan X Beta.'] 'Check for Updates': [true, 'Check for updated versions of 4chan X Beta.']
}, },
'Linkification': {
'Linkify': [true, 'Convert text into links where applicable.'],
'Embedding': [true, 'Embed supported services.'],
'Link Title': [true, 'Replace the link of a supported site with its actual title. Currently Supported: YouTube, Vimeo, SoundCloud']
},
'Filtering': { 'Filtering': {
'Anonymize': [false, 'Turn everyone Anonymous.'], 'Anonymize': [false, 'Turn everyone Anonymous.'],
'Filter': [true, 'Self-moderation placebo.'], 'Filter': [true, 'Self-moderation placebo.'],
@ -820,11 +825,13 @@
return style; return style;
}, },
x: function(path, root) { x: function(path, root) {
if (root == null) { root || (root = d.body);
root = d.body;
}
return d.evaluate(path, root, null, 8, null).singleNodeValue; return d.evaluate(path, root, null, 8, null).singleNodeValue;
}, },
X: function(path, root) {
root || (root = d.body);
return d.evaluate(path, root, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
},
addClass: function(el, className) { addClass: function(el, className) {
return el.classList.add(className); return el.classList.add(className);
}, },
@ -840,12 +847,15 @@
tn: function(s) { tn: function(s) {
return d.createTextNode(s); return d.createTextNode(s);
}, },
frag: function() {
return d.createDocumentFragment();
},
nodes: function(nodes) { nodes: function(nodes) {
var frag, node, _i, _len; var frag, node, _i, _len;
if (!(nodes instanceof Array)) { if (!(nodes instanceof Array)) {
return nodes; return nodes;
} }
frag = d.createDocumentFragment(); frag = $.frag();
for (_i = 0, _len = nodes.length; _i < _len; _i++) { for (_i = 0, _len = nodes.length; _i < _len; _i++) {
node = nodes[_i]; node = nodes[_i];
frag.appendChild(node); frag.appendChild(node);
@ -5973,6 +5983,262 @@
} }
}; };
Linkify = {
init: function() {
if (g.VIEW === 'catalog' || !Conf['Linkify']) {
return;
}
return Post.prototype.callbacks.push({
name: 'Linkify',
cb: this.node
});
},
regString: /(\b([a-z]+:\/\/|[a-z]{3,}\.[-a-z0-9]+\.[a-z]+|[-a-z0-9]+\.[a-z]|[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+|[a-z]{3,}:[a-z0-9?]|[a-z0-9._%+-:]+@[a-z0-9.-]+\.[a-z0-9])[^\s'"]+)/gi,
cypher: $.el('div'),
node: function() {
var a, child, cypher, cypherText, data, embedder, i, index, len, link, links, lookahead, name, next, node, nodes, snapshot, spoiler, text, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1, _ref2, _results;
if (this.isClone && Conf['Embedding']) {
_ref = $$('.embedder', this.nodes.comment);
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
embedder = _ref[_i];
$.on(embedder, Linkify.toggle);
}
return;
}
snapshot = $.X('.//text()', this.nodes.comment);
cypher = Linkify.cypher;
i = -1;
len = snapshot.snapshotLength;
_results = [];
while (++i < len) {
nodes = $.frag();
node = snapshot.snapshotItem(i);
data = node.data;
if (!(node.parentNode && Linkify.regString.test(data))) {
continue;
}
Linkify.regString.lastIndex = 0;
cypherText = [];
if (next = node.nextSibling) {
cypher.textContent = node.textContent;
cypherText[0] = cypher.innerHTML;
while ((next.nodeName.toLowerCase() === 'wbr' || next.nodeName.toLowerCase() === 's') && (lookahead = next.nextSibling) && ((name = lookahead.nodeName) === "#text" || name.toLowerCase() === 'br')) {
cypher.textContent = lookahead.textContent;
cypherText.push((spoiler = next.innerHTML) ? "<s>" + (spoiler.replace(/</g, ' <')) + "</s>" : '<wbr>');
cypherText.push(cypher.innerHTML);
$.rm(next);
next = lookahead.nextSibling;
if (lookahead.nodeName === "#text") {
$.rm(lookahead);
}
if (!next) {
break;
}
}
}
if (cypherText.length) {
data = cypherText.join('');
}
links = data.match(Linkify.regString);
for (_j = 0, _len1 = links.length; _j < _len1; _j++) {
link = links[_j];
index = data.indexOf(link);
if (text = data.slice(0, index)) {
cypher.innerHTML = text;
_ref1 = __slice.call(cypher.childNodes);
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
child = _ref1[_k];
$.add(nodes, child);
}
}
cypher.innerHTML = (link.indexOf(':') < 0 ? (link.indexOf('@') > 0 ? 'mailto:' + link : 'http://' + link) : link).replace(/<(wbr|s|\/s)>/g, '');
a = $.el('a', {
innerHTML: link,
className: 'linkify',
rel: 'nofollow noreferrer',
target: '_blank',
href: cypher.textContent
});
$.add(nodes, Linkify.embedder(a));
data = data.slice(index + link.length);
}
if (data) {
cypher.innerHTML = data;
_ref2 = __slice.call(cypher.childNodes);
for (_l = 0, _len3 = _ref2.length; _l < _len3; _l++) {
child = _ref2[_l];
$.add(nodes, child);
}
}
_results.push($.replace(node, nodes));
}
return _results;
},
toggle: function() {
var el, embed, style, type, url;
embed = this.previousElementSibling;
if (this.className.contains("embedded")) {
el = $.el('a', {
rel: 'nofollow noreferrer',
target: 'blank',
className: 'linkify',
href: url = this.getAttribute("data-originalURL"),
textContent: this.getAttribute("data-title") || url
});
this.textContent = '(embed)';
} else {
el = (type = Linkify.types[this.getAttribute("data-service")]).el.call(this);
el.style.cssText = (style = type.style) ? style : "border: 0; width: " + ($.get('embedWidth', Config['embedWidth'])) + "px; height: " + ($.get('embedHeight', Config['embedHeight'])) + "px";
this.textContent = '(unembed)';
}
$.replace(embed, el);
return $.toggleClass(this, 'embedded');
},
types: {
YouTube: {
regExp: /.*(?:youtu.be\/|youtube.*v=|youtube.*\/embed\/|youtube.*\/v\/|youtube.*videos\/)([^#\&\?]*).*/,
el: function() {
return $.el('iframe', {
src: "//www.youtube.com/embed/" + this.name
});
},
title: {
api: function() {
return "https://gdata.youtube.com/feeds/api/videos/" + this.name + "?alt=json&fields=title/text(),yt:noembed,app:control/yt:state/@reasonCode";
},
text: function() {
return JSON.parse(this.responseText).entry.title.$t;
}
}
},
Vocaroo: {
regExp: /.*(?:vocaroo.com\/)([^#\&\?]*).*/,
style: 'border: 0; width: 150px; height: 45px;',
el: function() {
return $.el('object', {
innerHTML: "<embed src='http://vocaroo.com/player.swf?playMediaID=" + (this.name.replace(/^i\//, '')) + "&autoplay=0' width='150' height='45' pluginspage='http://get.adobe.com/flashplayer/' type='application/x-shockwave-flash'></embed>"
});
}
},
Vimeo: {
regExp: /.*(?:vimeo.com\/)([^#\&\?]*).*/,
el: function() {
return $.el('iframe', {
src: "//player.vimeo.com/video/" + this.name
});
},
title: {
api: function() {
return "https://vimeo.com/api/oembed.json?url=http://vimeo.com/" + this.name;
},
text: function() {
return JSON.parse(this.responseText).title;
}
}
},
LiveLeak: {
regExp: /.*(?:liveleak.com\/view.+i=)([0-9a-z_]+)/,
el: function() {
return $.el('iframe', {
src: "http://www.liveleak.com/e/" + this.name + "?autostart=true"
});
}
},
audio: {
regExp: /(.*\.(mp3|ogg|wav))$/,
el: function() {
return $.el('audio', {
controls: 'controls',
preload: 'auto',
src: this.name
});
}
},
SoundCloud: {
regExp: /.*(?:soundcloud.com\/|snd.sc\/)([^#\&\?]*).*/,
el: function() {
var div;
div = $.el('div', {
className: "soundcloud",
name: "soundcloud"
});
return $.ajax("//soundcloud.com/oembed?show_artwork=false&&maxwidth=500px&show_comments=false&format=json&url=" + (this.getAttribute('data-originalURL')) + "&color=" + (Style.colorToHex(Themes[Conf['theme']]['Background Color'])), {
div: div,
onloadend: function() {
return this.div.innerHTML = JSON.parse(this.responseText).html;
}
}, false);
}
},
pastebin: {
regExp: /.*(?:pastebin.com\/)([^#\&\?]*).*/,
el: function() {
var div;
return div = $.el('iframe', {
src: "http://pastebin.com/embed_iframe.php?i=" + this.name
});
}
}
},
embedder: function(a) {
var callbacks, embed, key, match, service, title, titles, type, _ref;
if (!Conf['Embedding']) {
return [a];
}
callbacks = function() {
var title;
return a.textContent = (function() {
switch (this.status) {
case 200:
case 304:
title = "[" + (embed.getAttribute('data-service')) + "] " + (service.text.call(this));
embed.setAttribute('data-title', title);
titles[embed.name] = [title, Date.now()];
$.set('CachedTitles', titles);
return title;
case 404:
return "[" + key + "] Not Found";
case 403:
return "[" + key + "] Forbidden or Private";
default:
return "[" + key + "] " + this.status + "'d";
}
}).call(this);
};
_ref = Linkify.types;
for (key in _ref) {
type = _ref[key];
if (!(match = a.href.match(type.regExp))) {
continue;
}
embed = $.el('a', {
name: (a.name = match[1]),
className: 'embedder',
href: 'javascript:;',
textContent: '(embed)'
});
embed.setAttribute('data-service', key);
embed.setAttribute('data-originalURL', a.href);
$.on(embed, 'click', Linkify.toggle);
if (Conf['Link Title'] && (service = type.title)) {
titles = $.get('CachedTitles', {});
if (title = titles[match[1]]) {
a.textContent = title[0];
embed.setAttribute('data-title', title[0]);
} else {
try {
$.cache(service.api.call(a), callbacks);
} catch (err) {
a.innerHTML = "[" + key + "] <span class=warning>Title Link Blocked</span> (are you using NoScript?)</a>";
}
}
}
return [a, $.tn(' '), embed];
}
return [a];
}
};
QR = { QR = {
init: function() { init: function() {
if (g.VIEW === 'catalog' || !Conf['Quick Reply']) { if (g.VIEW === 'catalog' || !Conf['Quick Reply']) {
@ -7611,6 +7877,7 @@
initFeature('Settings', Settings); initFeature('Settings', Settings);
initFeature('Fourchan thingies', Fourchan); initFeature('Fourchan thingies', Fourchan);
initFeature('Custom CSS', CustomCSS); initFeature('Custom CSS', CustomCSS);
initFeature('Linkify', Linkify);
initFeature('Resurrect Quotes', Quotify); initFeature('Resurrect Quotes', Quotify);
initFeature('Filter', Filter); initFeature('Filter', Filter);
initFeature('Thread Hiding', ThreadHiding); initFeature('Thread Hiding', ThreadHiding);

View File

@ -127,9 +127,13 @@ $.extend $,
$.asap (-> d.head), -> $.asap (-> d.head), ->
$.add d.head, style $.add d.head, style
style style
x: (path, root=d.body) -> x: (path, root) ->
root or= d.body
# XPathResult.ANY_UNORDERED_NODE_TYPE === 8 # XPathResult.ANY_UNORDERED_NODE_TYPE === 8
d.evaluate(path, root, null, 8, null).singleNodeValue d.evaluate(path, root, null, 8, null).singleNodeValue
X: (path, root) ->
root or= d.body
d.evaluate path, root, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null
addClass: (el, className) -> addClass: (el, className) ->
el.classList.add className el.classList.add className
rmClass: (el, className) -> rmClass: (el, className) ->
@ -140,10 +144,12 @@ $.extend $,
el.parentNode.removeChild el el.parentNode.removeChild el
tn: (s) -> tn: (s) ->
d.createTextNode s d.createTextNode s
frag: ->
d.createDocumentFragment()
nodes: (nodes) -> nodes: (nodes) ->
unless nodes instanceof Array unless nodes instanceof Array
return nodes return nodes
frag = d.createDocumentFragment() frag = $.frag()
for node in nodes for node in nodes
frag.appendChild node frag.appendChild node
frag frag

View File

@ -50,6 +50,20 @@ Config =
'Check for updated versions of <%= meta.name %>.' 'Check for updated versions of <%= meta.name %>.'
] ]
'Linkification':
'Linkify': [
true
'Convert text into links where applicable.'
]
'Embedding': [
true
'Embed supported services.'
]
'Link Title': [
true
'Replace the link of a supported site with its actual title. Currently Supported: YouTube, Vimeo, SoundCloud'
]
'Filtering': 'Filtering':
'Anonymize': [ 'Anonymize': [
false false

View File

@ -4096,3 +4096,244 @@ ThreadWatcher =
textContent: Get.threadExcerpt thread textContent: Get.threadExcerpt thread
ThreadWatcher.refresh watched ThreadWatcher.refresh watched
$.set 'WatchedThreads', watched $.set 'WatchedThreads', watched
Linkify =
init: ->
return if g.VIEW is 'catalog' or not Conf['Linkify']
Post::callbacks.push
name: 'Linkify'
cb: @node
regString: ///(
\b(
[a-z]+://
|
[a-z]{3,}\.[-a-z0-9]+\.[a-z]+
|
[-a-z0-9]+\.[a-z]
|
[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+
|
[a-z]{3,}:[a-z0-9?]
|
[a-z0-9._%+-:]+@[a-z0-9.-]+\.[a-z0-9]
)
[^\s'"]+
)///gi
cypher: $.el 'div'
node: ->
if @isClone and Conf['Embedding']
for embedder in $$ '.embedder', @nodes.comment
$.on embedder, Linkify.toggle
return
snapshot = $.X './/text()', @nodes.comment
cypher = Linkify.cypher
i = -1
len = snapshot.snapshotLength
while ++i < len
nodes = $.frag()
node = snapshot.snapshotItem i
data = node.data
# Test for valid links
continue unless node.parentNode and Linkify.regString.test data
Linkify.regString.lastIndex = 0
cypherText = []
if next = node.nextSibling
cypher.textContent = node.textContent
cypherText[0] = cypher.innerHTML
while (next.nodeName.toLowerCase() is 'wbr' or next.nodeName.toLowerCase() is 's') and (lookahead = next.nextSibling) and ((name = lookahead.nodeName) is "#text" or name.toLowerCase() is 'br')
cypher.textContent = lookahead.textContent
cypherText.push if spoiler = next.innerHTML then "<s>#{spoiler.replace /</g, ' <'}</s>" else '<wbr>'
cypherText.push cypher.innerHTML
$.rm next
next = lookahead.nextSibling
$.rm lookahead if lookahead.nodeName is "#text"
unless next
break
if cypherText.length
data = cypherText.join ''
links = data.match Linkify.regString
for link in links
index = data.indexOf link
if text = data[...index]
# press button get bacon
cypher.innerHTML = text
for child in [cypher.childNodes...]
$.add nodes, child
cypher.innerHTML = (if link.indexOf(':') < 0 then (if link.indexOf('@') > 0 then 'mailto:' + link else 'http://' + link) else link).replace /<(wbr|s|\/s)>/g, ''
a = $.el 'a',
innerHTML: link
className: 'linkify'
rel: 'nofollow noreferrer'
target: '_blank'
href: cypher.textContent
$.add nodes, Linkify.embedder a
data = data[index + link.length..]
if data
# Potential text after the last valid link.
cypher.innerHTML = data
# Convert <wbr> into elements
for child in [cypher.childNodes...]
$.add nodes, child
$.replace node, nodes
toggle: ->
# We setup the link to be replaced by the embedded video
embed = @previousElementSibling
# Unembed.
if @className.contains "embedded"
# Recreate the original link.
el = $.el 'a',
rel: 'nofollow noreferrer'
target: 'blank'
className: 'linkify'
href: url = @getAttribute("data-originalURL")
textContent: @getAttribute("data-title") or url
@textContent = '(embed)'
# Embed
else
# We create an element to embed
el = (type = Linkify.types[@getAttribute("data-service")]).el.call @
# Set style values.
el.style.cssText = if style = type.style
style
else
"border: 0; width: #{$.get 'embedWidth', Config['embedWidth']}px; height: #{$.get 'embedHeight', Config['embedHeight']}px"
@textContent = '(unembed)'
$.replace embed, el
$.toggleClass @, 'embedded'
types:
YouTube:
regExp: /.*(?:youtu.be\/|youtube.*v=|youtube.*\/embed\/|youtube.*\/v\/|youtube.*videos\/)([^#\&\?]*).*/
el: ->
$.el 'iframe',
src: "//www.youtube.com/embed/#{@name}"
title:
api: -> "https://gdata.youtube.com/feeds/api/videos/#{@name}?alt=json&fields=title/text(),yt:noembed,app:control/yt:state/@reasonCode"
text: -> JSON.parse(@responseText).entry.title.$t
Vocaroo:
regExp: /.*(?:vocaroo.com\/)([^#\&\?]*).*/
style: 'border: 0; width: 150px; height: 45px;'
el: ->
$.el 'object',
innerHTML: "<embed src='http://vocaroo.com/player.swf?playMediaID=#{@name.replace /^i\//, ''}&autoplay=0' width='150' height='45' pluginspage='http://get.adobe.com/flashplayer/' type='application/x-shockwave-flash'></embed>"
Vimeo:
regExp: /.*(?:vimeo.com\/)([^#\&\?]*).*/
el: ->
$.el 'iframe',
src: "//player.vimeo.com/video/#{@name}"
title:
api: -> "https://vimeo.com/api/oembed.json?url=http://vimeo.com/#{@name}"
text: -> JSON.parse(@responseText).title
LiveLeak:
regExp: /.*(?:liveleak.com\/view.+i=)([0-9a-z_]+)/
el: ->
$.el 'iframe',
src: "http://www.liveleak.com/e/#{@name}?autostart=true"
audio:
regExp: /(.*\.(mp3|ogg|wav))$/
el: ->
$.el 'audio',
controls: 'controls'
preload: 'auto'
src: @name
SoundCloud:
regExp: /.*(?:soundcloud.com\/|snd.sc\/)([^#\&\?]*).*/
el: ->
div = $.el 'div',
className: "soundcloud"
name: "soundcloud"
$.ajax(
"//soundcloud.com/oembed?show_artwork=false&&maxwidth=500px&show_comments=false&format=json&url=#{@getAttribute 'data-originalURL'}&color=#{Style.colorToHex Themes[Conf['theme']]['Background Color']}"
div: div
onloadend: ->
@div.innerHTML = JSON.parse(@responseText).html
false)
pastebin:
regExp: /.*(?:pastebin.com\/)([^#\&\?]*).*/
el: ->
div = $.el 'iframe',
src: "http://pastebin.com/embed_iframe.php?i=#{@name}"
embedder: (a) ->
return [a] unless Conf['Embedding']
callbacks = ->
a.textContent = switch @status
when 200, 304
title = "[#{embed.getAttribute 'data-service'}] #{service.text.call @}"
embed.setAttribute 'data-title', title
titles[embed.name] = [title, Date.now()]
$.set 'CachedTitles', titles
title
when 404
"[#{key}] Not Found"
when 403
"[#{key}] Forbidden or Private"
else
"[#{key}] #{@status}'d"
for key, type of Linkify.types
continue unless match = a.href.match type.regExp
embed = $.el 'a',
name: (a.name = match[1])
className: 'embedder'
href: 'javascript:;'
textContent: '(embed)'
embed.setAttribute 'data-service', key
embed.setAttribute 'data-originalURL', a.href
$.on embed, 'click', Linkify.toggle
if Conf['Link Title'] and (service = type.title)
titles = $.get 'CachedTitles', {}
if title = titles[match[1]]
a.textContent = title[0]
embed.setAttribute 'data-title', title[0]
else
try
$.cache service.api.call(a), callbacks
catch err
a.innerHTML = "[#{key}] <span class=warning>Title Link Blocked</span> (are you using NoScript?)</a>"
return [a, $.tn(' '), embed]
return [a]

View File

@ -341,6 +341,7 @@ Main =
initFeature 'Settings', Settings initFeature 'Settings', Settings
initFeature 'Fourchan thingies', Fourchan initFeature 'Fourchan thingies', Fourchan
initFeature 'Custom CSS', CustomCSS initFeature 'Custom CSS', CustomCSS
initFeature 'Linkify', Linkify
initFeature 'Resurrect Quotes', Quotify initFeature 'Resurrect Quotes', Quotify
initFeature 'Filter', Filter initFeature 'Filter', Filter
initFeature 'Thread Hiding', ThreadHiding initFeature 'Thread Hiding', ThreadHiding