better abstraction of user styles

This commit is contained in:
gorhill 2016-12-16 16:25:36 -05:00
parent 34b359aa92
commit c39adacc50
4 changed files with 82 additions and 126 deletions

View File

@ -66,6 +66,17 @@
Additionally, the domSurveyor can turn itself off once it decides that Additionally, the domSurveyor can turn itself off once it decides that
it has become pointless (repeatedly not finding new cosmetic filters). it has become pointless (repeatedly not finding new cosmetic filters).
The domFilterer makes use of platform-dependent user styles[1] code, or
provide a default generic implementation if none is present.
At time of writing, only modern Firefox provides a custom implementation,
which makes for solid, reliable and low overhead cosmetic filtering on
Firefox.
The generic implementation[2] performs as best as can be, but won't ever be
as reliable as real user styles.
[1] "user styles" refer to local CSS rules which have priority over, and
can't be overriden by a web page's own CSS rules.
[2] below, see platformUserCSS / platformHideNode / platformUnhideNode
*/ */
/******************************************************************************/ /******************************************************************************/
@ -138,8 +149,7 @@ var reParserEx = /:(?:has|matches-css|matches-css-before|matches-css-after|style
var allExceptions = createSet(), var allExceptions = createSet(),
allSelectors = createSet(), allSelectors = createSet(),
stagedNodes = [], stagedNodes = [],
matchesProp = vAPI.matchesProp, matchesProp = vAPI.matchesProp;
userCSS = vAPI.userCSS;
// Complex selectors, due to their nature may need to be "de-committed". A // Complex selectors, due to their nature may need to be "de-committed". A
// Set() is used to implement this functionality. // Set() is used to implement this functionality.
@ -161,6 +171,62 @@ var cosmeticFiltersActivated = function() {
/******************************************************************************/ /******************************************************************************/
// If a platform does not support its own vAPI.userCSS (user styles), we
// provide a default (imperfect) implementation.
// Probably no longer need to watch for style tags removal/tampering with fix
// to https://github.com/gorhill/uBlock/issues/963
var platformUserCSS = (function() {
if ( vAPI.userCSS instanceof Object ) {
return vAPI.userCSS;
}
return {
enabled: true,
styles: [],
add: function(css) {
var style = document.createElement('style');
style.setAttribute('type', 'text/css');
style.textContent = css;
if ( document.head ) {
document.head.appendChild(style);
}
this.styles.push(style);
if ( style.sheet ) {
style.sheet.disabled = !this.enabled;
}
},
remove: function(css) {
var i = this.styles.length,
style, parent;
while ( i-- ) {
style = this.styles[i];
if ( style.textContent !== css ) { continue; }
parent = style.parentNode;
if ( parent !== null ) {
parent.removeChild(style);
}
this.styles.splice(i, 1);
}
},
toggle: function(state) {
if ( this.styles.length === '' ) { return; }
if ( state === undefined ) {
state = !this.enabled;
}
var i = this.styles.length, style;
while ( i-- ) {
style = this.styles[i];
if ( style.sheet !== null ) {
style.sheet.disabled = !state;
}
}
this.enabled = state;
}
};
})();
// If a platform does not provide its own (improved) vAPI.hideNode, we assign // If a platform does not provide its own (improved) vAPI.hideNode, we assign
// a default one to try to override author styles as best as can be. // a default one to try to override author styles as best as can be.
@ -344,7 +410,6 @@ var runXpathJob = function(job, fn) {
var domFilterer = { var domFilterer = {
addedNodesHandlerMissCount: 0, addedNodesHandlerMissCount: 0,
removedNodesHandlerMissCount: 0,
commitTimer: null, commitTimer: null,
disabledId: vAPI.randomToken(), disabledId: vAPI.randomToken(),
enabled: true, enabled: true,
@ -428,54 +493,6 @@ var domFilterer = {
} }
}, },
addStyleTag: function(text) {
var styleTag = document.createElement('style');
styleTag.setAttribute('type', 'text/css');
styleTag.textContent = text;
if ( document.head ) {
document.head.appendChild(styleTag);
}
this.styleTags.push(styleTag);
if ( userCSS ) {
userCSS.add(text);
}
},
checkStyleTags_: function() {
var doc = document,
html = doc.documentElement,
head = doc.head,
newParent = head || html;
if ( newParent === null ) { return; }
this.removedNodesHandlerMissCount += 1;
var styles = this.styleTags,
style, oldParent;
for ( var i = 0; i < styles.length; i++ ) {
style = styles[i];
oldParent = style.parentNode;
// https://github.com/gorhill/uBlock/issues/1031
// If our style tag was disabled, re-insert into the page.
if (
style.disabled &&
oldParent !== null &&
style.hasAttribute(this.disabledId) === false
) {
oldParent.removeChild(style);
oldParent = null;
}
if ( oldParent === head || oldParent === html ) { continue; }
style.disabled = false;
newParent.appendChild(style);
this.removedNodesHandlerMissCount = 0;
}
},
checkStyleTags: function() {
if ( this.removedNodesHandlerMissCount < 16 ) {
this.checkStyleTags_();
}
},
commit_: function() { commit_: function() {
this.commitTimer.clear(); this.commitTimer.clear();
@ -538,7 +555,7 @@ var domFilterer = {
} }
if ( styleText !== '' ) { if ( styleText !== '' ) {
this.addStyleTag(styleText); platformUserCSS.add(styleText);
} }
// Un-hide nodes previously hidden. // Un-hide nodes previously hidden.
@ -623,19 +640,17 @@ var domFilterer = {
}, },
toggleOff: function() { toggleOff: function() {
if ( userCSS ) { platformUserCSS.toggle(false);
userCSS.toggle(false);
}
this.enabled = false; this.enabled = false;
}, },
toggleOn: function() { toggleOn: function() {
if ( userCSS ) { platformUserCSS.toggle(true);
userCSS.toggle(true);
}
this.enabled = true; this.enabled = true;
}, },
userCSS: platformUserCSS,
unhideNode: function(node) { unhideNode: function(node) {
if ( node[this.hiddenId] !== undefined ) { if ( node[this.hiddenId] !== undefined ) {
this.hiddenNodeCount--; this.hiddenNodeCount--;
@ -651,13 +666,8 @@ var domFilterer = {
platformHideNode(node); platformHideNode(node);
}, },
domChangedHandler: function(addedNodes, removedNodes) { domChangedHandler: function(addedNodes) {
this.commit(addedNodes); this.commit(addedNodes);
// https://github.com/gorhill/uBlock/issues/873
// This will ensure our style elements will stay in the DOM.
if ( removedNodes ) {
domFilterer.checkStyleTags();
}
}, },
start: function() { start: function() {
@ -818,9 +828,9 @@ vAPI.domWatcher = (function() {
} }
addedNodeLists.length = 0; addedNodeLists.length = 0;
if ( addedNodes.length !== 0 || removedNodes ) { if ( addedNodes.length !== 0 || removedNodes ) {
listeners[0](addedNodes, removedNodes); listeners[0](addedNodes);
if ( listeners[1] ) { if ( listeners[1] ) {
listeners[1](addedNodes, removedNodes); listeners[1](addedNodes);
} }
addedNodes.length = 0; addedNodes.length = 0;
removedNodes = false; removedNodes = false;
@ -1485,19 +1495,16 @@ vAPI.domSurveyor = (function() {
surveyPhase2(addedNodes); surveyPhase2(addedNodes);
}; };
var domChangedHandler = function(addedNodes, removedNodes) { var domChangedHandler = function(addedNodes) {
if ( cosmeticSurveyingMissCount > 255 ) { if ( cosmeticSurveyingMissCount > 255 ) {
vAPI.domWatcher.removeListener(domChangedHandler); vAPI.domWatcher.removeListener(domChangedHandler);
vAPI.domSurveyor = null; vAPI.domSurveyor = null;
domFilterer.domChangedHandler(addedNodes, removedNodes); domFilterer.domChangedHandler(addedNodes);
domFilterer.start(); domFilterer.start();
return; return;
} }
surveyPhase1(addedNodes); surveyPhase1(addedNodes);
if ( removedNodes ) {
domFilterer.checkStyleTags();
}
}; };
var start = function() { var start = function() {
@ -1535,11 +1542,6 @@ vAPI.domIsLoaded = function(ev) {
vAPI.domCollapser.start(); vAPI.domCollapser.start();
if ( vAPI.domFilterer ) { if ( vAPI.domFilterer ) {
// https://github.com/chrisaljoudi/uBlock/issues/789
// https://github.com/gorhill/uBlock/issues/873
// Be sure our style tags used for cosmetic filtering are still
// applied.
vAPI.domFilterer.checkStyleTags();
// To avoid neddless CPU overhead, we commit existing cosmetic filters // To avoid neddless CPU overhead, we commit existing cosmetic filters
// only if the page loaded "slowly", i.e. if the code here had to wait // only if the page loaded "slowly", i.e. if the code here had to wait
// for a DOMContentLoaded event -- in which case the DOM may have // for a DOMContentLoaded event -- in which case the DOM may have

View File

@ -28,24 +28,12 @@
return; return;
} }
var styles = vAPI.domFilterer.styleTags;
// Disable all cosmetic filtering-related styles from the DOM
var i = styles.length, style;
while ( i-- ) {
style = styles[i];
if ( style.sheet !== null ) {
style.sheet.disabled = true;
style[vAPI.sessionId] = true;
}
}
var elems = []; var elems = [];
try { try {
elems = document.querySelectorAll('[' + vAPI.domFilterer.hiddenId + ']'); elems = document.querySelectorAll('[' + vAPI.domFilterer.hiddenId + ']');
} catch (e) { } catch (e) {
} }
i = elems.length; var i = elems.length;
while ( i-- ) { while ( i-- ) {
vAPI.domFilterer.showNode(elems[i]); vAPI.domFilterer.showNode(elems[i]);
} }

View File

@ -28,24 +28,12 @@
return; return;
} }
// Insert all cosmetic filtering-related style tags in the DOM
var styles = vAPI.domFilterer.styleTags;
var i = styles.length, style;
while ( i-- ) {
style = styles[i];
if ( style.sheet !== null ) {
style.sheet.disabled = false;
style[vAPI.sessionId] = undefined;
}
}
var elems = []; var elems = [];
try { try {
elems = document.querySelectorAll('[' + vAPI.domFilterer.hiddenId + ']'); elems = document.querySelectorAll('[' + vAPI.domFilterer.hiddenId + ']');
} catch (e) { } catch (e) {
} }
i = elems.length; var i = elems.length;
while ( i-- ) { while ( i-- ) {
vAPI.domFilterer.unshowNode(elems[i]); vAPI.domFilterer.unshowNode(elems[i]);
} }

View File

@ -686,10 +686,6 @@ var cosmeticFilterMapper = (function() {
matchesFnName = 'webkitMatchesSelector'; matchesFnName = 'webkitMatchesSelector';
} }
// Why the call to hideNode()?
// Not all target nodes have necessarily been force-hidden,
// do it now so that the inspector does not unhide these
// nodes when disabling style tags.
var nodesFromStyleTag = function(rootNode) { var nodesFromStyleTag = function(rootNode) {
var filterMap = nodeToCosmeticFilterMap, var filterMap = nodeToCosmeticFilterMap,
selectors, selector, selectors, selector,
@ -741,16 +737,7 @@ var cosmeticFilterMapper = (function() {
}; };
var incremental = function(rootNode) { var incremental = function(rootNode) {
var styleTags = vAPI.domFilterer.styleTags || []; vAPI.domFilterer.userCSS.toggle(false);
var styleTag;
var i = styleTags.length;
while ( i-- ) {
styleTag = styleTags[i];
if ( styleTag.sheet !== null ) {
styleTag.sheet.disabled = true;
styleTag[vAPI.sessionId] = true;
}
}
nodesFromStyleTag(rootNode); nodesFromStyleTag(rootNode);
}; };
@ -760,16 +747,7 @@ var cosmeticFilterMapper = (function() {
}; };
var shutdown = function() { var shutdown = function() {
var styleTags = vAPI.domFilterer.styleTags || []; vAPI.domFilterer.userCSS.toggle(true);
var styleTag;
var i = styleTags.length;
while ( i-- ) {
styleTag = styleTags[i];
if ( styleTag.sheet !== null ) {
styleTag.sheet.disabled = false;
styleTag[vAPI.sessionId] = undefined;
}
}
}; };
return { return {