Skip to content

Commit 32bf5eb

Browse files
committed
[mv3] Add support for procedural cosmetic filters
Related issue: uBlockOrigin/uBOL-home#325
1 parent e713e13 commit 32bf5eb

16 files changed

+414
-213
lines changed

platform/mv3/extension/js/background.js

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ import {
3737
injectCustomFilters,
3838
removeCustomFilter,
3939
selectorsFromCustomFilters,
40-
uninjectCustomFilters,
40+
startCustomFilters,
41+
terminateCustomFilters,
4142
} from './filter-manager.js';
4243

4344
import {
@@ -199,24 +200,32 @@ function onMessage(request, sender, callback) {
199200
return false;
200201
}
201202

202-
case 'injectCustomFilters':
203+
case 'startCustomFilters':
203204
if ( frameId === false ) { return false; }
204-
injectCustomFilters(tabId, frameId, request.hostname).then(selectors => {
205-
callback(selectors);
205+
startCustomFilters(tabId, frameId).then(( ) => {
206+
callback();
206207
});
207208
return true;
208209

209-
case 'uninjectCustomFilters':
210+
case 'terminateCustomFilters':
210211
if ( frameId === false ) { return false; }
211-
uninjectCustomFilters(tabId, frameId, request.hostname).then(( ) => {
212+
terminateCustomFilters(tabId, frameId).then(( ) => {
212213
callback();
213214
});
214215
return true;
215216

217+
case 'injectCustomFilters':
218+
if ( frameId === false ) { return false; }
219+
injectCustomFilters(tabId, frameId, request.hostname).then(selectors => {
220+
callback(selectors);
221+
});
222+
return true;
223+
216224
case 'injectCSSProceduralAPI':
217225
browser.scripting.executeScript({
218226
files: [ '/js/scripting/css-procedural-api.js' ],
219227
target: { tabId, frameIds: [ frameId ] },
228+
injectImmediately: true,
220229
}).catch(reason => {
221230
console.log(reason);
222231
}).then(( ) => {
@@ -503,7 +512,11 @@ function onCommand(command, tab) {
503512
case 'enter-picker-mode': {
504513
if ( browser.scripting === undefined ) { return; }
505514
browser.scripting.executeScript({
506-
files: [ '/js/scripting/tool-overlay.js', '/js/scripting/picker.js' ],
515+
files: [
516+
'/js/scripting/css-procedural-api.js',
517+
'/js/scripting/tool-overlay.js',
518+
'/js/scripting/picker.js',
519+
],
507520
target: { tabId: tab.id },
508521
});
509522
break;

platform/mv3/extension/js/filter-manager.js

Lines changed: 57 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ export async function selectorsFromCustomFilters(hostname) {
5050
for ( let i = 0; i < promises.length; i++ ) {
5151
const selectors = results[i];
5252
if ( selectors === undefined ) { continue; }
53-
selectors.forEach(selector => { out.push(selector.slice(1)); });
53+
selectors.forEach(selector => {
54+
out.push(selector.startsWith('0') ? selector.slice(1) : selector);
55+
});
5456
}
5557
return out.sort();
5658
}
@@ -64,38 +66,71 @@ export async function hasCustomFilters(hostname) {
6466

6567
/******************************************************************************/
6668

67-
export async function injectCustomFilters(tabId, frameId, hostname) {
68-
const selectors = await selectorsFromCustomFilters(hostname);
69-
if ( selectors.length === 0 ) { return; }
70-
await browser.scripting.insertCSS({
71-
css: `${selectors.join(',\n')}{display:none!important;}`,
72-
origin: 'USER',
69+
async function getAllCustomFilterKeys() {
70+
const storageKeys = await localKeys() || [];
71+
return storageKeys.filter(a => a.startsWith('site.'));
72+
}
73+
74+
/******************************************************************************/
75+
76+
export function startCustomFilters(tabId, frameId) {
77+
return browser.scripting.executeScript({
78+
files: [ '/js/scripting/css-user.js' ],
7379
target: { tabId, frameIds: [ frameId ] },
80+
injectImmediately: true,
7481
}).catch(reason => {
7582
console.log(reason);
76-
});
77-
return selectors;
83+
})
84+
}
85+
86+
export function terminateCustomFilters(tabId, frameId) {
87+
return browser.scripting.executeScript({
88+
files: [ '/js/scripting/css-user-terminate.js' ],
89+
target: { tabId, frameIds: [ frameId ] },
90+
injectImmediately: true,
91+
}).catch(reason => {
92+
console.log(reason);
93+
})
7894
}
7995

8096
/******************************************************************************/
8197

82-
export async function uninjectCustomFilters(tabId, frameId, hostname) {
98+
export async function injectCustomFilters(tabId, frameId, hostname) {
8399
const selectors = await selectorsFromCustomFilters(hostname);
84100
if ( selectors.length === 0 ) { return; }
85-
return browser.scripting.removeCSS({
86-
css: `${selectors.join(',\n')}{display:none!important;}`,
87-
origin: 'USER',
88-
target: { tabId, frameIds: [ frameId ] },
89-
}).catch(reason => {
90-
console.log(reason);
91-
});
101+
const promises = [];
102+
const plainSelectors = selectors.filter(a => a.startsWith('{') === false);
103+
if ( plainSelectors.length !== 0 ) {
104+
promises.push(
105+
browser.scripting.insertCSS({
106+
css: `${plainSelectors.join(',\n')}{display:none!important;}`,
107+
origin: 'USER',
108+
target: { tabId, frameIds: [ frameId ] },
109+
}).catch(reason => {
110+
console.log(reason);
111+
})
112+
);
113+
}
114+
const proceduralSelectors = selectors.filter(a => a.startsWith('{'));
115+
if ( proceduralSelectors.length !== 0 ) {
116+
promises.push(
117+
browser.scripting.executeScript({
118+
files: [ '/js/scripting/css-procedural-api.js' ],
119+
target: { tabId, frameIds: [ frameId ] },
120+
injectImmediately: true,
121+
}).catch(reason => {
122+
console.log(reason);
123+
})
124+
);
125+
}
126+
await Promise.all(promises);
127+
return { plainSelectors, proceduralSelectors };
92128
}
93129

94130
/******************************************************************************/
95131

96132
export async function registerCustomFilters(context) {
97-
const storageKeys = await localKeys() || [];
98-
const siteKeys = storageKeys.filter(a => a.startsWith('site.'));
133+
const siteKeys = await getAllCustomFilterKeys();
99134
if ( siteKeys.length === 0 ) { return; }
100135

101136
const { none } = context.filteringModeDetails;
@@ -133,9 +168,8 @@ export async function registerCustomFilters(context) {
133168
export async function addCustomFilter(hostname, selector) {
134169
const key = `site.${hostname}`;
135170
const selectors = await localRead(key) || [];
136-
const filter = `0${selector}`;
137-
if ( selectors.includes(filter) ) { return false; }
138-
selectors.push(filter);
171+
if ( selectors.includes(selector) ) { return false; }
172+
selectors.push(selector);
139173
selectors.sort();
140174
await localWrite(key, selectors);
141175
return true;
@@ -147,7 +181,7 @@ export async function removeCustomFilter(hostname, selector) {
147181
const key = `site.${hostname}`;
148182
const selectors = await localRead(key);
149183
if ( selectors === undefined ) { return false; }
150-
const i = selectors.indexOf(`0${selector}`);
184+
const i = selectors.indexOf(selector);
151185
if ( i === -1 ) { return false; }
152186
selectors.splice(i, 1);
153187
await selectors.length !== 0

platform/mv3/extension/js/picker-ui.js

Lines changed: 26 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*******************************************************************************
22
3-
uBlock Origin - a comprehensive, efficient content blocker
3+
uBlock Origin Lite - a comprehensive, MV3-compliant content blocker
44
Copyright (C) 2025-present Raymond Hill
55
66
This program is free software: you can redistribute it and/or modify
@@ -21,27 +21,27 @@
2121

2222
import { dom, qs$, qsa$ } from './dom.js';
2323
import { localRead, localWrite } from './ext.js';
24+
import { ExtSelectorCompiler } from './static-filtering-parser.js';
2425
import { toolOverlay } from './tool-overlay-ui.js';
2526

2627
/******************************************************************************/
2728

29+
const selectorCompiler = new ExtSelectorCompiler({ nativeCssHas: true });
30+
2831
let selectorPartsDB = new Map();
2932
let sliderParts = [];
3033
let sliderPartsPos = -1;
31-
let previewCSS = '';
3234

3335
/******************************************************************************/
3436

35-
function isValidSelector(selector) {
36-
isValidSelector.error = undefined;
37-
if ( selector === '' ) { return false; }
38-
try {
39-
void document.querySelector(`${selector},a`);
40-
} catch (reason) {
41-
isValidSelector.error = reason;
42-
return false;
37+
function validateSelector(selector) {
38+
validateSelector.error = undefined;
39+
if ( selector === '' ) { return; }
40+
const result = {};
41+
if ( selectorCompiler.compile(selector, result) ) {
42+
return result.compiled;
4343
}
44-
return true;
44+
validateSelector.error = 'Error';
4545
}
4646

4747
/******************************************************************************/
@@ -219,31 +219,22 @@ function updatePreview(state) {
219219
} else {
220220
dom.cl.toggle(dom.root, 'preview', state)
221221
}
222-
if ( previewCSS !== '' ) {
223-
toolOverlay.postMessage({ what: 'removeCSS', css: previewCSS });
224-
previewCSS = '';
225-
}
226-
if ( state === false ) { return; }
227-
const selector = qs$('textarea').value;
228-
if ( isValidSelector(selector) === false ) { return; }
229-
previewCSS = `${selector}{display:none!important;}`;
230-
toolOverlay.postMessage({ what: 'insertCSS', css: previewCSS });
222+
const selector = state && validateSelector(qs$('textarea').value) || '';
223+
return toolOverlay.postMessage({ what: 'previewSelector', selector });
231224
}
232225

233226
/******************************************************************************/
234227

235228
async function onCreateClicked() {
236-
const selector = qs$('textarea').value;
237-
if ( isValidSelector(selector) === false ) { return; }
238-
await toolOverlay.postMessage({ what: 'uninjectCustomFilters' }).then(( ) =>
239-
toolOverlay.sendMessage({
240-
what: 'addCustomFilter',
241-
hostname: toolOverlay.url.hostname,
242-
selector,
243-
})
244-
).then(( ) =>
245-
toolOverlay.postMessage({ what: 'injectCustomFilters' })
246-
);
229+
const selector = validateSelector(qs$('textarea').value);
230+
if ( selector === undefined ) { return; }
231+
await toolOverlay.postMessage({ what: 'terminateCustomFilters' });
232+
await toolOverlay.sendMessage({
233+
what: 'addCustomFilter',
234+
hostname: toolOverlay.url.hostname,
235+
selector,
236+
});
237+
await toolOverlay.postMessage({ what: 'startCustomFilters' });
247238
qs$('textarea').value = '';
248239
dom.cl.remove(dom.root, 'preview');
249240
quitPicker();
@@ -329,10 +320,10 @@ function showDialog(msg) {
329320
/******************************************************************************/
330321

331322
function highlightCandidate() {
332-
const selector = qs$('textarea').value;
333-
if ( isValidSelector(selector) === false ) {
323+
const selector = validateSelector(qs$('textarea').value);
324+
if ( selector === undefined ) {
334325
toolOverlay.postMessage({ what: 'unhighlight' });
335-
updateElementCount({ count: 0, error: isValidSelector.error });
326+
updateElementCount({ count: 0, error: validateSelector.error });
336327
return;
337328
}
338329
toolOverlay.postMessage({
@@ -366,11 +357,7 @@ function pausePicker() {
366357
function unpausePicker() {
367358
dom.cl.remove(dom.root, 'paused', 'preview');
368359
dom.cl.add(dom.root, 'minimized');
369-
updatePreview();
370-
toolOverlay.postMessage({
371-
what: 'togglePreview',
372-
state: false,
373-
});
360+
updatePreview(false);
374361
toolOverlay.highlightElementUnderMouse(true);
375362
}
376363

platform/mv3/extension/js/popup.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,11 @@ dom.on('#gotoZapper', 'click', ( ) => {
265265
dom.on('#gotoPicker', 'click', ( ) => {
266266
if ( browser.scripting === undefined ) { return; }
267267
browser.scripting.executeScript({
268-
files: [ '/js/scripting/tool-overlay.js', '/js/scripting/picker.js' ],
268+
files: [
269+
'/js/scripting/css-procedural-api.js',
270+
'/js/scripting/tool-overlay.js',
271+
'/js/scripting/picker.js',
272+
],
269273
target: { tabId: currentTab.id },
270274
});
271275
self.close();
@@ -276,7 +280,10 @@ dom.on('#gotoPicker', 'click', ( ) => {
276280
dom.on('#gotoUnpicker', 'click', ( ) => {
277281
if ( browser.scripting === undefined ) { return; }
278282
browser.scripting.executeScript({
279-
files: [ '/js/scripting/tool-overlay.js', '/js/scripting/unpicker.js' ],
283+
files: [
284+
'/js/scripting/tool-overlay.js',
285+
'/js/scripting/unpicker.js',
286+
],
280287
target: { tabId: currentTab.id },
281288
});
282289
self.close();

0 commit comments

Comments
 (0)