diff --git a/4chan_x.user.js b/4chan_x.user.js
index 775081f63..a66c387ca 100644
--- a/4chan_x.user.js
+++ b/4chan_x.user.js
@@ -90,9 +90,8 @@
},
Filtering: {
'Anonymize': [false, 'Make everybody anonymous'],
- 'Filter': [false, 'Self-moderation placebo'],
- 'Filter OPs': [false, 'Filter OPs along with their threads'],
- 'Recursive Filtering': [false, 'Filter replies of filtered posts, recursively'],
+ 'Filter': [true, 'Self-moderation placebo'],
+ 'Recursive Filtering': [true, 'Filter replies of filtered posts, recursively'],
'Reply Hiding': [true, 'Hide single replies'],
'Thread Hiding': [true, 'Hide entire threads'],
'Show Stubs': [true, 'Of hidden threads / replies']
@@ -532,86 +531,108 @@
};
filter = {
- regexps: {},
- callbacks: [],
+ filters: {},
init: function() {
- var f, filter, key, m, _i, _len;
+ var boards, filter, hl, key, op, regexp, _i, _len, _ref, _ref2, _ref3;
for (key in config.filter) {
- if (!(m = conf[key].match(/^\/.+\/\w*$/gm))) continue;
- this.regexps[key] = [];
- for (_i = 0, _len = m.length; _i < _len; _i++) {
- filter = m[_i];
- f = filter.match(/^\/(.+)\/(\w*)$/);
+ this.filters[key] = [];
+ _ref = conf[key].split('\n');
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ filter = _ref[_i];
+ if (filter[0] === '#') continue;
+ if (!(regexp = filter.match(/\/(.+)\/(\w*)/))) continue;
+ filter = filter.replace(regexp[0], '');
+ boards = ((_ref2 = filter.match(/boards:([^;]+)/)) != null ? _ref2[1].toLowerCase() : void 0) || 'global';
+ if (boards !== 'global' && boards.split(',').indexOf(g.BOARD) === -1) {
+ continue;
+ }
try {
- this.regexps[key].push(RegExp(f[1], f[2]));
+ regexp = RegExp(regexp[1], regexp[2]);
} catch (e) {
alert(e.message);
+ continue;
}
+ op = ((_ref3 = filter.match(/op:(yes|no|only)/)) != null ? _ref3[1].toLowerCase() : void 0) || 'no';
+ hl = /highlight/.test(filter);
+ this.filters[key].push(this.createFilter(regexp, op, hl));
}
- this.callbacks.push(this[key]);
+ if (!this.filters[key].length) delete this.filters[key];
}
- return g.callbacks.push(this.node);
+ if (Object.keys(this.filters).length) return g.callbacks.push(this.node);
+ },
+ createFilter: function(regexp, op, hl) {
+ return function(root, value, isOP) {
+ if (isOP && op === 'no' || op === 'only') return false;
+ if (!regexp.test(value)) return false;
+ if (hl) {
+ $.addClass(root, 'filter_highlight');
+ } else if (isOP) {
+ threadHiding.hideHide(root.parentNode);
+ } else {
+ replyHiding.hideHide(root.previousSibling);
+ }
+ return true;
+ };
},
node: function(root) {
- if (!root.className) {
- if (filter.callbacks.some(function(callback) {
- return callback(root);
- })) {
- return replyHiding.hideHide($('td:not([nowrap])', root));
- }
- } else if (root.className === 'op' && !g.REPLY && conf['Filter OPs']) {
- if (filter.callbacks.some(function(callback) {
- return callback(root);
- })) {
- return threadHiding.hideHide(root.parentNode);
- }
+ var isOP, key, klass;
+ klass = root.className;
+ if (/\binlined\b/.test(klass)) return;
+ if (!(isOP = klass === 'op')) root = $('td[id]', root);
+ for (key in filter.filters) {
+ if (filter.test(root, key, isOP)) return;
}
},
- test: function(key, value) {
- return filter.regexps[key].some(function(regexp) {
- return regexp.test(value);
- });
+ test: function(root, key, isOP) {
+ var filter, value, _i, _len, _ref;
+ value = this[key](root, isOP);
+ if (value === false) return false;
+ _ref = this.filters[key];
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ filter = _ref[_i];
+ if (filter(root, value, isOP)) return true;
+ }
+ return false;
},
- name: function(root) {
+ name: function(root, isOP) {
var name;
- name = root.className === 'op' ? $('.postername', root) : $('.commentpostername', root);
- return filter.test('name', name.textContent);
+ name = isOP ? $('.postername', root) : $('.commentpostername', root);
+ return name.textContent;
},
tripcode: function(root) {
var trip;
- if (trip = $('.postertrip', root)) {
- return filter.test('tripcode', trip.textContent);
- }
+ if (trip = $('.postertrip', root)) return trip.textContent;
+ return false;
},
email: function(root) {
var mail;
- if (mail = $('.linkmail', root)) return filter.test('email', mail.href);
+ if (!(mail = $('.linkmail', root))) return mail.href;
+ return false;
},
- subject: function(root) {
+ subject: function(root, isOP) {
var sub;
- sub = root.className === 'op' ? $('.filetitle', root) : $('.replytitle', root);
- return filter.test('subject', sub.textContent);
+ sub = isOP ? $('.filetitle', root) : $('.replytitle', root);
+ return sub.textContent;
},
comment: function(root) {
- return filter.test('comment', ($.el('a', {
+ return ($.el('a', {
innerHTML: $('blockquote', root).innerHTML.replace(/
/g, '\n')
- })).textContent);
+ })).textContent;
},
filename: function(root) {
var file;
- if (file = $('.filesize span', root)) {
- return filter.test('filename', file.title);
- }
+ if (file = $('.filesize > span', root)) return file.title;
+ return false;
},
filesize: function(root) {
var img;
- if (img = $('img[md5]', root)) return filter.test('filesize', img.alt);
+ if (img = $('img[md5]', root)) return img.alt;
+ return false;
},
md5: function(root) {
var img;
- if (img = $('img[md5]', root)) {
- return filter.test('md5', img.getAttribute('md5'));
- }
+ if (img = $('img[md5]', root)) return img.getAttribute('md5');
+ return false;
}
};
@@ -4032,13 +4053,16 @@ img[md5], img[md5] + img {\
.inlined {\
opacity: .5;\
}\
-.inline td.reply {\
+.inline .reply {\
background-color: rgba(255, 255, 255, 0.15);\
border: 1px solid rgba(128, 128, 128, 0.5);\
}\
.filetitle, .replytitle, .postername, .commentpostername, .postertrip {\
background: none;\
}\
+.filter_highlight {\
+ box-shadow: -5px 0 rgba(255,0,0,0.5);\
+}\
.filtered {\
text-decoration: line-through;\
}\
diff --git a/script.coffee b/script.coffee
index 233be78fc..6cb960952 100644
--- a/script.coffee
+++ b/script.coffee
@@ -13,9 +13,8 @@ config =
'Check for Updates': [true, 'Check for updated versions of 4chan X']
Filtering:
'Anonymize': [false, 'Make everybody anonymous']
- 'Filter': [false, 'Self-moderation placebo']
- 'Filter OPs': [false, 'Filter OPs along with their threads']
- 'Recursive Filtering': [false, 'Filter replies of filtered posts, recursively']
+ 'Filter': [true, 'Self-moderation placebo']
+ 'Recursive Filtering': [true, 'Filter replies of filtered posts, recursively']
'Reply Hiding': [true, 'Hide single replies']
'Thread Hiding': [true, 'Hide entire threads']
'Show Stubs': [true, 'Of hidden threads / replies']
@@ -408,58 +407,113 @@ $$ = (selector, root=d.body) ->
Array::slice.call root.querySelectorAll selector
filter =
- regexps: {}
- callbacks: []
+ filters: {}
init: ->
for key of config.filter
- unless m = conf[key].match /^\/.+\/\w*$/gm
- continue
- @regexps[key] = []
- for filter in m
- f = filter.match /^\/(.+)\/(\w*)$/
- try
- @regexps[key].push RegExp f[1], f[2]
- catch e
- alert e.message
- #only execute what's filterable
- @callbacks.push @[key]
+ @filters[key] = []
+ for filter in conf[key].split('\n')
+ continue if filter[0] is '#'
- g.callbacks.push @node
+ unless regexp = filter.match /\/(.+)\/(\w*)/
+ continue
+
+ # Don't mix up filter flags with the regular expression.
+ filter = filter.replace regexp[0], ''
+
+ # Do not add this filter to the list if it's not a global one
+ # and it's not specifically applicable to the current board.
+ # Defaults to global.
+ boards = filter.match(/boards:([^;]+)/)?[1].toLowerCase() or 'global'
+ if boards isnt 'global' and boards.split(',').indexOf(g.BOARD) is -1
+ continue
+
+ try
+ # Please, don't write silly regular expressions.
+ regexp = RegExp regexp[1], regexp[2]
+ catch e
+ # I warned you, bro.
+ alert e.message
+ continue
+
+ # Filter OPs along with their threads, replies only, or both.
+ # Defaults to replies only.
+ op = filter.match(/op:(yes|no|only)/)?[1].toLowerCase() or 'no'
+
+ # Highlight the post, or hide it.
+ # Defaults to post hiding.
+ hl = /highlight/.test filter
+
+ @filters[key].push @createFilter regexp, op, hl
+
+ # Only execute filter types that contain valid filters.
+ unless @filters[key].length
+ delete @filters[key]
+
+ if Object.keys(@filters).length
+ g.callbacks.push @node
+
+ createFilter: (regexp, op, hl) ->
+ (root, value, isOP) ->
+ if isOP and op is 'no' or op is 'only'
+ return false
+ unless regexp.test value
+ return false
+ if hl
+ $.addClass root, 'filter_highlight'
+ else if isOP
+ threadHiding.hideHide root.parentNode
+ else
+ replyHiding.hideHide root.previousSibling
+ true
node: (root) ->
- unless root.className
- if filter.callbacks.some((callback) -> callback root)
- replyHiding.hideHide $ 'td:not([nowrap])', root
- else if root.className is 'op' and not g.REPLY and conf['Filter OPs']
- if filter.callbacks.some((callback) -> callback root)
- threadHiding.hideHide root.parentNode
+ klass = root.className
+ return if /\binlined\b/.test klass
+ unless isOP = klass is 'op'
+ root = $ 'td[id]', root
+ for key of filter.filters
+ if filter.test root, key, isOP
+ return
- test: (key, value) ->
- filter.regexps[key].some (regexp) -> regexp.test value
+ test: (root, key, isOP) ->
+ value = @[key] root, isOP
+ if value is false
+ # Return if there's nothing to filter (no tripcode for example).
+ return false
- name: (root) ->
- name = if root.className is 'op' then $ '.postername', root else $ '.commentpostername', root
- filter.test 'name', name.textContent
+ for filter in @filters[key]
+ if filter root, value, isOP
+ return true
+ false
+
+ name: (root, isOP) ->
+ name = if isOP then $ '.postername', root else $ '.commentpostername', root
+ name.textContent
tripcode: (root) ->
if trip = $ '.postertrip', root
- filter.test 'tripcode', trip.textContent
+ return trip.textContent
+ false
email: (root) ->
- if mail = $ '.linkmail', root
- filter.test 'email', mail.href
- subject: (root) ->
- sub = if root.className is 'op' then $ '.filetitle', root else $ '.replytitle', root
- filter.test 'subject', sub.textContent
+ unless mail = $ '.linkmail', root
+ return mail.href
+ false
+ subject: (root, isOP) ->
+ sub = if isOP then $ '.filetitle', root else $ '.replytitle', root
+ sub.textContent
comment: (root) ->
- filter.test 'comment', ($.el 'a', innerHTML: $('blockquote', root).innerHTML.replace /
/g, '\n').textContent
+ ($.el 'a', innerHTML: $('blockquote', root).innerHTML.replace /
/g, '\n').textContent
filename: (root) ->
- if file = $ '.filesize span', root
- filter.test 'filename', file.title
+ if file = $ '.filesize > span', root
+ return file.title
+ false
filesize: (root) ->
if img = $ 'img[md5]', root
- filter.test 'filesize', img.alt
+ return img.alt
+ false
md5: (root) ->
if img = $ 'img[md5]', root
- filter.test 'md5', img.getAttribute('md5')
+ return img.getAttribute 'md5'
+ false
strikethroughQuotes =
init: ->
@@ -3306,13 +3360,16 @@ img[md5], img[md5] + img {
.inlined {
opacity: .5;
}
-.inline td.reply {
+.inline .reply {
background-color: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(128, 128, 128, 0.5);
}
.filetitle, .replytitle, .postername, .commentpostername, .postertrip {
background: none;
}
+.filter_highlight {
+ box-shadow: -5px 0 rgba(255,0,0,0.5);
+}
.filtered {
text-decoration: line-through;
}