Add support to chain `:style()` to procedural operators

Related issue:
- https://github.com/uBlockOrigin/uBlock-issues/issues/382

Additionally, remnant code for pseudo-user stylesheets
has been removed. Related commit:
- 5c68867b92
This commit is contained in:
Raymond Hill 2020-09-07 08:28:01 -04:00
parent 3a51ca0002
commit 35aefed926
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
11 changed files with 283 additions and 474 deletions

View File

@ -56,6 +56,16 @@ window.addEventListener('webextFlavor', function() {
/******************************************************************************/
vAPI.randomToken = function() {
const n = Math.random();
return String.fromCharCode(n * 26 + 97) +
Math.floor(
(0.25 + n * 0.75) * Number.MAX_SAFE_INTEGER
).toString(36).slice(-8);
};
/******************************************************************************/
vAPI.app = {
name: manifest.name.replace(/ dev\w+ build/, ''),
version: (( ) => {
@ -339,7 +349,10 @@ vAPI.Tabs = class {
return tabs.length !== 0 ? tabs[0] : null;
}
async insertCSS() {
async insertCSS(tabId, details) {
if ( vAPI.supportsUserStylesheets ) {
details.cssOrigin = 'user';
}
try {
await webext.tabs.insertCSS(...arguments);
}
@ -357,7 +370,10 @@ vAPI.Tabs = class {
return Array.isArray(tabs) ? tabs : [];
}
async removeCSS() {
async removeCSS(tabId, details) {
if ( vAPI.supportsUserStylesheets ) {
details.cssOrigin = 'user';
}
try {
await webext.tabs.removeCSS(...arguments);
}
@ -1003,9 +1019,6 @@ vAPI.messaging = {
frameId: sender.frameId,
matchAboutBlank: true
};
if ( vAPI.supportsUserStylesheets ) {
details.cssOrigin = 'user';
}
if ( msg.add ) {
details.runAt = 'document_start';
}

View File

@ -39,9 +39,11 @@ if (
/******************************************************************************/
vAPI.randomToken = function() {
const now = Date.now();
return String.fromCharCode(now % 26 + 97) +
Math.floor((1 + Math.random()) * now).toString(36);
const n = Math.random();
return String.fromCharCode(n * 26 + 97) +
Math.floor(
(0.25 + n * 0.75) * Number.MAX_SAFE_INTEGER
).toString(36).slice(-8);
};
vAPI.sessionId = vAPI.randomToken();

View File

@ -138,8 +138,8 @@ const µBlock = (( ) => { // jshint ignore:line
// Read-only
systemSettings: {
compiledMagic: 28, // Increase when compiled format changes
selfieMagic: 28, // Increase when selfie format changes
compiledMagic: 29, // Increase when compiled format changes
selfieMagic: 29, // Increase when selfie format changes
},
// https://github.com/uBlockOrigin/uBlock-issues/issues/759#issuecomment-546654501

View File

@ -88,16 +88,8 @@
The domFilterer makes use of platform-dependent user stylesheets[1].
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 and accurate as real user stylesheets.
[1] "user stylesheets" 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
*/
@ -492,6 +484,11 @@ vAPI.injectScriptlet = function(doc, text) {
*/
{
vAPI.hideStyle = 'display:none!important;';
// TODO: Experiment/evaluate loading procedural operator code using an
// on demand approach.
// 'P' stands for 'Procedural'
const PSelectorHasTextTask = class {
@ -562,14 +559,6 @@ vAPI.injectScriptlet = function(doc, text) {
}
};
const PSelectorPassthru = class {
constructor() {
}
transpose(node, output) {
output.push(node);
}
};
const PSelectorSpathTask = class {
constructor(task) {
this.spath = task[1];
@ -701,17 +690,13 @@ vAPI.injectScriptlet = function(doc, text) {
[ ':min-text-length', PSelectorMinTextLengthTask ],
[ ':not', PSelectorIfNotTask ],
[ ':nth-ancestor', PSelectorUpwardTask ],
[ ':remove', PSelectorPassthru ],
[ ':spath', PSelectorSpathTask ],
[ ':upward', PSelectorUpwardTask ],
[ ':watch-attr', PSelectorWatchAttrs ],
[ ':xpath', PSelectorXpathTask ],
]);
}
this.budget = 200; // I arbitrary picked a 1/5 second
this.raw = o.raw;
this.cost = 0;
this.lastAllowanceTime = 0;
this.selector = o.selector;
this.tasks = [];
const tasks = o.tasks;
@ -722,9 +707,6 @@ vAPI.injectScriptlet = function(doc, text) {
);
}
}
if ( o.action !== undefined ) {
this.action = o.action;
}
}
prime(input) {
const root = input || document;
@ -760,10 +742,20 @@ vAPI.injectScriptlet = function(doc, text) {
return false;
}
};
PSelector.prototype.action = undefined;
PSelector.prototype.hit = false;
PSelector.prototype.operatorToTaskMap = undefined;
const PSelectorRoot = class extends PSelector {
constructor(o, styleToken) {
super(o);
this.budget = 200; // I arbitrary picked a 1/5 second
this.raw = o.raw;
this.cost = 0;
this.lastAllowanceTime = 0;
this.styleToken = styleToken;
}
};
PSelectorRoot.prototype.hit = false;
const DOMProceduralFilterer = class {
constructor(domFilterer) {
this.domFilterer = domFilterer;
@ -771,40 +763,48 @@ vAPI.injectScriptlet = function(doc, text) {
this.domIsWatched = false;
this.mustApplySelectors = false;
this.selectors = new Map();
this.hiddenNodes = new Set();
this.masterToken = vAPI.randomToken();
this.styleTokenMap = new Map();
this.styledNodes = new Set();
if ( vAPI.domWatcher instanceof Object ) {
vAPI.domWatcher.addListener(this);
}
}
addProceduralSelectors(aa) {
addProceduralSelectors(selectors) {
const addedSelectors = [];
let mustCommit = this.domIsWatched;
for ( let i = 0, n = aa.length; i < n; i++ ) {
const raw = aa[i];
for ( const raw of selectors ) {
if ( this.selectors.has(raw) ) { continue; }
const o = JSON.parse(raw);
if ( o.action === 'style' ) {
this.domFilterer.addCSSRule(o.selector, o.tasks[0][1]);
mustCommit = true;
continue;
}
if ( o.pseudo !== undefined ) {
this.domFilterer.addCSSRule(
o.selector,
'display:none!important;'
);
this.domFilterer.addCSSRule(o.selector, vAPI.hideStyle);
mustCommit = true;
continue;
}
if ( o.tasks !== undefined ) {
if ( this.selectors.has(raw) === false ) {
const pselector = new PSelector(o);
this.selectors.set(raw, pselector);
addedSelectors.push(pselector);
mustCommit = true;
}
// CSS selector-based styles.
if (
o.action !== undefined &&
o.action[0] === ':style' &&
o.tasks === undefined
) {
this.domFilterer.addCSSRule(o.selector, o.action[1]);
mustCommit = true;
continue;
}
let style, styleToken;
if ( o.action === undefined ) {
style = vAPI.hideStyle;
} else if ( o.action[0] === ':style' ) {
style = o.action[1];
}
if ( style !== undefined ) {
styleToken = this.styleTokenFromStyle(style);
}
const pselector = new PSelectorRoot(o, styleToken);
this.selectors.set(raw, pselector);
addedSelectors.push(pselector);
mustCommit = true;
}
if ( mustCommit === false ) { return; }
this.mustApplySelectors = this.selectors.size !== 0;
@ -828,8 +828,8 @@ vAPI.injectScriptlet = function(doc, text) {
// https://github.com/uBlockOrigin/uBlock-issues/issues/341
// Be ready to unhide nodes which no longer matches any of
// the procedural selectors.
const toRemove = this.hiddenNodes;
this.hiddenNodes = new Set();
const toUnstyle = this.styledNodes;
this.styledNodes = new Set();
let t0 = Date.now();
@ -851,37 +851,54 @@ vAPI.injectScriptlet = function(doc, text) {
t0 = t1;
if ( nodes.length === 0 ) { continue; }
pselector.hit = true;
if ( pselector.action === 'remove' ) {
this.removeNodes(nodes);
} else {
this.hideNodes(nodes);
}
this.styleNodes(nodes, pselector.styleToken);
}
for ( const node of toRemove ) {
if ( this.hiddenNodes.has(node) ) { continue; }
this.domFilterer.unhideNode(node);
}
this.unstyleNodes(toUnstyle);
//console.timeEnd('procedural selectors/dom layout changed');
}
hideNodes(nodes) {
styleTokenFromStyle(style) {
if ( style === undefined ) { return; }
let styleToken = this.styleTokenMap.get(style);
if ( styleToken !== undefined ) { return styleToken; }
styleToken = vAPI.randomToken();
this.styleTokenMap.set(style, styleToken);
this.domFilterer.addCSSRule(
`[${this.masterToken}][${styleToken}]`,
style,
{ silent: true }
);
return styleToken;
}
styleNodes(nodes, styleToken) {
if ( styleToken === undefined ) {
for ( const node of nodes ) {
node.textContent = '';
node.remove();
}
return;
}
for ( const node of nodes ) {
if ( node.parentElement === null ) { continue; }
this.domFilterer.hideNode(node);
this.hiddenNodes.add(node);
node.setAttribute(this.masterToken, '');
node.setAttribute(styleToken, '');
}
}
removeNodes(nodes) {
// TODO: Current assumption is one style per hit element. Could be an
// issue if an element has multiple styling and one styling is
// brough back. Possibly too rare to care about this for now.
unstyleNodes(nodes) {
for ( const node of nodes ) {
node.textContent = '';
node.remove();
if ( this.styledNodes.has(node) ) { continue; }
node.removeAttribute(this.masterToken);
}
}
createProceduralFilter(o) {
return new PSelector(o);
return new PSelectorRoot(o);
}
onDOMCreated() {
@ -908,14 +925,10 @@ vAPI.injectScriptlet = function(doc, text) {
this.disabled = false;
this.listeners = [];
this.filterset = new Set();
this.excludedNodeSet = new WeakSet();
this.addedCSSRules = new Set();
this.exceptedCSSRules = [];
this.reOnlySelectors = /\n\{[^\n]+/g;
this.exceptions = [];
this.proceduralFilterer = null;
this.hideNodeAttr = undefined;
this.hideNodeStyleSheetInjected = false;
// https://github.com/uBlockOrigin/uBlock-issues/issues/167
// By the time the DOMContentLoaded is fired, the content script might
// have been disconnected from the background page. Unclear why this
@ -988,33 +1001,6 @@ vAPI.injectScriptlet = function(doc, text) {
}
}
excludeNode(node) {
this.excludedNodeSet.add(node);
this.unhideNode(node);
}
unexcludeNode(node) {
this.excludedNodeSet.delete(node);
}
hideNode(node) {
if ( this.excludedNodeSet.has(node) ) { return; }
if ( this.hideNodeAttr === undefined ) { return; }
node.setAttribute(this.hideNodeAttr, '');
if ( this.hideNodeStyleSheetInjected ) { return; }
this.hideNodeStyleSheetInjected = true;
this.addCSSRule(
`[${this.hideNodeAttr}]`,
'display:none!important;',
{ silent: true }
);
}
unhideNode(node) {
if ( this.hideNodeAttr === undefined ) { return; }
node.removeAttribute(this.hideNodeAttr);
}
toggle(state, callback) {
if ( state === undefined ) { state = this.disabled; }
if ( state !== this.disabled ) { return; }
@ -1031,24 +1017,6 @@ vAPI.injectScriptlet = function(doc, text) {
userStylesheet.apply(callback);
}
getAllSelectors_(all) {
const out = {
declarative: [],
exceptions: this.exceptedCSSRules,
};
for ( const entry of this.filterset ) {
let selectors = entry.selectors;
if ( all !== true && this.hideNodeAttr !== undefined ) {
selectors = selectors
.replace(`[${this.hideNodeAttr}]`, '')
.replace(/^,\n|,\n$/gm, '');
if ( selectors === '' ) { continue; }
}
out.declarative.push([ selectors, entry.declarations ]);
}
return out;
}
// Here we will deal with:
// - Injecting low priority user styles;
// - Notifying listeners about changed filterset.
@ -1097,7 +1065,7 @@ vAPI.injectScriptlet = function(doc, text) {
}
addProceduralSelectors(aa) {
if ( aa.length === 0 ) { return; }
if ( Array.isArray(aa) === false || aa.length === 0 ) { return; }
this.proceduralFiltererInstance().addProceduralSelectors(aa);
}
@ -1105,25 +1073,39 @@ vAPI.injectScriptlet = function(doc, text) {
return this.proceduralFiltererInstance().createProceduralFilter(o);
}
getAllSelectors() {
const out = this.getAllSelectors_(false);
out.procedural = this.proceduralFilterer instanceof Object
? Array.from(this.proceduralFilterer.selectors.values())
: [];
getAllSelectors(bits = 0) {
const out = {
declarative: [],
exceptions: this.exceptedCSSRules,
};
const hasProcedural = this.proceduralFilterer instanceof Object;
const includePrivateSelectors = (bits & 0b01) !== 0;
const masterToken = hasProcedural
? `[${this.proceduralFilterer.masterToken}]`
: undefined;
for ( const entry of this.filterset ) {
const selectors = entry.selectors;
if (
includePrivateSelectors === false &&
masterToken !== undefined &&
selectors.startsWith(masterToken)
) {
continue;
}
out.declarative.push([ selectors, entry.declarations ]);
}
const excludeProcedurals = (bits & 0b10) !== 0;
if ( excludeProcedurals !== true ) {
out.procedural = hasProcedural
? Array.from(this.proceduralFilterer.selectors.values())
: [];
}
return out;
}
getAllExceptionSelectors() {
return this.exceptions.join(',\n');
}
getFilteredElementCount() {
const details = this.getAllSelectors_(true);
if ( Array.isArray(details.declarative) === false ) { return 0; }
const selectors = details.declarative.map(entry => entry[0]);
if ( selectors.length === 0 ) { return 0; }
return document.querySelectorAll(selectors.join(',\n')).length;
}
};
}
@ -1548,29 +1530,11 @@ vAPI.injectScriptlet = function(doc, text) {
let mustCommit = false;
if ( result ) {
let selectors = result.simple;
if ( Array.isArray(selectors) && selectors.length !== 0 ) {
domFilterer.addCSSRule(
selectors,
'display:none!important;',
{ type: 'simple' }
);
mustCommit = true;
}
selectors = result.complex;
if ( Array.isArray(selectors) && selectors.length !== 0 ) {
domFilterer.addCSSRule(
selectors,
'display:none!important;',
{ type: 'complex' }
);
mustCommit = true;
}
selectors = result.injected;
let selectors = result.injected;
if ( typeof selectors === 'string' && selectors.length !== 0 ) {
domFilterer.addCSSRule(
selectors,
'display:none!important;',
vAPI.hideStyle,
{ injected: true }
);
mustCommit = true;
@ -1740,37 +1704,15 @@ vAPI.injectScriptlet = function(doc, text) {
vAPI.domSurveyor = null;
}
domFilterer.exceptions = cfeDetails.exceptionFilters;
domFilterer.hideNodeAttr = cfeDetails.hideNodeAttr;
domFilterer.hideNodeStyleSheetInjected =
cfeDetails.hideNodeStyleSheetInjected === true;
domFilterer.addCSSRule(
cfeDetails.declarativeFilters,
'display:none!important;'
);
domFilterer.addCSSRule(
cfeDetails.highGenericHideSimple,
'display:none!important;',
{ type: 'simple', lazy: true }
);
domFilterer.addCSSRule(
cfeDetails.highGenericHideComplex,
'display:none!important;',
{ type: 'complex', lazy: true }
);
domFilterer.addCSSRule(
cfeDetails.injectedHideFilters,
'display:none!important;',
vAPI.hideStyle,
{ injected: true }
);
domFilterer.addProceduralSelectors(cfeDetails.proceduralFilters);
domFilterer.exceptCSSRules(cfeDetails.exceptedFilters);
}
if ( cfeDetails.networkFilters.length !== 0 ) {
vAPI.userStylesheet.add(
cfeDetails.networkFilters + '\n{display:none!important;}');
}
vAPI.userStylesheet.apply();
// Library of resources is located at:

View File

@ -32,12 +32,6 @@ const cosmeticSurveyingMissCountMax =
parseInt(vAPI.localStorage.getItem('cosmeticSurveyingMissCountMax'), 10) ||
15;
let supportsUserStylesheets = vAPI.webextFlavor.soup.has('user_stylesheet');
// https://www.reddit.com/r/uBlockOrigin/comments/8dkvqn/116_broken_loading_custom_filters_from_my_filters/
window.addEventListener('webextFlavor', function() {
supportsUserStylesheets = vAPI.webextFlavor.soup.has('user_stylesheet');
}, { once: true });
/******************************************************************************/
/******************************************************************************/
@ -754,9 +748,9 @@ FilterContainer.prototype.triggerSelectorCachePruner = function() {
/******************************************************************************/
FilterContainer.prototype.addToSelectorCache = function(details) {
let hostname = details.hostname;
const hostname = details.hostname;
if ( typeof hostname !== 'string' || hostname === '' ) { return; }
let selectors = details.selectors;
const selectors = details.selectors;
if ( Array.isArray(selectors) === false ) { return; }
let entry = this.selectorCache.get(hostname);
if ( entry === undefined ) {
@ -838,14 +832,6 @@ FilterContainer.prototype.pruneSelectorCacheAsync = function() {
/******************************************************************************/
FilterContainer.prototype.randomAlphaToken = function() {
const now = Date.now();
return String.fromCharCode(now % 26 + 97) +
Math.floor((1 + Math.random()) * now).toString(36);
};
/******************************************************************************/
FilterContainer.prototype.getSession = function() {
return this.sessionFilterDB;
};
@ -912,57 +898,38 @@ FilterContainer.prototype.retrieveGenericSelectors = function(request) {
return;
}
const out = {
simple: Array.from(simpleSelectors),
complex: Array.from(complexSelectors),
injected: '',
excepted,
};
const out = { injected: '', excepted, };
// Important: always clear used registers before leaving.
simpleSelectors.clear();
complexSelectors.clear();
const injected = [];
if ( simpleSelectors.size !== 0 ) {
injected.push(Array.from(simpleSelectors).join(',\n'));
simpleSelectors.clear();
}
if ( complexSelectors.size !== 0 ) {
injected.push(Array.from(complexSelectors).join(',\n'));
complexSelectors.clear();
}
// Cache and inject (if user stylesheets supported) looked-up low generic
// cosmetic filters.
if (
(typeof request.hostname === 'string' && request.hostname !== '') &&
(out.simple.length !== 0 || out.complex.length !== 0)
) {
// Cache and inject looked-up low generic cosmetic filters.
if ( injected.length === 0 ) { return out; }
if ( typeof request.hostname === 'string' && request.hostname !== '' ) {
this.addToSelectorCache({
cost: request.surveyCost || 0,
hostname: request.hostname,
injectedHideFilters: '',
selectors: out.simple.concat(out.complex),
type: 'cosmetic'
selectors: injected,
type: 'cosmetic',
});
}
// If user stylesheets are supported in the current process, inject the
// cosmetic filters now.
if (
supportsUserStylesheets &&
request.tabId !== undefined &&
request.frameId !== undefined
) {
const injected = [];
if ( out.simple.length !== 0 ) {
injected.push(out.simple.join(',\n'));
out.simple = [];
}
if ( out.complex.length !== 0 ) {
injected.push(out.complex.join(',\n'));
out.complex = [];
}
out.injected = injected.join(',\n');
vAPI.tabs.insertCSS(request.tabId, {
code: out.injected + '\n{display:none!important;}',
cssOrigin: 'user',
frameId: request.frameId,
matchAboutBlank: true,
runAt: 'document_start',
});
}
out.injected = injected.join(',\n');
vAPI.tabs.insertCSS(request.tabId, {
code: out.injected + '\n{display:none!important;}',
frameId: request.frameId,
matchAboutBlank: true,
runAt: 'document_start',
});
//console.timeEnd('cosmeticFilteringEngine.retrieveGenericSelectors');
@ -989,18 +956,11 @@ FilterContainer.prototype.retrieveSpecificSelectors = function(
ready: this.frozen,
hostname: hostname,
domain: request.domain,
declarativeFilters: [],
exceptionFilters: [],
exceptedFilters: [],
hideNodeAttr: this.randomAlphaToken(),
hideNodeStyleSheetInjected: false,
highGenericHideSimple: '',
highGenericHideComplex: '',
injectedHideFilters: '',
networkFilters: '',
noDOMSurveying: this.needDOMSurveyor === false,
proceduralFilters: []
};
const injectedHideFilters = [];
if ( options.noCosmeticFiltering !== true ) {
const specificSet = this.$specificSet;
@ -1063,7 +1023,7 @@ FilterContainer.prototype.retrieveSpecificSelectors = function(
}
if ( specificSet.size !== 0 ) {
out.declarativeFilters = Array.from(specificSet);
injectedHideFilters.push(Array.from(specificSet).join(',\n'));
}
if ( proceduralSet.size !== 0 ) {
out.proceduralFilters = Array.from(proceduralSet);
@ -1078,10 +1038,10 @@ FilterContainer.prototype.retrieveSpecificSelectors = function(
// string in memory, which I have observed occurs when the string is
// stored directly as a value in a Map.
if ( options.noGenericCosmeticFiltering !== true ) {
const exceptionHash = out.exceptionFilters.join();
for ( const type in this.highlyGeneric ) {
const entry = this.highlyGeneric[type];
let str = entry.mru.lookup(exceptionHash);
const exceptionSetHash = out.exceptionFilters.join();
for ( const key in this.highlyGeneric ) {
const entry = this.highlyGeneric[key];
let str = entry.mru.lookup(exceptionSetHash);
if ( str === undefined ) {
str = { s: entry.str, excepted: [] };
let genericSet = entry.dict;
@ -1098,13 +1058,14 @@ FilterContainer.prototype.retrieveSpecificSelectors = function(
}
str.s = Array.from(genericSet).join(',\n');
}
entry.mru.add(exceptionHash, str);
entry.mru.add(exceptionSetHash, str);
}
out[entry.canonical] = str.s;
if ( str.excepted.length !== 0 ) {
out.exceptedFilters.push(...str.excepted);
}
if ( str.s.length !== 0 ) {
injectedHideFilters.push(str.s);
}
}
}
@ -1115,55 +1076,27 @@ FilterContainer.prototype.retrieveSpecificSelectors = function(
dummySet.clear();
}
const details = {
code: '',
frameId: request.frameId,
matchAboutBlank: true,
runAt: 'document_start',
};
if ( injectedHideFilters.length !== 0 ) {
out.injectedHideFilters = injectedHideFilters.join(',\n');
details.code = out.injectedHideFilters + '\n{display:none!important;}';
vAPI.tabs.insertCSS(request.tabId, details);
}
// CSS selectors for collapsible blocked elements
if ( cacheEntry ) {
const networkFilters = [];
cacheEntry.retrieve('net', networkFilters);
out.networkFilters = networkFilters.join(',\n');
}
// https://github.com/gorhill/uBlock/issues/3160
// If user stylesheets are supported in the current process, inject the
// cosmetic filters now.
if (
supportsUserStylesheets &&
request.tabId !== undefined &&
request.frameId !== undefined
) {
const injectedHideFilters = [];
if ( out.declarativeFilters.length !== 0 ) {
injectedHideFilters.push(out.declarativeFilters.join(',\n'));
out.declarativeFilters = [];
}
if ( out.proceduralFilters.length !== 0 ) {
injectedHideFilters.push('[' + out.hideNodeAttr + ']');
out.hideNodeStyleSheetInjected = true;
}
if ( out.highGenericHideSimple.length !== 0 ) {
injectedHideFilters.push(out.highGenericHideSimple);
out.highGenericHideSimple = '';
}
if ( out.highGenericHideComplex.length !== 0 ) {
injectedHideFilters.push(out.highGenericHideComplex);
out.highGenericHideComplex = '';
}
out.injectedHideFilters = injectedHideFilters.join(',\n');
const details = {
code: '',
cssOrigin: 'user',
frameId: request.frameId,
matchAboutBlank: true,
runAt: 'document_start',
};
if ( out.injectedHideFilters.length !== 0 ) {
details.code = out.injectedHideFilters + '\n{display:none!important;}';
if ( networkFilters.length !== 0 ) {
details.code = networkFilters.join('\n') + '\n{display:none!important;}';
vAPI.tabs.insertCSS(request.tabId, details);
}
if ( out.networkFilters.length !== 0 ) {
details.code = out.networkFilters + '\n{display:none!important;}';
vAPI.tabs.insertCSS(request.tabId, details);
out.networkFilters = '';
}
}
return out;

View File

@ -56,7 +56,7 @@ if ( epickerId === null ) { return; }
let epickerConnectionId;
let filterHostname = '';
let filterOrigin = '';
let filterResultset = [];
let resultsetOpt;
/******************************************************************************/
@ -88,11 +88,8 @@ const userFilterFromCandidate = function(filter) {
opts.push(`domain=${filterHostname}`);
}
if ( filterResultset.length !== 0 ) {
const item = filterResultset[0];
if ( item.opts ) {
opts.push(item.opts);
}
if ( resultsetOpt !== undefined ) {
opts.push(resultsetOpt);
}
if ( opts.length ) {
@ -632,10 +629,10 @@ const onPickerMessage = function(msg) {
case 'showDialog':
showDialog(msg);
break;
case 'filterResultset': {
filterResultset = msg.resultset;
$id('resultsetCount').textContent = filterResultset.length;
if ( filterResultset.length !== 0 ) {
case 'resultsetDetails': {
resultsetOpt = msg.opt;
$id('resultsetCount').textContent = msg.count;
if ( msg.count !== 0 ) {
$id('create').removeAttribute('disabled');
} else {
$id('create').setAttribute('disabled', '');

View File

@ -278,7 +278,11 @@ const handlers = {
continue;
}
const details = JSON.parse(selector);
if ( details.action === 'style' ) {
if (
details.action !== undefined &&
details.tasks === undefined &&
details.action[0] === ':style'
) {
exceptionDict.set(details.selector, details.raw);
continue;
}

View File

@ -501,16 +501,15 @@ const cosmeticFilterMapper = (function() {
}
const nodesFromStyleTag = function(rootNode) {
var filterMap = roRedNodes,
entry, selector, canonical, nodes, node;
var details = vAPI.domFilterer.getAllSelectors();
const filterMap = roRedNodes;
const details = vAPI.domFilterer.getAllSelectors();
// Declarative selectors.
for ( entry of (details.declarative || []) ) {
for ( selector of entry[0].split(',\n') ) {
canonical = selector;
if ( entry[1] !== 'display:none!important;' ) {
for ( const entry of (details.declarative || []) ) {
for ( const selector of entry[0].split(',\n') ) {
let canonical = selector;
let nodes;
if ( entry[1] !== vAPI.hideStyle ) {
canonical += ':style(' + entry[1] + ')';
}
if ( reHasCSSCombinators.test(selector) ) {
@ -524,7 +523,7 @@ const cosmeticFilterMapper = (function() {
}
nodes = rootNode.querySelectorAll(selector);
}
for ( node of nodes ) {
for ( const node of nodes ) {
if ( filterMap.has(node) === false ) {
filterMap.set(node, canonical);
}
@ -533,9 +532,9 @@ const cosmeticFilterMapper = (function() {
}
// Procedural selectors.
for ( entry of (details.procedural || []) ) {
nodes = entry.exec();
for ( node of nodes ) {
for ( const entry of (details.procedural || []) ) {
const nodes = entry.exec();
for ( const node of nodes ) {
// Upgrade declarative selector to procedural one
filterMap.set(node, entry.raw);
}

View File

@ -51,7 +51,7 @@
if ( isNaN(surveyResults.hiddenElementCount) ) {
surveyResults.hiddenElementCount = (( ) => {
if ( vAPI.domFilterer instanceof Object === false ) { return 0; }
const details = vAPI.domFilterer.getAllSelectors_(true);
const details = vAPI.domFilterer.getAllSelectors(0b11);
if (
Array.isArray(details.declarative) === false ||
details.declarative.length === 0
@ -59,7 +59,7 @@
return 0;
}
return document.querySelectorAll(
details.declarative.map(entry => entry[0]).join(',')
details.declarative.map(entry => entry[0]).join(',')
).length;
})();
}

View File

@ -49,6 +49,8 @@ const lastNetFilterSession = window.location.host + window.location.pathname;
let lastNetFilterHostname = '';
let lastNetFilterUnion = '';
const hideBackgroundStyle = 'background-image:none!important;';
/******************************************************************************/
const safeQuerySelectorAll = function(node, selector) {
@ -645,10 +647,10 @@ const filterToDOMInterface = (( ) => {
reFilter.test(elem.currentSrc)
) {
out.push({
type: 'network',
elem: elem,
elem,
src: srcProp,
opts: filterTypes[elem.localName],
opt: filterTypes[elem.localName],
style: vAPI.hideStyle,
});
}
}
@ -657,10 +659,10 @@ const filterToDOMInterface = (( ) => {
for ( const elem of candidateElements ) {
if ( reFilter.test(backgroundImageURLFromElement(elem)) ) {
out.push({
type: 'network',
elem: elem,
style: 'background-image',
opts: 'image',
elem,
bg: true,
opt: 'image',
style: hideBackgroundStyle,
});
}
}
@ -690,7 +692,7 @@ const filterToDOMInterface = (( ) => {
const out = [];
for ( const elem of elems ) {
if ( elem === pickerRoot ) { continue; }
out.push({ type: 'cosmetic', elem, raw });
out.push({ elem, raw, style: vAPI.hideStyle });
}
return out;
};
@ -702,33 +704,28 @@ const filterToDOMInterface = (( ) => {
// Remove trailing pseudo-element when querying.
const fromCompiledCosmeticFilter = function(raw) {
if ( typeof raw !== 'string' ) { return; }
let elems;
let elems, style;
try {
const o = JSON.parse(raw);
if ( o.action === 'style' ) {
elems = document.querySelectorAll(
o.selector.replace(rePseudoElements, '')
);
lastAction = o.selector + ' {' + o.tasks[0][1] + '}';
} else if ( o.tasks ) {
elems = vAPI.domFilterer.createProceduralFilter(o).exec();
}
elems = vAPI.domFilterer.createProceduralFilter(o).exec();
style = o.action === undefined || o.action[0] !== ':style'
? vAPI.hideStyle
: o.action[1];
} catch(ex) {
return;
}
if ( !elems ) { return; }
const out = [];
for ( const elem of elems ) {
out.push({ type: 'cosmetic', elem, raw });
out.push({ elem, raw, style });
}
return out;
};
vAPI.epickerStyleProxies = vAPI.epickerStyleProxies || new Map();
let lastFilter;
let lastResultset;
let lastAction;
let appliedStyleTag;
let applied = false;
let previewing = false;
const queryAll = function(details) {
@ -738,11 +735,10 @@ const filterToDOMInterface = (( ) => {
unapply();
if ( filter === '' || filter === '!' ) {
lastFilter = '';
lastResultset = [];
return lastResultset;
lastResultset = undefined;
return;
}
lastFilter = filter;
lastAction = undefined;
if ( filter.startsWith('##') === false ) {
lastResultset = fromNetworkFilter(filter);
if ( previewing ) { apply(); }
@ -759,86 +755,29 @@ const filterToDOMInterface = (( ) => {
return lastResultset;
};
// https://github.com/gorhill/uBlock/issues/1629
// Avoid hiding the element picker's related elements.
const applyHide = function() {
const htmlElem = document.documentElement;
for ( const item of lastResultset ) {
const elem = item.elem;
if ( elem === pickerRoot ) { continue; }
if (
(elem !== htmlElem) &&
(item.type === 'cosmetic' || item.type === 'network' && item.src !== undefined)
) {
vAPI.domFilterer.hideNode(elem);
item.hidden = true;
}
if ( item.type === 'network' && item.style === 'background-image' ) {
const style = elem.style;
item.backgroundImage = style.getPropertyValue('background-image');
item.backgroundImagePriority = style.getPropertyPriority('background-image');
style.setProperty('background-image', 'none', 'important');
}
}
};
const unapplyHide = function() {
if ( lastResultset === undefined ) { return; }
for ( const item of lastResultset ) {
if ( item.hidden === true ) {
vAPI.domFilterer.unhideNode(item.elem);
item.hidden = false;
}
if ( item.hasOwnProperty('backgroundImage') ) {
item.elem.style.setProperty(
'background-image',
item.backgroundImage,
item.backgroundImagePriority
);
delete item.backgroundImage;
}
}
};
const unapplyStyle = function() {
if ( !appliedStyleTag || appliedStyleTag.parentNode === null ) {
return;
}
appliedStyleTag.parentNode.removeChild(appliedStyleTag);
};
const applyStyle = function() {
if ( !appliedStyleTag ) {
appliedStyleTag = document.createElement('style');
appliedStyleTag.setAttribute('type', 'text/css');
}
appliedStyleTag.textContent = lastAction;
if ( appliedStyleTag.parentNode === null ) {
document.head.appendChild(appliedStyleTag);
}
};
const apply = function() {
if ( applied ) {
unapply();
unapply();
if ( Array.isArray(lastResultset) === false ) { return; }
const rootElem = document.documentElement;
for ( const { elem, style } of lastResultset ) {
if ( elem === pickerRoot ) { continue; }
if ( elem === rootElem && style === vAPI.hideStyle ) { continue; }
let styleToken = vAPI.epickerStyleProxies.get(style);
if ( styleToken === undefined ) {
styleToken = vAPI.randomToken();
vAPI.epickerStyleProxies.set(style, styleToken);
vAPI.userStylesheet.add(`[${styleToken}]\n{${style}}`, true);
}
elem.setAttribute(styleToken, '');
}
if ( lastResultset === undefined ) { return; }
if ( typeof lastAction === 'string' ) {
applyStyle();
} else {
applyHide();
}
applied = true;
};
const unapply = function() {
if ( !applied ) { return; }
if ( typeof lastAction === 'string' ) {
unapplyStyle();
} else {
unapplyHide();
for ( const styleToken of vAPI.epickerStyleProxies.values() ) {
for ( const elem of document.querySelectorAll(`[${styleToken}]`) ) {
elem.removeAttribute(styleToken);
}
}
applied = false;
};
// https://www.reddit.com/r/uBlockOrigin/comments/c62irc/
@ -849,24 +788,24 @@ const filterToDOMInterface = (( ) => {
if ( previewing === false ) {
return unapply();
}
if ( lastResultset === undefined ) { return; }
apply();
if ( permanent === false ) { return; }
if ( Array.isArray(lastResultset) === false ) { return; }
if ( permanent === false || lastFilter.startsWith('##') === false ) {
return apply();
}
if ( vAPI.domFilterer instanceof Object === false ) { return; }
const cssSelectors = new Set();
const proceduralSelectors = new Set();
for ( const item of lastResultset ) {
if ( item.type !== 'cosmetic' ) { continue; }
if ( item.raw.startsWith('{') ) {
proceduralSelectors.add(item.raw);
for ( const { raw } of lastResultset ) {
if ( raw.startsWith('{') ) {
proceduralSelectors.add(raw);
} else {
cssSelectors.add(item.raw);
cssSelectors.add(raw);
}
}
if ( cssSelectors.size !== 0 ) {
vAPI.domFilterer.addCSSRule(
Array.from(cssSelectors),
'display:none!important;'
vAPI.hideStyle
);
}
if ( proceduralSelectors.size !== 0 ) {
@ -876,11 +815,7 @@ const filterToDOMInterface = (( ) => {
}
};
return {
get previewing() { return previewing; },
preview,
queryAll,
};
return { preview, queryAll };
})();
/******************************************************************************/
@ -1091,11 +1026,6 @@ const quitPicker = function() {
if ( pickerRoot === null ) { return; }
// https://github.com/gorhill/uBlock/issues/2060
if ( vAPI.domFilterer instanceof Object ) {
vAPI.domFilterer.unexcludeNode(pickerRoot);
}
pickerRoot.remove();
pickerRoot = null;
@ -1118,16 +1048,13 @@ const onDialogMessage = function(msg) {
quitPicker();
break;
case 'dialogSetFilter': {
const resultset = filterToDOMInterface.queryAll(msg);
const resultset = filterToDOMInterface.queryAll(msg) || [];
highlightElements(resultset.map(a => a.elem), true);
if ( msg.filter === '!' ) { break; }
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
what: 'filterResultset',
resultset: resultset.map(a => {
const o = Object.assign({}, a);
o.elem = undefined;
return o;
}),
what: 'resultsetDetails',
count: resultset.length,
opt: resultset.length !== 0 ? resultset[0].opt : undefined,
});
break;
}
@ -1250,7 +1177,7 @@ const pickerCSSStyle = [
].join(' !important;');
const pickerCSS = `
:root [${vAPI.sessionId}] {
:root > [${vAPI.sessionId}] {
${pickerCSSStyle}
}
:root [${vAPI.sessionId}-clickblind] {
@ -1265,11 +1192,6 @@ pickerRoot = document.createElement('iframe');
pickerRoot.setAttribute(vAPI.sessionId, '');
document.documentElement.append(pickerRoot);
// https://github.com/gorhill/uBlock/issues/2060
if ( vAPI.domFilterer instanceof Object ) {
vAPI.domFilterer.excludeNode(pickerRoot);
}
vAPI.shutdown.add(quitPicker);
vAPI.MessagingConnection.addListener(onConnectionMessage);

View File

@ -1177,7 +1177,7 @@ Parser.prototype.SelectorCompiler = class {
]);
this.reSimpleSelector = /^[#.][A-Za-z_][\w-]*$/;
this.div = document.createElement('div');
this.rePseudoClass = /:(?::?after|:?before|:-?[a-z][a-z-]*[a-z])$/;
this.rePseudoElement = /:(?::?after|:?before|:-?[a-z][a-z-]*[a-z])$/;
this.reProceduralOperator = new RegExp([
'^(?:',
Array.from(parser.proceduralOperatorTokens.keys()).join('|'),
@ -1296,7 +1296,7 @@ Parser.prototype.SelectorCompiler = class {
// is fixed.
cssSelectorType(s) {
if ( this.reSimpleSelector.test(s) ) { return 1; }
const pos = this.cssPseudoSelector(s);
const pos = this.cssPseudoElement(s);
if ( pos !== -1 ) {
return this.cssSelectorType(s.slice(0, pos)) === 1 ? 3 : 0;
}
@ -1308,9 +1308,9 @@ Parser.prototype.SelectorCompiler = class {
return 1;
}
cssPseudoSelector(s) {
cssPseudoElement(s) {
if ( s.lastIndexOf(':') === -1 ) { return -1; }
const match = this.rePseudoClass.exec(s);
const match = this.rePseudoElement.exec(s);
return match !== null ? match.index : -1;
}
@ -1450,13 +1450,10 @@ Parser.prototype.SelectorCompiler = class {
// The normalized string version is what is reported in the logger,
// by design.
decompileProcedural(compiled) {
const tasks = compiled.tasks;
if ( Array.isArray(tasks) === false ) {
return compiled.selector;
}
const tasks = compiled.tasks || [];
const raw = [ compiled.selector ];
let value;
for ( const task of tasks ) {
let value;
switch ( task[0] ) {
case ':has':
case ':if':
@ -1494,8 +1491,6 @@ Parser.prototype.SelectorCompiler = class {
raw.push(task[1]);
break;
case ':min-text-length':
case ':remove':
case ':style':
case ':upward':
case ':watch-attr':
case ':xpath':
@ -1503,6 +1498,10 @@ Parser.prototype.SelectorCompiler = class {
break;
}
}
if ( Array.isArray(compiled.action) ) {
const [ op, arg ] = compiled.action;
raw.push(`${op}(${arg})`);
}
return raw.join('');
}
@ -1578,10 +1577,12 @@ Parser.prototype.SelectorCompiler = class {
tasks.push([ ':spath', spath ]);
}
if ( action !== undefined ) { return; }
tasks.push([ operator, args ]);
const task = [ operator, args ];
if ( this.actionOperators.has(operator) ) {
if ( root === false ) { return; }
action = operator.slice(1);
action = task;
} else {
tasks.push(task);
}
opPrefixBeg = i;
if ( i === n ) { break; }
@ -1589,7 +1590,7 @@ Parser.prototype.SelectorCompiler = class {
// No task found: then we have a CSS selector.
// At least one task found: nothing should be left to parse.
if ( tasks.length === 0 ) {
if ( tasks.length === 0 && action === undefined ) {
prefix = raw;
} else if ( opPrefixBeg < n ) {
if ( action !== undefined ) { return; }
@ -1626,21 +1627,17 @@ Parser.prototype.SelectorCompiler = class {
}
// Expose action to take in root descriptor.
//
// https://github.com/uBlockOrigin/uBlock-issues/issues/961
// https://github.com/uBlockOrigin/uBlock-issues/issues/382
// For the time being, `style` action can't be used in a
// procedural selector.
if ( action !== undefined ) {
if ( tasks.length > 1 && action === 'style' ) { return; }
out.action = action;
}
// Pseudo-selectors are valid only when used in a root task list.
// Pseudo elements are valid only when used in a root task list AND
// only when there are no procedural operators: pseudo elements can't
// be querySelectorAll-ed.
if ( prefix !== '' ) {
const pos = this.cssPseudoSelector(prefix);
const pos = this.cssPseudoElement(prefix);
if ( pos !== -1 ) {
if ( root === false ) { return; }
if ( root === false || tasks.length !== 0 ) { return; }
out.pseudo = pos;
}
}