this fixes #145 + refactoring/performance

This commit is contained in:
gorhill 2014-08-12 12:19:54 -04:00
parent fe2f996910
commit c6b7dfabb1
2 changed files with 460 additions and 388 deletions

View File

@ -176,14 +176,11 @@ var FilterParser = function() {
this.prefix = '';
this.suffix = '';
this.anchor = 0;
this.filterType = '#';
this.unhide = 0;
this.hostnames = [];
this.invalid = false;
this.unsupported = false;
this.reParser = /^\s*([^#]*)(##|#@#)(.+)\s*$/;
this.rePlain = /^([#.][\w-]+)/;
this.rePlainMore = /^[#.][\w-]+[^\w-]/;
this.reElement = /^[a-z]/i;
};
/******************************************************************************/
@ -193,8 +190,8 @@ FilterParser.prototype.reset = function() {
this.prefix = '';
this.suffix = '';
this.anchor = '';
this.filterType = '#';
this.hostnames = [];
this.unhide = 0;
this.hostnames.length = 0;
this.invalid = false;
return this;
};
@ -221,12 +218,19 @@ FilterParser.prototype.parse = function(s) {
// https://github.com/gorhill/httpswitchboard/issues/260
// Any sequence of `#` longer than one means the line is not a valid
// cosmetic filter.
if ( this.suffix.indexOf('##') >= 0 ) {
if ( this.suffix.indexOf('##') !== -1 ) {
this.invalid = true;
return this;
}
this.filterType = this.anchor.charAt(1);
// Normalize high-medium selectors: `href` is assumed to imply `a` tag. We
// need to do this here in order to correctly avoid duplicates. The test
// is designed to minimize overhead -- this is a low occurrence filter.
if ( this.suffix.charAt(1) === '[' && this.suffix.slice(2, 9) === 'href^="' ) {
this.suffix = this.suffix.slice(1);
}
this.unhide = this.anchor.charAt(1) === '@' ? 1 : 0;
if ( this.prefix !== '' ) {
this.hostnames = this.prefix.split(/\s*,\s*/);
}
@ -234,30 +238,79 @@ FilterParser.prototype.parse = function(s) {
};
/******************************************************************************/
FilterParser.prototype.isPlainMore = function() {
return this.rePlainMore.test(this.suffix);
};
/******************************************************************************/
FilterParser.prototype.isElement = function() {
return this.reElement.test(this.suffix);
};
// Two Unicode characters:
// T0HHHHHHH HHHHHHHHH
// | | |
// | | |
// | | |
// | | +-- bit 8-0 of FNV
// | |
// | +-- bit 15-9 of FNV
// |
// +-- filter type (0=hide 1=unhide)
//
/******************************************************************************/
FilterParser.prototype.extractPlain = function() {
var matches = this.rePlain.exec(this.suffix);
if ( matches && matches.length === 2 ) {
return matches[1];
var makeHash = function(unhide, token, mask) {
// Ref: Given a URL, returns a unique 4-character long hash string
// Based on: FNV32a
// http://www.isthe.com/chongo/tech/comp/fnv/index.html#FNV-reference-source
// The rest is custom, suited for µBlock.
var i1 = token.length;
var i2 = i1 >> 1;
var i4 = i1 >> 2;
var i8 = i1 >> 3;
var hval = (0x811c9dc5 ^ token.charCodeAt(0)) >>> 0;
hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24);
hval >>>= 0;
hval ^= token.charCodeAt(i8);
hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24);
hval >>>= 0;
hval ^= token.charCodeAt(i4);
hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24);
hval >>>= 0;
hval ^= token.charCodeAt(i4+i8);
hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24);
hval >>>= 0;
hval ^= token.charCodeAt(i2);
hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24);
hval >>>= 0;
hval ^= token.charCodeAt(i2+i8);
hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24);
hval >>>= 0;
hval ^= token.charCodeAt(i2+i4);
hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24);
hval >>>= 0;
hval ^= token.charCodeAt(i1-1);
hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24);
hval >>>= 0;
hval &= mask;
if ( unhide !== 0 ) {
hval |= 0x20000;
}
return '';
return String.fromCharCode(hval >>> 9, hval & 0x1FF);
};
/******************************************************************************/
/******************************************************************************/
// Cosmetic filter family tree:
//
// Generic
// Low generic simple: class or id only
// Low generic complex: class or id + extra stuff after
// High generic:
// High-low generic: [alt="..."],[title="..."]
// High-medium generic: [href^="..."]
// High-high generic: everything else
// Specific
// Specfic hostname
// Specific entity
//
// Generic filters can only be enforced once the main document is loaded.
// Specific filers can be enforced before the main document is loaded.
var FilterContainer = function() {
this.filterParser = new FilterParser();
this.reset();
@ -271,25 +324,44 @@ FilterContainer.prototype.reset = function() {
this.filterParser.reset();
this.frozen = false;
this.acceptedCount = 0;
this.processedCount = 0;
this.duplicateCount = 0;
this.domainHashMask = (1 << 10) - 1;
this.genericHashMask = (1 << 15) - 1;
this.genericFilters = {};
// temporary (at parse time)
this.lowGenericHide = {};
this.lowGenericDonthide = {};
this.highGenericHide = {};
this.highGenericDonthide = {};
this.hostnameHide = {};
this.hostnameDonthide = {};
this.hostnameFilters = {};
this.entityHide = {};
this.entityDonthide = {};
// permanent
// [class], [id]
this.lowGenericFilters = {};
// [alt="..."], [title="..."]
this.highLowGenericHide = {};
this.highLowGenericDonthide = {};
this.highLowGenericHideCount = 0;
this.highLowGenericDonthideCount = 0;
// a[href^="http..."]
this.highMediumGenericHide = {};
this.highMediumGenericDonthide = {};
this.highMediumGenericHideCount = 0;
this.highMediumGenericDonthideCount = 0;
// everything else
this.highHighGenericHide = [];
this.highHighGenericDonthide = [];
this.highHighGenericHideCount = 0;
this.highHighGenericDonthideCount = 0;
this.hostnameFilters = {};
this.entityFilters = {};
this.hideUnfiltered = [];
this.hideLowGenerics = {};
this.hideHighGenerics = [];
this.donthideUnfiltered = [];
this.donthideLowGenerics = {};
this.donthideHighGenerics = [];
this.rejected = [];
this.duplicates = {};
this.duplicateCount = 0;
};
/******************************************************************************/
@ -301,43 +373,132 @@ FilterContainer.prototype.add = function(s) {
return false;
}
this.processedCount += 1;
//if ( s === 'mail.google.com##.nH.adC > .nH > .nH > .u5 > .azN' ) {
// debugger;
//}
// hostname-based filters: with a hostname, narrowing is good enough, no
// need to further narrow.
if ( parsed.hostnames.length ) {
return this.addSpecificFilter(parsed);
}
if ( this.duplicates[s] ) {
this.duplicateCount++;
return false;
}
this.duplicates[s] = true;
// no specific hostname, narrow using class or id.
var selectorType = parsed.suffix.charAt(0);
if ( selectorType === '#' || selectorType === '.' ) {
return this.addPlainFilter(parsed);
}
// no specific hostname, no class, no id.
if ( parsed.filterType === '#' ) {
this.hideUnfiltered.push(parsed.suffix);
var hostnames = parsed.hostnames;
var i = hostnames.length;
if ( i === 0 ) {
this.addGenericSelector(parsed);
} else {
this.donthideUnfiltered.push(parsed.suffix);
while ( i-- ) {
this.addSpecificSelector(hostnames[i], parsed);
}
}
this.acceptedCount += 1;
return true;
};
/******************************************************************************/
FilterContainer.prototype.addGenericSelector = function(parsed) {
var entries;
var selectorType = parsed.suffix.charAt(0);
if ( selectorType === '#' || selectorType === '.' ) {
entries = parsed.unhide === 0 ?
this.lowGenericHide :
this.lowGenericDonthide;
} else {
entries = parsed.unhide === 0 ?
this.highGenericHide :
this.highGenericDonthide;
}
if ( entries[parsed.suffix] === undefined ) {
entries[parsed.suffix] = true;
this.acceptedCount += 1;
} else {
this.duplicateCount += 1;
}
return true;
};
/******************************************************************************/
FilterContainer.prototype.addSpecificSelector = function(hostname, parsed) {
// rhill 2014-07-13: new filter class: entity.
if ( hostname.slice(-2) === '.*' ) {
this.addEntitySelector(hostname, parsed);
} else {
this.addHostnameSelector(hostname, parsed);
}
};
/******************************************************************************/
FilterContainer.prototype.addHostnameSelector = function(hostname, parsed) {
// https://github.com/gorhill/uBlock/issues/145
var unhide = parsed.unhide;
if ( hostname.charAt(0) === '~' ) {
this.addGenericSelector(parsed);
hostname = hostname.slice(1);
unhide ^= 1;
}
var entries = unhide === 0 ?
this.hostnameHide :
this.hostnameDonthide;
var entry = entries[hostname];
if ( entry === undefined ) {
entry = entries[hostname] = {};
entry[parsed.suffix] = true;
this.acceptedCount += 1;
} else if ( entry[parsed.suffix] === undefined ) {
entry[parsed.suffix] = true;
this.acceptedCount += 1;
} else {
this.duplicateCount += 1;
}
};
/******************************************************************************/
FilterContainer.prototype.addEntitySelector = function(hostname, parsed) {
var entries = parsed.unhide === 0 ?
this.entityHide :
this.entityDonthide;
var entity = hostname.slice(0, -2);
var entry = entries[entity];
if ( entry === undefined ) {
entry = entries[entity] = {};
entry[parsed.suffix] = true;
this.acceptedCount += 1;
} else if ( entry[parsed.suffix] === undefined ) {
entry[parsed.suffix] = true;
this.acceptedCount += 1;
} else {
this.duplicateCount += 1;
}
};
/******************************************************************************/
FilterContainer.prototype.freezeLowGenerics = function(what, type) {
var selectors = this[what];
var matches, selectorPrefix, f, hash, bucket;
for ( var selector in selectors ) {
if ( selectors.hasOwnProperty(selector) === false ) {
continue;
}
matches = this.rePlainSelector.exec(selector);
if ( !matches ) {
continue;
}
selectorPrefix = matches[1];
f = selectorPrefix === selector ?
new FilterPlain(selector) :
new FilterPlainMore(selector);
hash = makeHash(type, selectorPrefix, this.genericHashMask);
bucket = this.lowGenericFilters[hash];
if ( bucket === undefined ) {
this.lowGenericFilters[hash] = f;
} else if ( bucket instanceof FilterBucket ) {
bucket.add(f);
} else {
this.lowGenericFilters[hash] = new FilterBucket(bucket, f);
}
}
this[what] = {};
};
FilterContainer.prototype.rePlainSelector = /^([#.][\w-]+)/;
/******************************************************************************/
FilterContainer.prototype.freezeHostnameSpecifics = function(what, type) {
var µburi = µb.URI;
var entries = this[what];
@ -387,215 +548,72 @@ FilterContainer.prototype.freezeEntitySpecifics = function(what, type) {
/******************************************************************************/
FilterContainer.prototype.freezeGenerics = function(what) {
var selectors = this[what + 'Unfiltered'];
//console.log('%d highly generic selectors:\n', selectors.length, selectors.sort().join('\n'));
FilterContainer.prototype.freezeHighGenerics = function(what) {
var selectors = this['highGeneric' + what];
// ["title"] and ["alt"] will be sorted out manually, these are the most
// common generic selectors, aka "low generics". The rest will be put in
// the high genericity bin.
var lowGenerics = {};
var lowGenericCount = 0;
var re = /^(([a-z]*)\[(alt|title)="([^"]+)"\])$/;
var i = selectors.length;
var selector, matches;
while ( i-- ) {
selector = selectors[i];
matches = re.exec(selector);
if ( !matches ) {
// ["title"] and ["alt"] will go in high-low generic bin.
// [href^="..."] wil go in high-mdium generic bin.
// The rest will be put in the high-high generic bin.
var highLowGeneric = {};
var highLowGenericCount = 0;
var highMediumGeneric = {};
var highMediumGenericCount = 0;
var highHighGeneric = [];
var reHighLow = /^[a-z]*(\[(?:alt|title)="[^"]+"\])$/;
var reHighMedium = /^\[href\^="https?:\/\/([^"]{8})[^"]*"\]$/;
var matches, hash;
for ( var selector in selectors ) {
if ( selectors.hasOwnProperty(selector) === false ) {
continue;
}
lowGenerics[matches[1]] = true;
lowGenericCount++;
selectors.splice(i, 1);
matches = reHighLow.exec(selector);
if ( matches && matches.length === 2 ) {
highLowGeneric[matches[1]] = true;
highLowGenericCount += 1;
continue;
}
// Chunksize is a compromise between number of selectors per chunk (the
// number of selectors querySelector() will have to deal with), and the
// number of chunks (the number of times querySelector() will have to
// be called.)
// Benchmarking shows this is a hot spot performance-wise for "heavy"
// sites (like say, Sports Illustrated, good test case). Not clear what
// better can be done at this point, I doubt javascript-side code can beat
// querySelector().
var chunkSize = Math.max(selectors.length >>> 3, 8);
var chunkified = [], chunk;
for (;;) {
chunk = selectors.splice(0, chunkSize);
if ( chunk.length === 0 ) {
break;
matches = reHighMedium.exec(selector);
if ( matches && matches.length === 2 ) {
hash = matches[1];
if ( highMediumGeneric[hash] === undefined ) {
highMediumGeneric[hash] = matches[0];
} else {
highMediumGeneric[hash] += ',\n' + matches[0];
}
chunkified.push(chunk.join(','));
highMediumGenericCount += 1;
continue;
}
this[what + 'LowGenerics'] = lowGenerics;
this[what + 'LowGenericCount'] = lowGenericCount;
this[what + 'HighGenerics'] = chunkified;
this[what + 'Unfiltered'] = [];
highHighGeneric.push(selector);
}
this['highLowGeneric' + what] = highLowGeneric;
this['highLowGeneric' + what + 'Count'] = highLowGenericCount;
this['highMediumGeneric' + what] = highMediumGeneric;
this['highMediumGeneric' + what + 'Count'] = highMediumGenericCount;
this['highHighGeneric' + what] = highHighGeneric.join(',\n');
this['highHighGeneric' + what + 'Count'] = highHighGeneric.length;
this['highGeneric' + what] = {};
};
/******************************************************************************/
FilterContainer.prototype.freeze = function() {
this.freezeHostnameSpecifics('hostnameHide', '#');
this.freezeHostnameSpecifics('hostnameDonthide', '@');
this.freezeEntitySpecifics('entityHide', '#');
this.freezeEntitySpecifics('entityDonthide', '@');
this.freezeGenerics('hide');
this.freezeGenerics('donthide');
this.freezeLowGenerics('lowGenericHide', 0);
this.freezeLowGenerics('lowGenericDonthide', 1);
this.freezeHighGenerics('Hide');
this.freezeHighGenerics('Donthide');
this.freezeHostnameSpecifics('hostnameHide', 0);
this.freezeHostnameSpecifics('hostnameDonthide', 1);
this.freezeEntitySpecifics('entityHide', 0);
this.freezeEntitySpecifics('entityDonthide', 1);
this.filterParser.reset();
// console.debug('Number of duplicate cosmetic filters skipped:', this.duplicateCount);
this.duplicates = {};
this.frozen = true;
//histogram('genericFilters', this.genericFilters);
//histogram('lowGenericFilters', this.lowGenericFilters);
//histogram('hostnameFilters', this.hostnameFilters);
};
/******************************************************************************/
// Two Unicode characters:
// T0HHHHHHH HHHHHHHHH
// | | |
// | | |
// | | |
// | | +-- bit 8-0 of FNV
// | |
// | +-- bit 15-9 of FNV
// |
// +-- filter type (0=hide 1=unhide)
//
var makeHash = function(type, token, mask) {
// Ref: Given a URL, returns a unique 4-character long hash string
// Based on: FNV32a
// http://www.isthe.com/chongo/tech/comp/fnv/index.html#FNV-reference-source
// The rest is custom, suited for µBlock.
var i1 = token.length;
var i2 = i1 >> 1;
var i4 = i1 >> 2;
var i8 = i1 >> 3;
var hval = (0x811c9dc5 ^ token.charCodeAt(0)) >>> 0;
hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24);
hval >>>= 0;
hval ^= token.charCodeAt(i8);
hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24);
hval >>>= 0;
hval ^= token.charCodeAt(i4);
hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24);
hval >>>= 0;
hval ^= token.charCodeAt(i4+i8);
hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24);
hval >>>= 0;
hval ^= token.charCodeAt(i2);
hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24);
hval >>>= 0;
hval ^= token.charCodeAt(i2+i8);
hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24);
hval >>>= 0;
hval ^= token.charCodeAt(i2+i4);
hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24);
hval >>>= 0;
hval ^= token.charCodeAt(i1-1);
hval += (hval<<1) + (hval<<4) + (hval<<7) + (hval<<8) + (hval<<24);
hval >>>= 0;
hval &= mask;
if ( type === '@' ) {
hval |= 0x20000;
}
return String.fromCharCode(hval >>> 9, hval & 0x1FF);
};
/******************************************************************************/
FilterContainer.prototype.addPlainFilter = function(parsed) {
// Verify whether the plain selector is followed by extra selector stuff
if ( parsed.isPlainMore() ) {
return this.addPlainMoreFilter(parsed);
}
var f = new FilterPlain(parsed.suffix);
var hash = makeHash(parsed.filterType, parsed.suffix, this.genericHashMask);
this.addFilterEntry(this.genericFilters, hash, f);
this.acceptedCount += 1;
};
/******************************************************************************/
FilterContainer.prototype.addPlainMoreFilter = function(parsed) {
var selectorSuffix = parsed.extractPlain();
if ( selectorSuffix === '' ) {
return;
}
var f = new FilterPlainMore(parsed.suffix);
var hash = makeHash(parsed.filterType, selectorSuffix, this.genericHashMask);
this.addFilterEntry(this.genericFilters, hash, f);
this.acceptedCount += 1;
};
/******************************************************************************/
FilterContainer.prototype.addHostnameFilter = function(hostname, parsed) {
var entries = parsed.filterType === '#' ?
this.hostnameHide :
this.hostnameDonthide;
var entry = entries[hostname];
if ( entry === undefined ) {
entry = entries[hostname] = {};
}
entry[parsed.suffix] = true;
};
/******************************************************************************/
FilterContainer.prototype.addEntityFilter = function(hostname, parsed) {
var entries = parsed.filterType === '#' ?
this.entityHide :
this.entityDonthide;
var entity = hostname.slice(0, -2);
var entry = entries[entity];
if ( entry === undefined ) {
entry = entries[entity] = {};
}
entry[parsed.suffix] = true;
};
/******************************************************************************/
FilterContainer.prototype.addSpecificFilter = function(parsed) {
var hostnames = parsed.hostnames;
var i = hostnames.length, hostname;
while ( i-- ) {
hostname = hostnames[i];
if ( !hostname ) {
continue;
}
// rhill 2014-07-13: new filter class: entity.
if ( hostname.slice(-2) === '.*' ) {
this.addEntityFilter(hostname, parsed);
} else {
this.addHostnameFilter(hostname, parsed);
}
}
this.acceptedCount += 1;
};
/******************************************************************************/
FilterContainer.prototype.addFilterEntry = function(filterDict, hash, f) {
var bucket = filterDict[hash];
if ( bucket === undefined ) {
filterDict[hash] = f;
} else if ( bucket instanceof FilterBucket ) {
bucket.add(f);
} else {
filterDict[hash] = new FilterBucket(bucket, f);
}
};
/******************************************************************************/
FilterContainer.prototype.retrieveGenericSelectors = function(request) {
if ( µb.userSettings.parseAllABPHideFilters !== true ) {
return;
@ -608,15 +626,26 @@ FilterContainer.prototype.retrieveGenericSelectors = function(request) {
var r = {
hide: [],
donthide: [],
hideLowGenerics: this.hideLowGenerics,
hideLowGenericCount: this.hideLowGenericCount,
hideHighGenerics: this.hideHighGenerics,
donthideLowGenerics: this.donthideLowGenerics,
donthideLowGenericCount: this.donthideLowGenericCount,
donthideHighGenerics: this.donthideHighGenerics
donthide: []
};
if ( request.highGenerics ) {
r.highGenerics = {
hideLow: this.highLowGenericHide,
hideLowCount: this.highLowGenericHideCount,
hideMedium: this.highMediumGenericHide,
hideMediumCount: this.highMediumGenericHideCount,
hideHigh: this.highHighGenericHide,
hideHighCount: this.highHighGenericHideCount,
donthideLow: this.highLowGenericDonthide,
donthideLowCount: this.highLowGenericDonthideCount,
donthideMedium: this.highMediumGenericDonthide,
donthideMediumCount: this.highMediumGenericDonthideCount,
donthideHigh: this.highHighGenericDonthide,
donthideHighCount: this.highHighGenericDonthideCount
};
}
var hash, bucket;
var hashMask = this.genericHashMask;
var hideSelectors = r.hide;
@ -628,8 +657,8 @@ FilterContainer.prototype.retrieveGenericSelectors = function(request) {
if ( !selector ) {
continue;
}
hash = makeHash('#', selector, hashMask);
if ( bucket = this.genericFilters[hash] ) {
hash = makeHash(0, selector, hashMask);
if ( bucket = this.lowGenericFilters[hash] ) {
bucket.retrieve(selector, hideSelectors);
}
}
@ -668,15 +697,15 @@ FilterContainer.prototype.retrieveDomainSelectors = function(request) {
};
var hash, bucket;
hash = makeHash('#', r.domain, this.domainHashMask);
hash = makeHash(0, r.domain, this.domainHashMask);
if ( bucket = this.hostnameFilters[hash] ) {
bucket.retrieve(hostname, r.hide);
}
hash = makeHash('#', r.entity, this.domainHashMask);
hash = makeHash(0, r.entity, this.domainHashMask);
if ( bucket = this.entityFilters[hash] ) {
bucket.retrieve(pos === -1 ? domain : hostname.slice(0, pos - domain.length), r.hide);
}
hash = makeHash('@', r.domain, this.domainHashMask);
hash = makeHash(1, r.domain, this.domainHashMask);
if ( bucket = this.hostnameFilters[hash] ) {
bucket.retrieve(hostname, r.donthide);
}

View File

@ -26,13 +26,9 @@
/******************************************************************************/
/******************************************************************************/
(function() {
/******************************************************************************/
// https://github.com/gorhill/httpswitchboard/issues/345
var messaging = (function(name){
var uBlockMessaging = (function(name){
var port = null;
var dangling = false;
var requestId = 1;
@ -127,10 +123,13 @@ var messaging = (function(name){
// ABP cosmetic filters
(function() {
var messaging = uBlockMessaging;
var queriedSelectors = {};
var injectedSelectors = {};
var classSelectors = null;
var idSelectors = null;
var highGenerics = null;
var contextNodes = [document];
var domLoaded = function() {
// https://github.com/gorhill/uBlock/issues/14
@ -155,46 +154,70 @@ var messaging = (function(name){
if ( idSelectors !== null ) {
selectors = selectors.concat(idSelectors);
}
if ( selectors.length > 0 ) {
if ( selectors.length > 0 || highGenerics === null ) {
//console.log('µBlock> ABP cosmetic filters: retrieving CSS rules using %d selectors', selectors.length);
messaging.ask({
what: 'retrieveGenericCosmeticSelectors',
pageURL: window.location.href,
selectors: selectors
selectors: selectors,
highGenerics: highGenerics === null
},
retrieveHandler
);
} else {
retrieveHandler(null);
}
idSelectors = null;
classSelectors = null;
};
var retrieveHandler = function(selectors) {
if ( !selectors ) {
return;
//console.debug('µBlock> contextNodes = %o', contextNodes);
if ( selectors && selectors.highGenerics ) {
highGenerics = selectors.highGenerics;
}
filterLowGenerics(selectors, 'donthide');
filterHighGenerics(selectors, 'donthide');
if ( selectors.donthide.length ) {
var i = selectors.donthide.length;
while ( i-- ) {
injectedSelectors[selectors.donthide[i]] = true;
if ( selectors && selectors.donthide.length ) {
processLowGenerics(selectors.donthide);
}
if ( highGenerics ) {
if ( highGenerics.donthideLowCount ) {
processHighLowGenerics(highGenerics.donthideLow);
}
if ( highGenerics.donthideMediumCount ) {
processHighMediumGenerics(highGenerics.donthideMedium);
}
}
filterLowGenerics(selectors, 'hide');
filterHighGenerics(selectors, 'hide');
reduce(selectors.hide, injectedSelectors);
if ( selectors.hide.length ) {
applyCSS(selectors.hide, 'display', 'none');
// No such thing as high-high generic exceptions
//if ( highGenerics.donthideHighCount ) {
// processHighHighGenerics(document, highGenerics.donthideHigh);
//}
var hideSelectors = [];
if ( selectors && selectors.hide.length ) {
processLowGenerics(selectors.hide, hideSelectors);
}
if ( highGenerics ) {
if ( highGenerics.hideLowCount ) {
processHighLowGenerics(highGenerics.hideLow, hideSelectors);
}
if ( highGenerics.hideMediumCount ) {
processHighMediumGenerics(highGenerics.hideMedium, hideSelectors);
}
if ( highGenerics.hideHighCount ) {
processHighHighGenerics(highGenerics.hideHigh, hideSelectors);
}
}
if ( hideSelectors.length ) {
applyCSS(hideSelectors, 'display', 'none');
var style = document.createElement('style');
var text = selectors.hide.join(',\n') + ' {display:none !important;}';
var text = hideSelectors.join(',\n') + ' {display:none !important;}';
style.appendChild(document.createTextNode(text));
var parent = document.body || document.documentElement;
if ( parent ) {
parent.appendChild(style);
}
//console.debug('µBlock> generic cosmetic filters: injecting %d CSS rules:', selectors.hide.length, hideStyleText);
//console.debug('µBlock> generic cosmetic filters: injecting %d CSS rules:', hideSelectors.length, text);
}
contextNodes.length = 0;
};
var applyCSS = function(selectors, prop, value) {
@ -208,88 +231,103 @@ var messaging = (function(name){
}
};
var filterTitleGeneric = function(generics, root, out) {
if ( !root.title.length ) {
return;
var selectNodes = function(selector) {
var targetNodes = [];
var i = contextNodes.length;
var node, nodeList, j;
var doc = document;
while ( i-- ) {
node = contextNodes[i];
if ( node === doc ) {
return doc.querySelectorAll(selector);
}
var selector = '[title="' + root.title + '"]';
if ( generics[selector] && !injectedSelectors[selector] ) {
out.push(selector);
targetNodes.push(node);
nodeList = node.querySelectorAll(selector);
j = nodeList.length;
while ( j-- ) {
targetNodes.push(nodeList[j]);
}
selector = root.tagName + selector;
if ( generics[selector] && !injectedSelectors[selector] ) {
out.push(selector);
}
return targetNodes;
};
var filterAltGeneric = function(generics, root, out) {
var alt = root.getAttribute('alt');
if ( !alt || !alt.length ) {
return;
}
var selector = '[alt="' + root.title + '"]';
if ( generics[selector] && !injectedSelectors[selector] ) {
out.push(selector);
}
selector = root.tagName + selector;
if ( generics[selector] && !injectedSelectors[selector] ) {
out.push(selector);
}
};
var filterLowGenerics = function(selectors, what) {
if ( selectors[what + 'LowGenericCount'] === 0 ) {
return;
}
var out = selectors[what];
var generics = selectors[what + 'LowGenerics'];
var nodeList, iNode;
// Low generics: ["title"]
nodeList = document.querySelectorAll('[title]');
iNode = nodeList.length;
while ( iNode-- ) {
filterTitleGeneric(generics, nodeList[iNode], out);
}
// Low generics: ["alt"]
nodeList = document.querySelectorAll('[alt]');
iNode = nodeList.length;
while ( iNode-- ) {
filterAltGeneric(generics, nodeList[iNode], out);
}
};
var filterHighGenerics = function(selectors, what) {
var out = selectors[what];
var generics = selectors[what + 'HighGenerics'];
var iGeneric = generics.length;
var processLowGenerics = function(generics, out) {
var i = generics.length;
var selector;
while ( iGeneric-- ) {
selector = generics[iGeneric];
if ( injectedSelectors[selector] ) {
while ( i-- ) {
selector = generics[i];
if ( injectedSelectors[selector] !== undefined ) {
continue;
}
if ( document.querySelector(selector) !== null ) {
injectedSelectors[selector] = true;
if ( out !== undefined ) {
out.push(selector);
}
}
};
var reduce = function(selectors, dict) {
var i = selectors.length, selector, end;
var processHighLowGenerics = function(generics, out) {
var attrs = ['title', 'alt'];
var attr, attrValue, nodeList, iNode, node, selector;
while ( attr = attrs.pop() ) {
nodeList = selectNodes('[' + attr + ']');
iNode = nodeList.length;
while ( iNode-- ) {
node = nodeList[iNode];
attrValue = node.getAttribute(attr);
if ( !attrValue ) { continue; }
selector = '[' + attr + '="' + attrValue + '"]';
if ( injectedSelectors[selector] === undefined && generics[selector] ) {
injectedSelectors[selector] = true;
if ( out !== undefined ) {
out.push(selector);
}
}
selector = node.tagName.toLowerCase() + selector;
if ( injectedSelectors[selector] === undefined && generics[selector] ) {
injectedSelectors[selector] = true;
if ( out !== undefined ) {
out.push(selector);
}
}
}
}
};
var processHighMediumGenerics = function(generics, out) {
var nodeList = selectNodes('a[href^="http"]');
var iNode = nodeList.length;
var node, href, pos, hash, selector;
while ( iNode-- ) {
node = nodeList[iNode];
href = node.getAttribute('href');
if ( !href ) { continue; }
pos = href.indexOf('://');
if ( pos === -1 ) { continue; }
hash = href.slice(pos + 3, pos + 11);
selector = generics[hash];
if ( selector === undefined ) { continue; }
if ( injectedSelectors[selector] !== undefined ) { continue; }
injectedSelectors[selector] = true;
if ( out !== undefined ) {
out.push(selector);
}
}
};
var processHighHighGenerics = function(generics, out) {
if ( injectedSelectors[generics] !== undefined ) { return; }
if ( document.querySelectorAll(generics) === null ) { return; }
injectedSelectors[generics] = true;
if ( out !== undefined ) {
var selectors = generics.split(',\n');
var i = selectors.length;
while ( i-- ) {
selector = selectors[i];
if ( !dict[selector] ) {
if ( end !== undefined ) {
selectors.splice(i+1, end-i);
end = undefined;
}
dict[selector] = true;
} else if ( end === undefined ) {
end = i;
if ( injectedSelectors[selectors[i]] !== undefined ) {
selectors.splice(i, 1);
}
}
if ( end !== undefined ) {
selectors.splice(0, end+1);
out.push(selectors.join(',\n'));
}
};
@ -362,25 +400,6 @@ var messaging = (function(name){
}
};
var processNodeLists = function(nodeLists) {
var i = nodeLists.length;
var nodeList, j, node;
while ( i-- ) {
nodeList = nodeLists[i];
idsFromNodeList(nodeList);
classesFromNodeList(nodeList);
j = nodeList.length;
while ( j-- ) {
node = nodeList[j];
if ( typeof node.querySelectorAll === 'function' ) {
idsFromNodeList(node.querySelectorAll('[id]'));
classesFromNodeList(node.querySelectorAll('[class]'));
}
}
}
retrieveGenericSelectors();
};
domLoaded();
// Observe changes in the DOM only if...
@ -390,19 +409,41 @@ var messaging = (function(name){
return;
}
var ignoreTags = {
'style': true,
'STYLE': true,
'script': true,
'SCRIPT': true
};
var mutationObservedHandler = function(mutations) {
var iMutation = mutations.length;
var nodeLists = [], nodeList;
var nodes = [];
var nodeList, iNode, node;
while ( iMutation-- ) {
nodeList = mutations[iMutation].addedNodes;
if ( nodeList && nodeList.length ) {
nodeLists.push(nodeList);
if ( !nodeList ) {
continue;
}
iNode = nodeList.length;
while ( iNode-- ) {
node = nodeList[iNode];
if ( typeof node.querySelectorAll !== 'function' ) {
continue;
}
if ( ignoreTags[node.tagName] ) {
continue;
}
contextNodes.push(node);
}
}
if ( nodeLists.length ) {
processNodeLists(nodeLists);
if ( contextNodes.length !== 0 ) {
idsFromNodeList(selectNodes('[id]'));
classesFromNodeList(selectNodes('[class]'));
retrieveGenericSelectors();
}
};
// https://github.com/gorhill/httpswitchboard/issues/176
var observer = new MutationObserver(mutationObservedHandler);
observer.observe(document.body, {
@ -419,6 +460,8 @@ var messaging = (function(name){
// https://github.com/gorhill/uBlock/issues/7
(function() {
var messaging = uBlockMessaging;
var hideOne = function(elem, collapse) {
// If `!important` is not there, going back using history will likely
// cause the hidden element to re-appear.
@ -430,7 +473,7 @@ var messaging = (function(name){
// First pass
messaging.ask({ what: 'blockedRequests' }, function(details) {
var elems = document.querySelectorAll('img,iframe');
var elems = document.querySelectorAll('img,iframe,embed');
var blockedRequests = details.blockedRequests;
var collapse = details.collapse;
var i = elems.length;
@ -453,8 +496,9 @@ var messaging = (function(name){
// - Elements which resource URL changes
var onResourceLoaded = function(ev) {
var target = ev.target;
if ( target.tagName.toLowerCase() !== 'iframe' ) { return; }
//console.debug('Loaded %s[src="%s"]', target.tagName, target.src);
if ( !target || !target.src ) { return; }
if ( target.tagName.toLowerCase() !== 'iframe' ) { return; }
var onAnswerReceived = function(details) {
if ( details.blocked ) {
hideOne(target, details.collapse);
@ -464,8 +508,9 @@ var messaging = (function(name){
};
var onResourceFailed = function(ev) {
var target = ev.target;
if ( target.tagName.toLowerCase() !== 'img' ) { return; }
//console.debug('Failed to load %s[src="%s"]', target.tagName, target.src);
if ( !target || !target.src ) { return; }
if ( target.tagName.toLowerCase() !== 'img' ) { return; }
var onAnswerReceived = function(details) {
if ( details.blocked ) {
hideOne(target, details.collapse);
@ -478,5 +523,3 @@ var messaging = (function(name){
})();
/******************************************************************************/
})();