_
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
+
diff --git a/platform/mv3/extension/settings.html b/platform/mv3/extension/settings.html
new file mode 100644
index 000000000..88b5e44fd
--- /dev/null
+++ b/platform/mv3/extension/settings.html
@@ -0,0 +1,109 @@
+
+
+
+
+
+
-
-
- uBlock Origin Lite — Filter lists
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
++
+
+
+
+Filter lists
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/platform/mv3/make-rulesets.js b/platform/mv3/make-rulesets.js
index 35801ef0b..011b0f821 100644
--- a/platform/mv3/make-rulesets.js
+++ b/platform/mv3/make-rulesets.js
@@ -382,15 +382,54 @@ function addScriptingAPIResources(id, hostnames, fid) {
}
}
-const toCSSFileId = s => (uidint32(s) & ~0b11) | 0b00;
-const toJSFileId = s => (uidint32(s) & ~0b11) | 0b01;
-const toProceduralFileId = s => (uidint32(s) & ~0b11) | 0b10;
+const toIsolatedStartFileId = s => (uidint32(s) & ~0b11) | 0b00;
+const toMainStartFileId = s => (uidint32(s) & ~0b11) | 0b01;
+const toIsolatedEndFileId = s => (uidint32(s) & ~0b11) | 0b10;
const pathFromFileName = fname => `${scriptletDir}/${fname.slice(0,2)}/${fname.slice(2)}.js`;
/******************************************************************************/
-const MAX_COSMETIC_FILTERS_PER_FILE = 128;
+async function processGenericCosmeticFilters(assetDetails, bucketsMap, exclusions) {
+ const out = {
+ count: 0,
+ exclusionCount: 0,
+ };
+ if ( bucketsMap === undefined ) { return out; }
+ if ( bucketsMap.size === 0 ) { return out; }
+ const bucketsList = Array.from(bucketsMap);
+ const count = bucketsList.reduce((a, v) => a += v[1].length, 0);
+ if ( count === 0 ) { return out; }
+ out.count = count;
+
+ const selectorLists = bucketsList.map(v => [ v[0], v[1].join(',') ]);
+ const originalScriptletMap = await loadAllSourceScriptlets();
+
+ const patchedScriptlet = originalScriptletMap.get('css-generic')
+ .replace(
+ '$rulesetId$',
+ assetDetails.id
+ ).replace(
+ /\bself\.\$excludeHostnameSet\$/m,
+ `${JSON.stringify(exclusions, scriptletJsonReplacer)}`
+ ).replace(
+ /\bself\.\$genericSelectorLists\$/m,
+ `${JSON.stringify(selectorLists, scriptletJsonReplacer)}`
+ );
+
+ writeFile(
+ `${scriptletDir}/${assetDetails.id}.generic.js`,
+ patchedScriptlet
+ );
+
+ log(`CSS-generic: ${count} plain CSS selectors`);
+
+ return out;
+}
+
+/******************************************************************************/
+
+const MAX_COSMETIC_FILTERS_PER_FILE = 256;
// This merges selectors which are used by the same hostnames
@@ -530,11 +569,11 @@ async function processCosmeticFilters(assetDetails, mapin) {
/\bself\.\$hostnamesMap\$/m,
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
);
- const fid = toCSSFileId(patchedScriptlet);
+ const fid = toIsolatedStartFileId(patchedScriptlet);
if ( globalPatchedScriptletsSet.has(fid) === false ) {
globalPatchedScriptletsSet.add(fid);
const fname = fnameFromFileId(fid);
- writeFile(pathFromFileName(fname), patchedScriptlet, {});
+ writeFile(pathFromFileName(fname), patchedScriptlet);
generatedFiles.push(fname);
}
for ( const entry of slice ) {
@@ -543,8 +582,8 @@ async function processCosmeticFilters(assetDetails, mapin) {
}
if ( generatedFiles.length !== 0 ) {
- log(`CSS-related distinct filters: ${contentArray.length} distinct combined selectors`);
- log(`CSS-related injectable files: ${generatedFiles.length}`);
+ log(`CSS-specific distinct filters: ${contentArray.length} distinct combined selectors`);
+ log(`CSS-specific injectable files: ${generatedFiles.length}`);
log(`\t${generatedFiles.join(', ')}`);
}
@@ -596,11 +635,11 @@ async function processProceduralCosmeticFilters(assetDetails, mapin) {
/\bself\.\$hostnamesMap\$/m,
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
);
- const fid = toProceduralFileId(patchedScriptlet);
+ const fid = toIsolatedEndFileId(patchedScriptlet);
if ( globalPatchedScriptletsSet.has(fid) === false ) {
globalPatchedScriptletsSet.add(fid);
const fname = fnameFromFileId(fid);
- writeFile(pathFromFileName(fname), patchedScriptlet, {});
+ writeFile(pathFromFileName(fname), patchedScriptlet);
generatedFiles.push(fname);
}
for ( const entry of slice ) {
@@ -725,11 +764,11 @@ async function processScriptletFilters(assetDetails, mapin) {
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
);
// ends-with 1 = scriptlet resource
- const fid = toJSFileId(patchedScriptlet);
+ const fid = toMainStartFileId(patchedScriptlet);
if ( globalPatchedScriptletsSet.has(fid) === false ) {
globalPatchedScriptletsSet.add(fid);
const fname = fnameFromFileId(fid);
- writeFile(pathFromFileName(fname), patchedScriptlet, {});
+ writeFile(pathFromFileName(fname), patchedScriptlet);
generatedFiles.push(fname);
}
for ( const details of argsDetails.values() ) {
@@ -771,8 +810,8 @@ const rulesetFromURLS = async function(assetDetails) {
const declarativeCosmetic = new Map();
const proceduralCosmetic = new Map();
const rejectedCosmetic = [];
- if ( results.cosmetic ) {
- for ( const [ selector, details ] of results.cosmetic ) {
+ if ( results.specificCosmetic ) {
+ for ( const [ selector, details ] of results.specificCosmetic ) {
if ( details.rejected ) {
rejectedCosmetic.push(selector);
continue;
@@ -786,7 +825,12 @@ const rulesetFromURLS = async function(assetDetails) {
proceduralCosmetic.set(JSON.stringify(parsed), details);
}
}
- const cosmeticStats = await processCosmeticFilters(
+ const genericCosmeticStats = await processGenericCosmeticFilters(
+ assetDetails,
+ results.genericCosmetic,
+ results.network.generichideExclusions.filter(hn => hn.endsWith('.*') === false)
+ );
+ const specificCosmeticStats = await processCosmeticFilters(
assetDetails,
declarativeCosmetic
);
@@ -824,7 +868,8 @@ const rulesetFromURLS = async function(assetDetails) {
rejected: netStats.rejected,
},
css: {
- specific: cosmeticStats,
+ generic: genericCosmeticStats,
+ specific: specificCosmeticStats,
procedural: proceduralStats,
},
scriptlets: {
@@ -896,20 +941,36 @@ async function main() {
'ara-0',
'EST-0',
];
+ // Merge lists which have same target languages
+ const langToListsMap = new Map();
for ( const [ id, asset ] of Object.entries(assets) ) {
if ( asset.content !== 'filters' ) { continue; }
if ( asset.off !== true ) { continue; }
if ( typeof asset.lang !== 'string' ) { continue; }
if ( excludedLists.includes(id) ) { continue; }
- const contentURL = Array.isArray(asset.contentURL)
- ? asset.contentURL[0]
- : asset.contentURL;
+ let ids = langToListsMap.get(asset.lang);
+ if ( ids === undefined ) {
+ langToListsMap.set(asset.lang, ids = []);
+ }
+ ids.push(id);
+ }
+ for ( const ids of langToListsMap.values() ) {
+ const urls = [];
+ for ( const id of ids ) {
+ const asset = assets[id];
+ const contentURL = Array.isArray(asset.contentURL)
+ ? asset.contentURL[0]
+ : asset.contentURL;
+ urls.push(contentURL);
+ }
+ const id = ids[0];
+ const asset = assets[id];
await rulesetFromURLS({
id: id.toLowerCase(),
lang: asset.lang,
name: asset.title,
enabled: false,
- urls: [ contentURL ],
+ urls,
homeURL: asset.supportURL,
});
}
@@ -933,6 +994,13 @@ async function main() {
}
// Handpicked rulesets from abroad
+ await rulesetFromURLS({
+ id: 'cname-trackers',
+ name: 'AdGuard CNAME-cloaked trackers',
+ enabled: true,
+ urls: [ 'https://raw.githubusercontent.com/AdguardTeam/cname-trackers/master/combined_disguised_trackers.txt' ],
+ homeURL: 'https://github.com/AdguardTeam/cname-trackers#cname-cloaked-trackers',
+ });
await rulesetFromURLS({
id: 'stevenblack-hosts',
name: 'Steven Black\'s hosts file',
diff --git a/platform/mv3/scriptlets/css-generic.js b/platform/mv3/scriptlets/css-generic.js
new file mode 100644
index 000000000..7ded24d75
--- /dev/null
+++ b/platform/mv3/scriptlets/css-generic.js
@@ -0,0 +1,291 @@
+/*******************************************************************************
+
+ uBlock Origin - a browser extension to block requests.
+ Copyright (C) 2014-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+*/
+
+/* jshint esversion:11 */
+
+'use strict';
+
+/******************************************************************************/
+
+/// name css-generic
+
+/******************************************************************************/
+
+// Important!
+// Isolate from global scope
+(function uBOL_cssGeneric() {
+
+/******************************************************************************/
+
+// $rulesetId$
+
+{
+ const excludeHostnameSet = new Set(self.$excludeHostnameSet$);
+
+ let hn;
+ try { hn = document.location.hostname; } catch(ex) { }
+ while ( hn ) {
+ if ( excludeHostnameSet.has(hn) ) { return; }
+ const pos = hn.indexOf('.');
+ if ( pos === -1 ) { break; }
+ hn = hn.slice(pos+1);
+ }
+ excludeHostnameSet.clear();
+}
+
+const genericSelectorLists = new Map(self.$genericSelectorLists$);
+
+/******************************************************************************/
+
+const queriedHashes = new Set();
+const maxSurveyTimeSlice = 4;
+const styleSheetSelectors = [];
+const stopAllRatio = 0.95; // To be investigated
+
+let surveyCount = 0;
+let surveyMissCount = 0;
+let styleSheetTimer;
+let processTimer;
+let domChangeTimer;
+let lastDomChange = Date.now();
+
+/******************************************************************************/
+
+// https://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
+const hashFromStr = (type, s) => {
+ const len = s.length;
+ const step = len + 7 >>> 3;
+ let hash = type;
+ for ( let i = 0; i < len; i += step ) {
+ hash = (hash << 5) - hash + s.charCodeAt(i) | 0;
+ }
+ return hash & 0x00FFFFFF;
+};
+
+/******************************************************************************/
+
+// Extract all classes/ids: these will be passed to the cosmetic
+// filtering engine, and in return we will obtain only the relevant
+// CSS selectors.
+
+// https://github.com/gorhill/uBlock/issues/672
+// http://www.w3.org/TR/2014/REC-html5-20141028/infrastructure.html#space-separated-tokens
+// http://jsperf.com/enumerate-classes/6
+
+const uBOL_idFromNode = (node, out) => {
+ const raw = node.id;
+ if ( typeof raw !== 'string' || raw.length === 0 ) { return; }
+ const s = raw.trim();
+ const hash = hashFromStr(0x23 /* '#' */, s);
+ if ( queriedHashes.has(hash) ) { return; }
+ out.push(hash);
+ queriedHashes.add(hash);
+};
+
+// https://github.com/uBlockOrigin/uBlock-issues/discussions/2076
+// Performance: avoid using Element.classList
+const uBOL_classesFromNode = (node, out) => {
+ const s = node.getAttribute('class');
+ if ( typeof s !== 'string' ) { return; }
+ const len = s.length;
+ for ( let beg = 0, end = 0, token = ''; beg < len; beg += 1 ) {
+ end = s.indexOf(' ', beg);
+ if ( end === beg ) { continue; }
+ if ( end === -1 ) { end = len; }
+ token = s.slice(beg, end);
+ beg = end;
+ const hash = hashFromStr(0x2E /* '.' */, token);
+ if ( queriedHashes.has(hash) ) { continue; }
+ out.push(hash);
+ queriedHashes.add(hash);
+ }
+};
+
+/******************************************************************************/
+
+const pendingNodes = {
+ nodeLists: [],
+ buffer: [
+ null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null,
+ ],
+ j: 0,
+ add(nodes) {
+ if ( nodes.length === 0 ) { return; }
+ this.nodeLists.push(nodes);
+ },
+ next() {
+ if ( this.nodeLists.length === 0 ) { return 0; }
+ const maxSurveyBuffer = this.buffer.length;
+ const nodeLists = this.nodeLists;
+ let ib = 0;
+ do {
+ const nodeList = nodeLists[0];
+ let j = this.j;
+ let n = j + maxSurveyBuffer - ib;
+ if ( n > nodeList.length ) {
+ n = nodeList.length;
+ }
+ for ( let i = j; i < n; i++ ) {
+ this.buffer[ib++] = nodeList[j++];
+ }
+ if ( j !== nodeList.length ) {
+ this.j = j;
+ break;
+ }
+ this.j = 0;
+ this.nodeLists.shift();
+ } while ( ib < maxSurveyBuffer && nodeLists.length !== 0 );
+ return ib;
+ },
+ hasNodes() {
+ return this.nodeLists.length !== 0;
+ },
+};
+
+/******************************************************************************/
+
+const uBOL_processNodes = ( ) => {
+ const t0 = Date.now();
+ const hashes = [];
+ const nodes = pendingNodes.buffer;
+ const deadline = t0 + maxSurveyTimeSlice;
+ let processed = 0;
+ for (;;) {
+ const n = pendingNodes.next();
+ if ( n === 0 ) { break; }
+ for ( let i = 0; i < n; i++ ) {
+ const node = nodes[i];
+ nodes[i] = null;
+ uBOL_idFromNode(node, hashes);
+ uBOL_classesFromNode(node, hashes);
+ }
+ processed += n;
+ if ( performance.now() >= deadline ) { break; }
+ }
+ for ( const hash of hashes ) {
+ const selectorList = genericSelectorLists.get(hash);
+ if ( selectorList === undefined ) { continue; }
+ styleSheetSelectors.push(selectorList);
+ genericSelectorLists.delete(hash);
+ }
+ surveyCount += 1;
+ if ( styleSheetSelectors.length === 0 ) {
+ surveyMissCount += 1;
+ if (
+ surveyCount >= 100 &&
+ (surveyMissCount / surveyCount) >= stopAllRatio
+ ) {
+ stopAll('too many misses in surveyor');
+ }
+ return;
+ }
+ if ( styleSheetTimer !== undefined ) { return; }
+ styleSheetTimer = self.requestAnimationFrame(( ) => {
+ styleSheetTimer = undefined;
+ uBOL_injectStyleSheet();
+ });
+};
+
+/******************************************************************************/
+
+const uBOL_processChanges = mutations => {
+ for ( let i = 0; i < mutations.length; i++ ) {
+ const mutation = mutations[i];
+ for ( const added of mutation.addedNodes ) {
+ if ( added.nodeType !== 1 ) { continue; }
+ pendingNodes.add([ added ]);
+ if ( added.firstElementChild === null ) { continue; }
+ pendingNodes.add(added.querySelectorAll('[id],[class]'));
+ }
+ }
+ if ( pendingNodes.hasNodes() === false ) { return; }
+ lastDomChange = Date.now();
+ if ( processTimer !== undefined ) { return; }
+ processTimer = self.setTimeout(( ) => {
+ processTimer = undefined;
+ uBOL_processNodes();
+ }, 64);
+};
+
+/******************************************************************************/
+
+const uBOL_injectStyleSheet = ( ) => {
+ try {
+ const sheet = new CSSStyleSheet();
+ sheet.replace(`@layer{${styleSheetSelectors.join(',')}{display:none!important;}}`);
+ document.adoptedStyleSheets = [
+ ...document.adoptedStyleSheets,
+ sheet
+ ];
+ } catch(ex) {
+ }
+ styleSheetSelectors.length = 0;
+};
+
+/******************************************************************************/
+
+pendingNodes.add(document.querySelectorAll('[id],[class]'));
+uBOL_processNodes();
+
+let domMutationObserver = new MutationObserver(uBOL_processChanges);
+domMutationObserver.observe(document, {
+ childList: true,
+ subtree: true,
+});
+
+const needDomChangeObserver = ( ) => {
+ domChangeTimer = undefined;
+ if ( domMutationObserver === undefined ) { return; }
+ if ( (Date.now() - lastDomChange) > 20000 ) {
+ return stopAll('no more DOM changes');
+ }
+ domChangeTimer = self.setTimeout(needDomChangeObserver, 20000);
+};
+
+needDomChangeObserver();
+
+/******************************************************************************/
+
+const stopAll = reason => {
+ if ( domChangeTimer !== undefined ) {
+ self.clearTimeout(domChangeTimer);
+ domChangeTimer = undefined;
+ }
+ domMutationObserver.disconnect();
+ domMutationObserver.takeRecords();
+ domMutationObserver = undefined;
+ genericSelectorLists.clear();
+ queriedHashes.clear();
+ console.info(`uBOL: Generic cosmetic filtering stopped because ${reason}`);
+};
+
+/******************************************************************************/
+
+})();
+
+/******************************************************************************/
diff --git a/platform/mv3/scriptlets/css-specific-procedural.js b/platform/mv3/scriptlets/css-specific-procedural.js
index 7c7f22562..1fe9791d4 100644
--- a/platform/mv3/scriptlets/css-specific-procedural.js
+++ b/platform/mv3/scriptlets/css-specific-procedural.js
@@ -43,6 +43,10 @@ const hostnamesMap = new Map(self.$hostnamesMap$);
/******************************************************************************/
+let proceduralFilterer;
+
+/******************************************************************************/
+
const addStylesheet = text => {
try {
const sheet = new CSSStyleSheet();
@@ -149,11 +153,8 @@ class PSelectorMatchesMediaTask extends PSelectorTask {
this.mql = window.matchMedia(task[1]);
if ( this.mql.media === 'not all' ) { return; }
this.mql.addEventListener('change', ( ) => {
- if ( typeof vAPI !== 'object' ) { return; }
- if ( vAPI === null ) { return; }
- const filterer = vAPI.domFilterer && vAPI.domFilterer.proceduralFilterer;
- if ( filterer instanceof Object === false ) { return; }
- filterer.onDOMChanged([ null ]);
+ if ( proceduralFilterer instanceof Object === false ) { return; }
+ proceduralFilterer.onDOMChanged([ null ]);
});
}
transpose(node, output) {
@@ -258,25 +259,10 @@ class PSelectorSpathTask extends PSelectorTask {
this.spath = `:scope ${this.spath.trim()}`;
}
}
- qsa(node) {
- if ( this.nth === false ) {
- return node.querySelectorAll(this.spath);
- }
- const parent = node.parentElement;
- if ( parent === null ) { return; }
- let pos = 1;
- for (;;) {
- node = node.previousElementSibling;
- if ( node === null ) { break; }
- pos += 1;
- }
- return parent.querySelectorAll(
- `:scope > :nth-child(${pos})${this.spath}`
- );
- }
transpose(node, output) {
- const nodes = this.qsa(node);
- if ( nodes === undefined ) { return; }
+ const nodes = this.nth
+ ? PSelectorSpathTask.qsa(node, this.spath)
+ : node.querySelectorAll(this.spath);
for ( const node of nodes ) {
output.push(node);
}
@@ -344,10 +330,8 @@ class PSelectorWatchAttrs extends PSelectorTask {
}
// TODO: Is it worth trying to re-apply only the current selector?
handler() {
- const filterer =
- vAPI.domFilterer && vAPI.domFilterer.proceduralFilterer;
- if ( filterer instanceof Object ) {
- filterer.onDOMChanged([ null ]);
+ if ( proceduralFilterer instanceof Object ) {
+ proceduralFilterer.onDOMChanged([ null ]);
}
}
transpose(node, output) {
@@ -420,12 +404,10 @@ class PSelector {
prime(input) {
const root = input || document;
if ( this.selector === '' ) { return [ root ]; }
- let selector = this.selector;
if ( input !== document && /^ [>+~]/.test(this.selector) ) {
return Array.from(PSelectorSpathTask.qsa(input, this.selector));
}
- const elems = root.querySelectorAll(selector);
- return Array.from(elems);
+ return Array.from(root.querySelectorAll(this.selector));
}
exec(input) {
let nodes = this.prime(input);
@@ -509,7 +491,7 @@ class ProceduralFilterer {
this.onDOMChanged();
}
- commitNow() {
+ uBOL_commitNow() {
//console.time('procedural selectors/dom layout changed');
// https://github.com/uBlockOrigin/uBlock-issues/issues/341
@@ -589,7 +571,7 @@ class ProceduralFilterer {
if ( this.timer !== undefined ) { return; }
this.timer = self.requestAnimationFrame(( ) => {
this.timer = undefined;
- this.commitNow();
+ this.uBOL_commitNow();
});
}
}
@@ -668,10 +650,8 @@ if ( styleSelectors.length !== 0 ) {
/******************************************************************************/
-// Procedural selectors
-
if ( proceduralSelectors.length !== 0 ) {
- const filterer = new ProceduralFilterer(proceduralSelectors);
+ proceduralFilterer = new ProceduralFilterer(proceduralSelectors);
const observer = new MutationObserver(mutations => {
let domChanged = false;
for ( let i = 0; i < mutations.length && !domChanged; i++ ) {
@@ -686,7 +666,7 @@ if ( proceduralSelectors.length !== 0 ) {
}
}
if ( domChanged === false ) { return; }
- filterer.onDOMChanged();
+ proceduralFilterer.onDOMChanged();
});
observer.observe(document, {
childList: true,
diff --git a/src/css/common.css b/src/css/common.css
index 5f298a651..15f1b4fab 100644
--- a/src/css/common.css
+++ b/src/css/common.css
@@ -185,6 +185,9 @@ section.notice {
position: relative;
width: var(--checkbox-size);
}
+label:hover .checkbox:not([disabled]) {
+ background-color: var(--surface-2);
+ }
.checkbox > input[type="checkbox"] {
box-sizing: border-box;
height: 100%;
@@ -217,6 +220,49 @@ section.notice {
filter: var(--checkbox-disabled-filter);
}
+.radio {
+ --margin-end: calc(var(--font-size) * 0.75);
+ box-sizing: border-box;
+ display: inline-flex;
+ flex-shrink: 0;
+ height: calc(var(--checkbox-size) + 2px);
+ margin: 0;
+ margin-inline-end: var(--margin-end);
+ -webkit-margin-end: var(--margin-end);
+ position: relative;
+ width: calc(var(--checkbox-size) + 2px);
+ }
+.radio > input[type="radio"] {
+ box-sizing: border-box;
+ height: 100%;
+ margin: 0;
+ min-width: var(--checkbox-size);
+ opacity: 0;
+ position: absolute;
+ width: 100%;
+ }
+.radio > input[type="radio"] + svg {
+ background-color: transparent;
+ box-sizing: border-box;
+ height: 100%;
+ pointer-events: none;
+ position: absolute;
+ width: 100%;
+ }
+.radio > input[type="radio"] + svg > path {
+ fill: var(--checkbox-ink);
+ }
+.radio > input[type="radio"] + svg > circle {
+ fill: transparent;
+ }
+label:hover .radio > input[type="radio"]:not(:checked) + svg > circle {
+ fill: var(--surface-3);
+ }
+.radio > input[type="radio"]:checked + svg > path,
+.radio > input[type="radio"]:checked + svg > circle {
+ fill: var(--checkbox-checked-ink);
+ }
+
select {
padding: 2px;
}
diff --git a/src/js/messaging.js b/src/js/messaging.js
index 90a57ed58..7ebbdeff7 100644
--- a/src/js/messaging.js
+++ b/src/js/messaging.js
@@ -240,9 +240,9 @@ const onMessage = function(request, sender, callback) {
isUnsupported(rule)
);
out.push(`+ Unsupported filters (${bad.length}): ${JSON.stringify(bad, replacer, 2)}`);
-
- out.push(`\n+ Cosmetic filters: ${result.cosmetic.length}`);
- for ( const details of result.cosmetic ) {
+ out.push(`+ generichide exclusions (${network.generichideExclusions.length}): ${JSON.stringify(network.generichideExclusions, replacer, 2)}`);
+ out.push(`+ Cosmetic filters: ${result.specificCosmetic.size}`);
+ for ( const details of result.specificCosmetic ) {
out.push(` ${JSON.stringify(details)}`);
}
diff --git a/src/js/static-dnr-filtering.js b/src/js/static-dnr-filtering.js
index 876970fcc..9347a825b 100644
--- a/src/js/static-dnr-filtering.js
+++ b/src/js/static-dnr-filtering.js
@@ -34,6 +34,57 @@ import {
/******************************************************************************/
+// https://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
+
+const hashFromStr = (type, s) => {
+ const len = s.length;
+ const step = len + 7 >>> 3;
+ let hash = type;
+ for ( let i = 0; i < len; i += step ) {
+ hash = (hash << 5) - hash + s.charCodeAt(i) | 0;
+ }
+ return hash & 0x00FFFFFF;
+};
+
+/******************************************************************************/
+
+// Copied from cosmetic-filter.js for the time being to avoid unwanted
+// dependencies
+
+const rePlainSelector = /^[#.][\w\\-]+/;
+const rePlainSelectorEscaped = /^[#.](?:\\[0-9A-Fa-f]+ |\\.|\w|-)+/;
+const reEscapeSequence = /\\([0-9A-Fa-f]+ |.)/g;
+
+const keyFromSelector = selector => {
+ let matches = rePlainSelector.exec(selector);
+ if ( matches === null ) { return; }
+ let key = matches[0];
+ if ( key.indexOf('\\') === -1 ) {
+ return key;
+ }
+ matches = rePlainSelectorEscaped.exec(selector);
+ if ( matches === null ) { return; }
+ key = '';
+ const escaped = matches[0];
+ let beg = 0;
+ reEscapeSequence.lastIndex = 0;
+ for (;;) {
+ matches = reEscapeSequence.exec(escaped);
+ if ( matches === null ) {
+ return key + escaped.slice(beg);
+ }
+ key += escaped.slice(beg, matches.index);
+ beg = reEscapeSequence.lastIndex;
+ if ( matches[1].length === 1 ) {
+ key += matches[1];
+ } else {
+ key += String.fromCharCode(parseInt(matches[1], 16));
+ }
+ }
+};
+
+/******************************************************************************/
+
function addExtendedToDNR(context, parser) {
if ( parser.category !== parser.CATStaticExtFilter ) { return false; }
@@ -87,13 +138,35 @@ function addExtendedToDNR(context, parser) {
}
// Cosmetic filtering
- if ( context.cosmeticFilters === undefined ) {
- context.cosmeticFilters = new Map();
+
+ // Generic cosmetic filtering
+ if ( parser.hasOptions() === false ) {
+ if ( context.genericCosmeticFilters === undefined ) {
+ context.genericCosmeticFilters = new Map();
+ }
+ const { compiled } = parser.result;
+ if ( compiled === undefined ) { return; }
+ if ( compiled.length <= 1 ) { return; }
+ if ( compiled.charCodeAt(0) === 0x7B /* '{' */ ) { return; }
+ const key = keyFromSelector(compiled);
+ if ( key === undefined ) { return; }
+ const type = key.charCodeAt(0);
+ const hash = hashFromStr(type, key.slice(1));
+ let bucket = context.genericCosmeticFilters.get(hash);
+ if ( bucket === undefined ) {
+ context.genericCosmeticFilters.set(hash, bucket = []);
+ }
+ bucket.push(compiled);
+ return;
}
+ // Specific cosmetic filtering
// https://github.com/chrisaljoudi/uBlock/issues/151
// Negated hostname means the filter applies to all non-negated hostnames
// of same filter OR globally if there is no non-negated hostnames.
+ if ( context.specificCosmeticFilters === undefined ) {
+ context.specificCosmeticFilters = new Map();
+ }
for ( const { hn, not, bad } of parser.extOptions() ) {
if ( bad ) { continue; }
let { compiled, exception, raw } = parser.result;
@@ -107,11 +180,11 @@ function addExtendedToDNR(context, parser) {
if ( rejected ) {
compiled = rejected;
}
- let details = context.cosmeticFilters.get(compiled);
+ let details = context.specificCosmeticFilters.get(compiled);
if ( details === undefined ) {
details = {};
if ( rejected ) { details.rejected = true; }
- context.cosmeticFilters.set(compiled, details);
+ context.specificCosmeticFilters.set(compiled, details);
}
if ( rejected ) { continue; }
if ( not ) {
@@ -206,7 +279,8 @@ async function dnrRulesetFromRawLists(lists, options = {}) {
return {
network: staticNetFilteringEngine.dnrFromCompiled('end', context),
- cosmetic: context.cosmeticFilters,
+ genericCosmetic: context.genericCosmeticFilters,
+ specificCosmetic: context.specificCosmeticFilters,
scriptlet: context.scriptletFilters,
};
}
diff --git a/src/js/static-net-filtering.js b/src/js/static-net-filtering.js
index b61dd3415..6e102a2eb 100644
--- a/src/js/static-net-filtering.js
+++ b/src/js/static-net-filtering.js
@@ -4033,6 +4033,24 @@ FilterContainer.prototype.dnrFromCompiled = function(op, context, ...args) {
}
}
+ // Collect generichide filters
+ const generichideExclusions = [];
+ {
+ const bucket = buckets.get(AllowAction | typeNameToTypeValue['generichide']);
+ if ( bucket ) {
+ for ( const rules of bucket.values() ) {
+ for ( const rule of rules ) {
+ if ( rule.condition === undefined ) { continue; }
+ if ( rule.condition.initiatorDomains ) {
+ generichideExclusions.push(...rule.condition.initiatorDomains);
+ } else if ( rule.condition.requestDomains ) {
+ generichideExclusions.push(...rule.condition.requestDomains);
+ }
+ }
+ }
+ }
+ }
+
// Patch modifier filters
for ( const rule of ruleset ) {
if ( rule.__modifierType === undefined ) { continue; }
@@ -4247,6 +4265,7 @@ FilterContainer.prototype.dnrFromCompiled = function(op, context, ...args) {
filterCount: context.filterCount,
acceptedFilterCount: context.acceptedFilterCount,
rejectedFilterCount: context.rejectedFilterCount,
+ generichideExclusions,
};
};
+
+
+
+
+
+
+