diff --git a/platform/mv3/extension/js/background.js b/platform/mv3/extension/js/background.js index 3f541486b..097f97e0b 100644 --- a/platform/mv3/extension/js/background.js +++ b/platform/mv3/extension/js/background.js @@ -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; diff --git a/platform/mv3/extension/js/filter-manager.js b/platform/mv3/extension/js/filter-manager.js index 282c97243..cfbd4f876 100644 --- a/platform/mv3/extension/js/filter-manager.js +++ b/platform/mv3/extension/js/filter-manager.js @@ -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 diff --git a/platform/mv3/extension/js/picker-ui.js b/platform/mv3/extension/js/picker-ui.js index bb4356554..04ea605d4 100644 --- a/platform/mv3/extension/js/picker-ui.js +++ b/platform/mv3/extension/js/picker-ui.js @@ -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); } diff --git a/platform/mv3/extension/js/popup.js b/platform/mv3/extension/js/popup.js index d00398303..b4589333e 100644 --- a/platform/mv3/extension/js/popup.js +++ b/platform/mv3/extension/js/popup.js @@ -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(); diff --git a/platform/mv3/extension/js/scripting/css-procedural-api.js b/platform/mv3/extension/js/scripting/css-procedural-api.js index 5559b2b1a..af3abf1e9 100644 --- a/platform/mv3/extension/js/scripting/css-procedural-api.js +++ b/platform/mv3/extension/js/scripting/css-procedural-api.js @@ -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; diff --git a/platform/mv3/extension/js/scripting/css-procedural.js b/platform/mv3/extension/js/scripting/css-procedural.js index 376d305e1..26968b03c 100644 --- a/platform/mv3/extension/js/scripting/css-procedural.js +++ b/platform/mv3/extension/js/scripting/css-procedural.js @@ -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); } diff --git a/platform/mv3/extension/js/scripting/css-user-terminate.js b/platform/mv3/extension/js/scripting/css-user-terminate.js new file mode 100644 index 000000000..6e63eb934 --- /dev/null +++ b/platform/mv3/extension/js/scripting/css-user-terminate.js @@ -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; diff --git a/platform/mv3/extension/js/scripting/css-user.js b/platform/mv3/extension/js/scripting/css-user.js index 3adae6628..04a8ddcec 100644 --- a/platform/mv3/extension/js/scripting/css-user.js +++ b/platform/mv3/extension/js/scripting/css-user.js @@ -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; + /******************************************************************************/ })(); diff --git a/platform/mv3/extension/js/scripting/picker.js b/platform/mv3/extension/js/scripting/picker.js index 124d4dcb7..60df48b0e 100644 --- a/platform/mv3/extension/js/scripting/picker.js +++ b/platform/mv3/extension/js/scripting/picker.js @@ -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; } diff --git a/platform/mv3/extension/js/scripting/tool-overlay.js b/platform/mv3/extension/js/scripting/tool-overlay.js index e2708b159..252acadac 100644 --- a/platform/mv3/extension/js/scripting/tool-overlay.js +++ b/platform/mv3/extension/js/scripting/tool-overlay.js @@ -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 []; }, diff --git a/platform/mv3/extension/js/scripting/unpicker.js b/platform/mv3/extension/js/scripting/unpicker.js index 41c5fb71c..afc5ab143 100644 --- a/platform/mv3/extension/js/scripting/unpicker.js +++ b/platform/mv3/extension/js/scripting/unpicker.js @@ -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, diff --git a/platform/mv3/extension/js/scripting/zapper.js b/platform/mv3/extension/js/scripting/zapper.js index 9d9628f76..262be1b8e 100644 --- a/platform/mv3/extension/js/scripting/zapper.js +++ b/platform/mv3/extension/js/scripting/zapper.js @@ -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 diff --git a/platform/mv3/extension/js/tool-overlay-ui.js b/platform/mv3/extension/js/tool-overlay-ui.js index 609d548bf..423154ee5 100644 --- a/platform/mv3/extension/js/tool-overlay-ui.js +++ b/platform/mv3/extension/js/tool-overlay-ui.js @@ -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 diff --git a/platform/mv3/extension/js/unpicker-ui.js b/platform/mv3/extension/js/unpicker-ui.js index afc9f64bd..dcf3983a7 100644 --- a/platform/mv3/extension/js/unpicker-ui.js +++ b/platform/mv3/extension/js/unpicker-ui.js @@ -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(); } diff --git a/platform/mv3/extension/js/zapper-ui.js b/platform/mv3/extension/js/zapper-ui.js index 3562f6b36..433ea0fa7 100644 --- a/platform/mv3/extension/js/zapper-ui.js +++ b/platform/mv3/extension/js/zapper-ui.js @@ -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, }, }); } diff --git a/src/js/static-filtering-parser.js b/src/js/static-filtering-parser.js index 2087d360d..bb3ef24f7 100644 --- a/src/js/static-filtering-parser.js +++ b/src/js/static-filtering-parser.js @@ -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