[mv3] Add support for procedural cosmetic filters

Related issue:
https://github.com/uBlockOrigin/uBOL-home/issues/325
This commit is contained in:
Raymond Hill 2025-08-03 15:50:22 -04:00
parent e713e133eb
commit 32bf5ebde3
No known key found for this signature in database
GPG key ID: 25E1490B761470C2
16 changed files with 418 additions and 217 deletions

View file

@ -37,7 +37,8 @@ import {
injectCustomFilters,
removeCustomFilter,
selectorsFromCustomFilters,
uninjectCustomFilters,
startCustomFilters,
terminateCustomFilters,
} from './filter-manager.js';
import {
@ -199,6 +200,20 @@ function onMessage(request, sender, callback) {
return false;
}
case 'startCustomFilters':
if ( frameId === false ) { return false; }
startCustomFilters(tabId, frameId).then(( ) => {
callback();
});
return true;
case 'terminateCustomFilters':
if ( frameId === false ) { return false; }
terminateCustomFilters(tabId, frameId).then(( ) => {
callback();
});
return true;
case 'injectCustomFilters':
if ( frameId === false ) { return false; }
injectCustomFilters(tabId, frameId, request.hostname).then(selectors => {
@ -206,17 +221,11 @@ function onMessage(request, sender, callback) {
});
return true;
case 'uninjectCustomFilters':
if ( frameId === false ) { return false; }
uninjectCustomFilters(tabId, frameId, request.hostname).then(( ) => {
callback();
});
return true;
case 'injectCSSProceduralAPI':
browser.scripting.executeScript({
files: [ '/js/scripting/css-procedural-api.js' ],
target: { tabId, frameIds: [ frameId ] },
injectImmediately: true,
}).catch(reason => {
console.log(reason);
}).then(( ) => {
@ -503,7 +512,11 @@ function onCommand(command, tab) {
case 'enter-picker-mode': {
if ( browser.scripting === undefined ) { return; }
browser.scripting.executeScript({
files: [ '/js/scripting/tool-overlay.js', '/js/scripting/picker.js' ],
files: [
'/js/scripting/css-procedural-api.js',
'/js/scripting/tool-overlay.js',
'/js/scripting/picker.js',
],
target: { tabId: tab.id },
});
break;

View file

@ -50,7 +50,9 @@ export async function selectorsFromCustomFilters(hostname) {
for ( let i = 0; i < promises.length; i++ ) {
const selectors = results[i];
if ( selectors === undefined ) { continue; }
selectors.forEach(selector => { out.push(selector.slice(1)); });
selectors.forEach(selector => {
out.push(selector.startsWith('0') ? selector.slice(1) : selector);
});
}
return out.sort();
}
@ -64,38 +66,71 @@ export async function hasCustomFilters(hostname) {
/******************************************************************************/
export async function injectCustomFilters(tabId, frameId, hostname) {
const selectors = await selectorsFromCustomFilters(hostname);
if ( selectors.length === 0 ) { return; }
await browser.scripting.insertCSS({
css: `${selectors.join(',\n')}{display:none!important;}`,
origin: 'USER',
target: { tabId, frameIds: [ frameId ] },
}).catch(reason => {
console.log(reason);
});
return selectors;
async function getAllCustomFilterKeys() {
const storageKeys = await localKeys() || [];
return storageKeys.filter(a => a.startsWith('site.'));
}
/******************************************************************************/
export async function uninjectCustomFilters(tabId, frameId, hostname) {
const selectors = await selectorsFromCustomFilters(hostname);
if ( selectors.length === 0 ) { return; }
return browser.scripting.removeCSS({
css: `${selectors.join(',\n')}{display:none!important;}`,
origin: 'USER',
export function startCustomFilters(tabId, frameId) {
return browser.scripting.executeScript({
files: [ '/js/scripting/css-user.js' ],
target: { tabId, frameIds: [ frameId ] },
injectImmediately: true,
}).catch(reason => {
console.log(reason);
});
})
}
export function terminateCustomFilters(tabId, frameId) {
return browser.scripting.executeScript({
files: [ '/js/scripting/css-user-terminate.js' ],
target: { tabId, frameIds: [ frameId ] },
injectImmediately: true,
}).catch(reason => {
console.log(reason);
})
}
/******************************************************************************/
export async function injectCustomFilters(tabId, frameId, hostname) {
const selectors = await selectorsFromCustomFilters(hostname);
if ( selectors.length === 0 ) { return; }
const promises = [];
const plainSelectors = selectors.filter(a => a.startsWith('{') === false);
if ( plainSelectors.length !== 0 ) {
promises.push(
browser.scripting.insertCSS({
css: `${plainSelectors.join(',\n')}{display:none!important;}`,
origin: 'USER',
target: { tabId, frameIds: [ frameId ] },
}).catch(reason => {
console.log(reason);
})
);
}
const proceduralSelectors = selectors.filter(a => a.startsWith('{'));
if ( proceduralSelectors.length !== 0 ) {
promises.push(
browser.scripting.executeScript({
files: [ '/js/scripting/css-procedural-api.js' ],
target: { tabId, frameIds: [ frameId ] },
injectImmediately: true,
}).catch(reason => {
console.log(reason);
})
);
}
await Promise.all(promises);
return { plainSelectors, proceduralSelectors };
}
/******************************************************************************/
export async function registerCustomFilters(context) {
const storageKeys = await localKeys() || [];
const siteKeys = storageKeys.filter(a => a.startsWith('site.'));
const siteKeys = await getAllCustomFilterKeys();
if ( siteKeys.length === 0 ) { return; }
const { none } = context.filteringModeDetails;
@ -133,9 +168,8 @@ export async function registerCustomFilters(context) {
export async function addCustomFilter(hostname, selector) {
const key = `site.${hostname}`;
const selectors = await localRead(key) || [];
const filter = `0${selector}`;
if ( selectors.includes(filter) ) { return false; }
selectors.push(filter);
if ( selectors.includes(selector) ) { return false; }
selectors.push(selector);
selectors.sort();
await localWrite(key, selectors);
return true;
@ -147,7 +181,7 @@ export async function removeCustomFilter(hostname, selector) {
const key = `site.${hostname}`;
const selectors = await localRead(key);
if ( selectors === undefined ) { return false; }
const i = selectors.indexOf(`0${selector}`);
const i = selectors.indexOf(selector);
if ( i === -1 ) { return false; }
selectors.splice(i, 1);
await selectors.length !== 0

View file

@ -1,6 +1,6 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
uBlock Origin Lite - a comprehensive, MV3-compliant content blocker
Copyright (C) 2025-present Raymond Hill
This program is free software: you can redistribute it and/or modify
@ -21,27 +21,27 @@
import { dom, qs$, qsa$ } from './dom.js';
import { localRead, localWrite } from './ext.js';
import { ExtSelectorCompiler } from './static-filtering-parser.js';
import { toolOverlay } from './tool-overlay-ui.js';
/******************************************************************************/
const selectorCompiler = new ExtSelectorCompiler({ nativeCssHas: true });
let selectorPartsDB = new Map();
let sliderParts = [];
let sliderPartsPos = -1;
let previewCSS = '';
/******************************************************************************/
function isValidSelector(selector) {
isValidSelector.error = undefined;
if ( selector === '' ) { return false; }
try {
void document.querySelector(`${selector},a`);
} catch (reason) {
isValidSelector.error = reason;
return false;
function validateSelector(selector) {
validateSelector.error = undefined;
if ( selector === '' ) { return; }
const result = {};
if ( selectorCompiler.compile(selector, result) ) {
return result.compiled;
}
return true;
validateSelector.error = 'Error';
}
/******************************************************************************/
@ -219,31 +219,22 @@ function updatePreview(state) {
} else {
dom.cl.toggle(dom.root, 'preview', state)
}
if ( previewCSS !== '' ) {
toolOverlay.postMessage({ what: 'removeCSS', css: previewCSS });
previewCSS = '';
}
if ( state === false ) { return; }
const selector = qs$('textarea').value;
if ( isValidSelector(selector) === false ) { return; }
previewCSS = `${selector}{display:none!important;}`;
toolOverlay.postMessage({ what: 'insertCSS', css: previewCSS });
const selector = state && validateSelector(qs$('textarea').value) || '';
return toolOverlay.postMessage({ what: 'previewSelector', selector });
}
/******************************************************************************/
async function onCreateClicked() {
const selector = qs$('textarea').value;
if ( isValidSelector(selector) === false ) { return; }
await toolOverlay.postMessage({ what: 'uninjectCustomFilters' }).then(( ) =>
toolOverlay.sendMessage({
what: 'addCustomFilter',
hostname: toolOverlay.url.hostname,
selector,
})
).then(( ) =>
toolOverlay.postMessage({ what: 'injectCustomFilters' })
);
const selector = validateSelector(qs$('textarea').value);
if ( selector === undefined ) { return; }
await toolOverlay.postMessage({ what: 'terminateCustomFilters' });
await toolOverlay.sendMessage({
what: 'addCustomFilter',
hostname: toolOverlay.url.hostname,
selector,
});
await toolOverlay.postMessage({ what: 'startCustomFilters' });
qs$('textarea').value = '';
dom.cl.remove(dom.root, 'preview');
quitPicker();
@ -329,10 +320,10 @@ function showDialog(msg) {
/******************************************************************************/
function highlightCandidate() {
const selector = qs$('textarea').value;
if ( isValidSelector(selector) === false ) {
const selector = validateSelector(qs$('textarea').value);
if ( selector === undefined ) {
toolOverlay.postMessage({ what: 'unhighlight' });
updateElementCount({ count: 0, error: isValidSelector.error });
updateElementCount({ count: 0, error: validateSelector.error });
return;
}
toolOverlay.postMessage({
@ -366,11 +357,7 @@ function pausePicker() {
function unpausePicker() {
dom.cl.remove(dom.root, 'paused', 'preview');
dom.cl.add(dom.root, 'minimized');
updatePreview();
toolOverlay.postMessage({
what: 'togglePreview',
state: false,
});
updatePreview(false);
toolOverlay.highlightElementUnderMouse(true);
}

View file

@ -265,7 +265,11 @@ dom.on('#gotoZapper', 'click', ( ) => {
dom.on('#gotoPicker', 'click', ( ) => {
if ( browser.scripting === undefined ) { return; }
browser.scripting.executeScript({
files: [ '/js/scripting/tool-overlay.js', '/js/scripting/picker.js' ],
files: [
'/js/scripting/css-procedural-api.js',
'/js/scripting/tool-overlay.js',
'/js/scripting/picker.js',
],
target: { tabId: currentTab.id },
});
self.close();
@ -276,7 +280,10 @@ dom.on('#gotoPicker', 'click', ( ) => {
dom.on('#gotoUnpicker', 'click', ( ) => {
if ( browser.scripting === undefined ) { return; }
browser.scripting.executeScript({
files: [ '/js/scripting/tool-overlay.js', '/js/scripting/unpicker.js' ],
files: [
'/js/scripting/tool-overlay.js',
'/js/scripting/unpicker.js',
],
target: { tabId: currentTab.id },
});
self.close();

View file

@ -23,8 +23,8 @@
// Isolate from global scope
(function uBOL_cssProceduralAPI() {
if ( self.cssProceduralAPI !== undefined ) {
if ( self.cssProceduralAPI instanceof Promise === false ) { return; }
if ( self.ProceduralFiltererAPI !== undefined ) {
if ( self.ProceduralFiltererAPI instanceof Promise === false ) { return; }
}
/******************************************************************************/
@ -55,11 +55,21 @@ const regexFromString = (s, exact = false) => {
return new RegExp(exact ? `^${reStr}$` : reStr);
};
const randomToken = ( ) => {
const n = Math.random();
return String.fromCharCode(n * 25 + 97) +
Math.floor(
(0.25 + n * 0.75) * Number.MAX_SAFE_INTEGER
).toString(36).slice(-8);
};
/******************************************************************************/
// 'P' stands for 'Procedural'
class PSelectorTask {
destructor() {
}
begin() {
}
end() {
@ -69,7 +79,7 @@ class PSelectorTask {
/******************************************************************************/
class PSelectorVoidTask extends PSelectorTask {
constructor(task) {
constructor(filterer, task) {
super();
console.info(`uBO: :${task[0]}() operator does not exist`);
}
@ -80,7 +90,7 @@ class PSelectorVoidTask extends PSelectorTask {
/******************************************************************************/
class PSelectorHasTextTask extends PSelectorTask {
constructor(task) {
constructor(filterer, task) {
super();
this.needle = regexFromString(task[1]);
}
@ -94,26 +104,26 @@ class PSelectorHasTextTask extends PSelectorTask {
/******************************************************************************/
class PSelectorIfTask extends PSelectorTask {
constructor(task) {
constructor(filterer, task) {
super();
this.pselector = new PSelector(task[1]);
this.pselector = new PSelector(filterer, task[1]);
}
transpose(node, output) {
if ( this.pselector.test(node) === this.target ) {
output.push(node);
}
}
target = true;
}
PSelectorIfTask.prototype.target = true;
class PSelectorIfNotTask extends PSelectorIfTask {
target = false;
}
PSelectorIfNotTask.prototype.target = false;
/******************************************************************************/
class PSelectorMatchesAttrTask extends PSelectorTask {
constructor(task) {
constructor(filterer, task) {
super();
this.reAttr = regexFromString(task[1].attr, true);
this.reValue = regexFromString(task[1].value, true);
@ -132,7 +142,7 @@ class PSelectorMatchesAttrTask extends PSelectorTask {
/******************************************************************************/
class PSelectorMatchesCSSTask extends PSelectorTask {
constructor(task) {
constructor(filterer, task) {
super();
this.name = task[1].name;
this.pseudo = task[1].pseudo ? `::${task[1].pseudo}` : null;
@ -150,15 +160,15 @@ class PSelectorMatchesCSSTask extends PSelectorTask {
}
}
class PSelectorMatchesCSSAfterTask extends PSelectorMatchesCSSTask {
constructor(task) {
super(task);
constructor(filterer, task) {
super(filterer, task);
this.pseudo = '::after';
}
}
class PSelectorMatchesCSSBeforeTask extends PSelectorMatchesCSSTask {
constructor(task) {
super(task);
constructor(filterer, task) {
super(filterer, task);
this.pseudo = '::before';
}
}
@ -166,26 +176,32 @@ class PSelectorMatchesCSSBeforeTask extends PSelectorMatchesCSSTask {
/******************************************************************************/
class PSelectorMatchesMediaTask extends PSelectorTask {
constructor(task) {
constructor(filterer, task) {
super();
this.filterer = filterer;
this.mql = window.matchMedia(task[1]);
if ( this.mql.media === 'not all' ) { return; }
this.mql.addEventListener('change', ( ) => {
const { proceduralFilterer } = self.cssProceduralAPI;
if ( proceduralFilterer instanceof Object === false ) { return; }
proceduralFilterer.uBOL_DOMChanged();
});
this.boundHandler = this.handler.bind(this);
this.mql.addEventListener('change', this.boundHandler);
}
destructor() {
super.destructor();
this.mql.removeEventListener('change', this.boundHandler);
}
transpose(node, output) {
if ( this.mql.matches === false ) { return; }
output.push(node);
}
handler() {
if ( this.filterer instanceof Object === false ) { return; }
this.filterer.uBOL_DOMChanged();
}
}
/******************************************************************************/
class PSelectorMatchesPathTask extends PSelectorTask {
constructor(task) {
constructor(filterer, task) {
super();
this.needle = regexFromString(
task[1].replace(/\P{ASCII}/gu, s => encodeURIComponent(s))
@ -201,7 +217,7 @@ class PSelectorMatchesPathTask extends PSelectorTask {
/******************************************************************************/
class PSelectorMatchesPropTask extends PSelectorTask {
constructor(task) {
constructor(filterer, task) {
super();
this.props = task[1].attr.split('.');
this.reValue = task[1].value !== ''
@ -227,7 +243,7 @@ class PSelectorMatchesPropTask extends PSelectorTask {
/******************************************************************************/
class PSelectorMinTextLengthTask extends PSelectorTask {
constructor(task) {
constructor(filterer, task) {
super();
this.min = task[1];
}
@ -298,7 +314,7 @@ class PSelectorOthersTask extends PSelectorTask {
/******************************************************************************/
class PSelectorShadowTask extends PSelectorTask {
constructor(task) {
constructor(filterer, task) {
super();
this.selector = task[1];
}
@ -332,7 +348,7 @@ class PSelectorShadowTask extends PSelectorTask {
// https://github.com/AdguardTeam/ExtendedCss/issues/31#issuecomment-302391277
// Prepend `:scope ` if needed.
class PSelectorSpathTask extends PSelectorTask {
constructor(task) {
constructor(filterer, task) {
super();
this.spath = task[1];
this.nth = /^(?:\s*[+~]|:)/.test(this.spath);
@ -368,7 +384,7 @@ class PSelectorSpathTask extends PSelectorTask {
/******************************************************************************/
class PSelectorUpwardTask extends PSelectorTask {
constructor(task) {
constructor(filterer, task) {
super();
const arg = task[1];
if ( typeof arg === 'number' ) {
@ -394,15 +410,16 @@ class PSelectorUpwardTask extends PSelectorTask {
}
output.push(node);
}
i = 0;
s = '';
}
PSelectorUpwardTask.prototype.i = 0;
PSelectorUpwardTask.prototype.s = '';
/******************************************************************************/
class PSelectorWatchAttrs extends PSelectorTask {
constructor(task) {
constructor(filterer, task) {
super();
this.filterer = filterer;
this.observer = null;
this.observed = new WeakSet();
this.observerOptions = {
@ -414,17 +431,22 @@ class PSelectorWatchAttrs extends PSelectorTask {
this.observerOptions.attributeFilter = task[1];
}
}
// TODO: Is it worth trying to re-apply only the current selector?
handler() {
const { proceduralFilterer } = self.cssProceduralAPI;
if ( proceduralFilterer instanceof Object === false ) { return; }
proceduralFilterer.uBOL_DOMChanged();
destructor() {
super.destructor();
if ( this.observer ) {
this.observer.takeRecords();
this.observer.disconnect();
this.observer = null;
}
}
transpose(node, output) {
output.push(node);
if ( this.filterer instanceof Object === false ) { return; }
if ( this.observed.has(node) ) { return; }
if ( this.observer === null ) {
this.observer = new MutationObserver(this.handler);
this.observer = new MutationObserver(( ) => {
this.filterer.uBOL_DOMChanged();
});
}
this.observer.observe(node, this.observerOptions);
this.observed.add(node);
@ -434,7 +456,7 @@ class PSelectorWatchAttrs extends PSelectorTask {
/******************************************************************************/
class PSelectorXpathTask extends PSelectorTask {
constructor(task) {
constructor(filterer, task) {
super();
this.xpe = document.createExpression(task[1], null);
this.xpr = null;
@ -458,17 +480,22 @@ class PSelectorXpathTask extends PSelectorTask {
/******************************************************************************/
class PSelector {
constructor(o) {
constructor(filterer, o) {
this.selector = o.selector;
this.tasks = [];
const tasks = [];
if ( Array.isArray(o.tasks) === false ) { return; }
for ( const task of o.tasks ) {
const ctor = this.operatorToTaskMap.get(task[0]) || PSelectorVoidTask;
tasks.push(new ctor(task));
const ctor = PSelector.operatorToTaskMap.get(task[0]) || PSelectorVoidTask;
tasks.push(new ctor(filterer, task));
}
this.tasks = tasks;
}
destructor() {
for ( const task of this.tasks ) {
task.destructor();
}
}
prime(input) {
const root = input || document;
if ( this.selector === '' ) { return [ root ]; }
@ -514,34 +541,34 @@ class PSelector {
}
return false;
}
static operatorToTaskMap = new Map([
[ 'has', PSelectorIfTask ],
[ 'has-text', PSelectorHasTextTask ],
[ 'if', PSelectorIfTask ],
[ 'if-not', PSelectorIfNotTask ],
[ 'matches-attr', PSelectorMatchesAttrTask ],
[ 'matches-css', PSelectorMatchesCSSTask ],
[ 'matches-css-after', PSelectorMatchesCSSAfterTask ],
[ 'matches-css-before', PSelectorMatchesCSSBeforeTask ],
[ 'matches-media', PSelectorMatchesMediaTask ],
[ 'matches-path', PSelectorMatchesPathTask ],
[ 'matches-prop', PSelectorMatchesPropTask ],
[ 'min-text-length', PSelectorMinTextLengthTask ],
[ 'not', PSelectorIfNotTask ],
[ 'others', PSelectorOthersTask ],
[ 'shadow', PSelectorShadowTask ],
[ 'spath', PSelectorSpathTask ],
[ 'upward', PSelectorUpwardTask ],
[ 'watch-attr', PSelectorWatchAttrs ],
[ 'xpath', PSelectorXpathTask ],
]);
}
PSelector.prototype.operatorToTaskMap = new Map([
[ 'has', PSelectorIfTask ],
[ 'has-text', PSelectorHasTextTask ],
[ 'if', PSelectorIfTask ],
[ 'if-not', PSelectorIfNotTask ],
[ 'matches-attr', PSelectorMatchesAttrTask ],
[ 'matches-css', PSelectorMatchesCSSTask ],
[ 'matches-css-after', PSelectorMatchesCSSAfterTask ],
[ 'matches-css-before', PSelectorMatchesCSSBeforeTask ],
[ 'matches-media', PSelectorMatchesMediaTask ],
[ 'matches-path', PSelectorMatchesPathTask ],
[ 'matches-prop', PSelectorMatchesPropTask ],
[ 'min-text-length', PSelectorMinTextLengthTask ],
[ 'not', PSelectorIfNotTask ],
[ 'others', PSelectorOthersTask ],
[ 'shadow', PSelectorShadowTask ],
[ 'spath', PSelectorSpathTask ],
[ 'upward', PSelectorUpwardTask ],
[ 'watch-attr', PSelectorWatchAttrs ],
[ 'xpath', PSelectorXpathTask ],
]);
/******************************************************************************/
class PSelectorRoot extends PSelector {
constructor(o) {
super(o);
constructor(filterer, o) {
super(filterer, o);
this.budget = 200; // I arbitrary picked a 1/5 second
this.raw = o.raw;
this.cost = 0;
@ -569,16 +596,39 @@ class PSelectorRoot extends PSelector {
class ProceduralFilterer {
constructor() {
this.selectors = [];
this.masterToken = this.randomToken();
this.styleTokenMap = new Map();
this.styledNodes = new Set();
this.timer = undefined;
this.hideStyle = 'display:none!important;';
}
async reset() {
if ( this.timer ) {
self.cancelAnimationFrame(this.timer);
this.timer = undefined;
}
for ( const pselector of this.selectors.values() ) {
pselector.destructor();
}
this.selectors.length = 0;
const promises = [];
for ( const [ style, token ] of this.styleTokenMap ) {
for ( const elem of this.styledNodes ) {
elem.removeAttribute(token);
}
const css = `[${token}]\n{${style}}\n`;
promises.push(
chrome.runtime.sendMessage({ what: 'removeCSS', css }).catch(( ) => { })
);
}
this.styleTokenMap.clear();
this.styledNodes.clear();
return Promise.all(promises);
}
addSelectors(selectors) {
for ( const selector of selectors ) {
const pselector = new PSelectorRoot(selector);
const pselector = new PSelectorRoot(this, selector);
this.primeProceduralSelector(pselector);
this.selectors.push(pselector);
}
@ -636,9 +686,9 @@ class ProceduralFilterer {
if ( style === undefined ) { return; }
let styleToken = this.styleTokenMap.get(style);
if ( styleToken !== undefined ) { return styleToken; }
styleToken = this.randomToken();
styleToken = randomToken();
this.styleTokenMap.set(style, styleToken);
uBOL_injectCSS(`[${this.masterToken}][${styleToken}]\n{${style}}\n`);
uBOL_injectCSS(`[${styleToken}]\n{${style}}\n`);
return styleToken;
}
@ -653,7 +703,6 @@ class ProceduralFilterer {
arg === '' ? this.hideStyle : arg
);
for ( const node of nodes ) {
node.setAttribute(this.masterToken, '');
node.setAttribute(styleToken, '');
this.styledNodes.add(node);
}
@ -692,24 +741,16 @@ class ProceduralFilterer {
}
}
// TODO: Current assumption is one style per hit element. Could be an
// issue if an element has multiple styling and one styling is
// brought back. Possibly too rare to care about this for now.
unprocessNodes(nodes) {
const tokens = Array.from(this.styleTokenMap.values());
for ( const node of nodes ) {
if ( this.styledNodes.has(node) ) { continue; }
node.removeAttribute(this.masterToken);
for ( const token of tokens ) {
node.removeAttribute(token);
}
}
}
randomToken() {
const n = Math.random();
return String.fromCharCode(n * 25 + 97) +
Math.floor(
(0.25 + n * 0.75) * Number.MAX_SAFE_INTEGER
).toString(36).slice(-8);
}
uBOL_DOMChanged() {
if ( this.timer !== undefined ) { return; }
this.timer = self.requestAnimationFrame(( ) => {
@ -721,9 +762,24 @@ class ProceduralFilterer {
/******************************************************************************/
self.cssProceduralAPI = {
proceduralFilterer: null,
domObserver: null,
self.ProceduralFiltererAPI = class {
constructor() {
this.proceduralFilterer = null;
this.domObserver = null;
}
async reset() {
if ( this.domObserver ) {
this.domObserver.takeRecords();
this.domObserver.disconnect();
this.domObserver = null;
}
if ( this.proceduralFilterer ) {
await this.proceduralFilterer.reset();
this.proceduralFilterer = null;
}
}
addSelectors(selectors) {
if ( this.proceduralFilterer === null ) {
this.proceduralFilterer = new ProceduralFilterer();
@ -732,14 +788,18 @@ self.cssProceduralAPI = {
this.domObserver = new MutationObserver(mutations => {
this.onDOMChanged(mutations);
});
this.domObserver.observe(document, {
childList: true,
subtree: true,
});
this.domObserver.observe(document, { childList: true, subtree: true });
}
this.proceduralFilterer.addSelectors(selectors);
this.proceduralFilterer.uBOL_commit();
},
}
qsa(selector) {
const o = JSON.parse(selector);
const pselector = new PSelectorRoot(null, o);
return pselector.exec();
}
onDOMChanged(mutations) {
for ( const mutation of mutations ) {
for ( const added of mutation.addedNodes ) {
@ -751,13 +811,11 @@ self.cssProceduralAPI = {
return this.proceduralFilterer.uBOL_DOMChanged();
}
}
},
}
};
/******************************************************************************/
})();
/******************************************************************************/
void 0;

View file

@ -112,17 +112,20 @@ if ( declaratives.length !== 0 ) {
const procedurals = exceptedSelectors.filter(a => a.cssable === undefined);
if ( procedurals.length !== 0 ) {
const addSelectors = selectors => {
if ( self.cssProceduralAPI instanceof Object === false ) { return; }
self.cssProceduralAPI.addSelectors(selectors);
if ( self.listsProceduralFiltererAPI instanceof Object === false ) { return; }
self.listsProceduralFiltererAPI.addSelectors(selectors);
};
if ( self.cssProceduralAPI === undefined ) {
self.cssProceduralAPI = chrome.runtime.sendMessage({
if ( self.ProceduralFiltererAPI === undefined ) {
self.ProceduralFiltererAPI = chrome.runtime.sendMessage({
what: 'injectCSSProceduralAPI'
}).catch(( ) => {
});
}
if ( self.cssProceduralAPI instanceof Promise ) {
self.cssProceduralAPI.then(( ) => { addSelectors(procedurals); });
if ( self.ProceduralFiltererAPI instanceof Promise ) {
self.ProceduralFiltererAPI.then(( ) => {
self.listsProceduralFiltererAPI = new self.ProceduralFiltererAPI();
addSelectors(procedurals);
});
} else {
addSelectors(procedurals);
}

View file

@ -0,0 +1,45 @@
/*******************************************************************************
uBlock Origin Lite - a comprehensive, MV3-compliant content blocker
Copyright (C) 2019-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
*/
(function uBOL_cssUserTerminate() {
/******************************************************************************/
const plainSelectors = self.customFilters?.plainSelectors;
if ( plainSelectors ) {
chrome.runtime.sendMessage({
what: 'removeCSS',
css: `${plainSelectors.join(',\n')}{display:none!important;}`,
}).catch(( ) => {
});
}
if ( self.customProceduralFiltererAPI instanceof Object ) {
self.customProceduralFiltererAPI.reset();
}
self.customFilters = undefined;
/******************************************************************************/
})();
void 0;

View file

@ -24,12 +24,23 @@
/******************************************************************************/
const docURL = new URL(document.baseURI);
chrome.runtime.sendMessage({
const details = await chrome.runtime.sendMessage({
what: 'injectCustomFilters',
hostname: docURL.hostname,
}).catch(( ) => {
});
if ( details?.proceduralSelectors?.length ) {
if ( self.ProceduralFiltererAPI ) {
self.customProceduralFiltererAPI = new self.ProceduralFiltererAPI();
self.customProceduralFiltererAPI.addSelectors(
details.proceduralSelectors.map(a => JSON.parse(a))
);
}
}
self.customFilters = details;
/******************************************************************************/
})();

View file

@ -1,6 +1,6 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
uBlock Origin Lite - a comprehensive, MV3-compliant content blocker
Copyright (C) 2025-present Raymond Hill
This program is free software: you can redistribute it and/or modify
@ -237,22 +237,55 @@ const excludedSelectors = [
/******************************************************************************/
async function previewSelector(selector) {
if ( selector === previewedSelector ) { return; }
if ( previewedSelector !== '' ) {
if ( previewedSelector.startsWith('{') ) {
if ( self.pickerProceduralFilteringAPI ) {
await self.pickerProceduralFilteringAPI.reset();
}
}
if ( previewedCSS !== '' ) {
await ubolOverlay.sendMessage({ what: 'removeCSS', css: previewedCSS });
previewedCSS = '';
}
}
previewedSelector = selector || '';
if ( selector === '' ) { return; }
if ( selector.startsWith('{') ) {
if ( self.ProceduralFiltererAPI === undefined ) { return; }
if ( self.pickerProceduralFilteringAPI === undefined ) {
self.pickerProceduralFilteringAPI = new self.ProceduralFiltererAPI();
}
self.pickerProceduralFilteringAPI.addSelectors([ JSON.parse(selector) ]);
return;
}
previewedCSS = `${selector}{display:none!important;}`;
await ubolOverlay.sendMessage({ what: 'insertCSS', css: previewedCSS });
}
let previewedSelector = '';
let previewedCSS = '';
/******************************************************************************/
const previewProceduralFiltererAPI = new self.ProceduralFiltererAPI();
/******************************************************************************/
function onMessage(msg) {
switch ( msg.what ) {
case 'injectCustomFilters':
return ubolOverlay.sendMessage({ what: 'injectCustomFilters',
hostname: ubolOverlay.url.hostname,
});
case 'uninjectCustomFilters':
return ubolOverlay.sendMessage({ what: 'uninjectCustomFilters',
hostname: ubolOverlay.url.hostname,
});
case 'quitTool':
previewProceduralFiltererAPI.reset();
break;
case 'startCustomFilters':
return ubolOverlay.sendMessage({ what: 'startCustomFilters' });
case 'terminateCustomFilters':
return ubolOverlay.sendMessage({ what: 'terminateCustomFilters' });
case 'candidatesAtPoint':
return candidatesAtPoint(msg.mx, msg.my, msg.broad);
case 'insertCSS':
return ubolOverlay.sendMessage(msg);
case 'removeCSS':
return ubolOverlay.sendMessage(msg);
case 'previewSelector':
return previewSelector(msg.selector);
default:
break;
}

View file

@ -1,6 +1,6 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
uBlock Origin Lite - a comprehensive, MV3-compliant content blocker
Copyright (C) 2025-present Raymond Hill
This program is free software: you can redistribute it and/or modify
@ -254,14 +254,20 @@ self.ubolOverlay = {
},
qsa(node, selector) {
if ( node !== null ) {
try {
const elems = node.querySelectorAll(selector);
this.qsa.error = undefined;
return elems;
} catch (reason) {
this.qsa.error = `${reason}`;
if ( node === null ) { return []; }
if ( selector.startsWith('{') ) {
if ( this.proceduralFiltererAPI === undefined ) {
if ( self.ProceduralFiltererAPI === undefined ) { return []; }
this.proceduralFiltererAPI = new self.ProceduralFiltererAPI();
}
return this.proceduralFiltererAPI.qsa(selector);
}
try {
const elems = node.querySelectorAll(selector);
this.qsa.error = undefined;
return elems;
} catch (reason) {
this.qsa.error = `${reason}`;
}
return [];
},

View file

@ -1,6 +1,6 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
uBlock Origin Lite - a comprehensive, MV3-compliant content blocker
Copyright (C) 2025-present Raymond Hill
This program is free software: you can redistribute it and/or modify
@ -31,14 +31,10 @@ if ( ubolOverlay.file === '/unpicker-ui.html' ) { return; }
function onMessage(msg) {
switch ( msg.what ) {
case 'injectCustomFilters':
return ubolOverlay.sendMessage({ what: 'injectCustomFilters',
hostname: ubolOverlay.url.hostname,
});
case 'uninjectCustomFilters':
return ubolOverlay.sendMessage({ what: 'uninjectCustomFilters',
hostname: ubolOverlay.url.hostname,
});
case 'startCustomFilters':
return ubolOverlay.sendMessage({ what: 'startCustomFilters' });
case 'terminateCustomFilters':
return ubolOverlay.sendMessage({ what: 'terminateCustomFilters' });
case 'removeCustomFilter':
return ubolOverlay.sendMessage({ what: 'removeCustomFilter',
hostname: ubolOverlay.url.hostname,

View file

@ -1,6 +1,6 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
uBlock Origin Lite - a comprehensive, MV3-compliant content blocker
Copyright (C) 2025-present Raymond Hill
This program is free software: you can redistribute it and/or modify

View file

@ -1,6 +1,6 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
uBlock Origin Lite - a comprehensive, MV3-compliant content blocker
Copyright (C) 2025-present Raymond Hill
This program is free software: you can redistribute it and/or modify

View file

@ -1,6 +1,6 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
uBlock Origin Lite - a comprehensive, MV3-compliant content blocker
Copyright (C) 2025-present Raymond Hill
This program is free software: you can redistribute it and/or modify
@ -33,8 +33,8 @@ function onMinimizeClicked() {
function highlight() {
const selectors = [];
for ( const selectorElem of qsa$('#customFilters .customFilter.on > span.selector') ) {
selectors.push(selectorElem.textContent);
for ( const selectorElem of qsa$('#customFilters .customFilter.on') ) {
selectors.push(selectorElem.dataset.selector);
}
if ( selectors.length !== 0 ) {
toolOverlay.postMessage({
@ -64,7 +64,7 @@ function onFilterClicked(ev) {
highlight();
return;
}
const selector = selectorElem.textContent;
const selector = filterElem.dataset.selector;
const trashElem = qs$(filterElem, ':scope > span.remove');
if ( target === trashElem ) {
dom.cl.add(filterElem, 'removed');
@ -111,9 +111,16 @@ function populateFilters(selectors) {
dom.clear(container);
const rowTemplate = qs$('template#customFilterRow');
for ( const selector of selectors ) {
const row = rowTemplate.content.cloneNode(true);
qs$(row, '.customFilter > span.selector').textContent = selector;
container.append(row);
const fragment = rowTemplate.content.cloneNode(true);
const row = qs$(fragment, '.customFilter');
row.dataset.selector = selector;
let text = selector;
if ( selector.startsWith('{') ) {
const o = JSON.parse(selector);
text = o.raw;
}
qs$(row, '.selector').textContent = text;
container.append(fragment);
}
faIconsInit(container);
autoSelectFilter();
@ -129,8 +136,8 @@ async function startUnpicker() {
if ( selectors.length === 0 ) {
return quitUnpicker();
}
await toolOverlay.postMessage({ what: 'terminateCustomFilters' });
await toolOverlay.postMessage({ what: 'startTool' });
await toolOverlay.postMessage({ what: 'uninjectCustomFilters' });
populateFilters(selectors);
dom.on('#minimize', 'click', onMinimizeClicked);
dom.on('#customFilters', 'click', onFilterClicked);
@ -140,7 +147,7 @@ async function startUnpicker() {
/******************************************************************************/
async function quitUnpicker() {
await toolOverlay.postMessage({ what: 'injectCustomFilters' });
await toolOverlay.postMessage({ what: 'startCustomFilters' });
toolOverlay.stop();
}

View file

@ -1,6 +1,6 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
uBlock Origin Lite - a comprehensive, MV3-compliant content blocker
Copyright (C) 2025-present Raymond Hill
This program is free software: you can redistribute it and/or modify
@ -33,7 +33,8 @@ function onSvgClicked(ev) {
my: ev.clientY,
options: {
stay: true,
highlight: ev.target !== toolOverlay.svgIslands,
highlight: dom.cl.has(dom.root, 'mobile') &&
ev.target !== toolOverlay.svgIslands,
},
});
}

View file

@ -3199,8 +3199,8 @@ export const netOptionTokenDescriptors = new Map([
// https://github.com/uBlockOrigin/uBlock-issues/issues/89
// Do not discard unknown pseudo-elements.
class ExtSelectorCompiler {
constructor(instanceOptions) {
export class ExtSelectorCompiler {
constructor(instanceOptions = {}) {
this.reParseRegexLiteral = /^\/(.+)\/([imu]+)?$/;
// Use a regex for most common CSS selectors known to be valid in any