[mv3] Merge all :style() filters with procedural filters

This commit is contained in:
Raymond Hill 2025-07-19 13:15:43 -04:00
parent 8ae7c1501b
commit fed7f4a0b8
No known key found for this signature in database
GPG key ID: 25E1490B761470C2
6 changed files with 77 additions and 320 deletions

View file

@ -354,70 +354,6 @@ function registerProcedural(context) {
/******************************************************************************/
function registerDeclarative(context) {
const { before, filteringModeDetails, rulesetsDetails } = context;
const js = [];
for ( const rulesetDetails of rulesetsDetails ) {
const count = rulesetDetails.css?.declarative || 0;
if ( count === 0 ) { continue; }
js.push(`/rulesets/scripting/declarative/${rulesetDetails.id}.js`);
}
if ( js.length === 0 ) { return; }
const { none, basic, optimal, complete } = filteringModeDetails;
const matches = [
...ut.matchesFromHostnames(optimal),
...ut.matchesFromHostnames(complete),
];
if ( matches.length === 0 ) { return; }
normalizeMatches(matches);
js.unshift('/js/scripting/isolated-api.js');
js.push('/js/scripting/css-declarative.js');
const excludeMatches = [];
if ( none.has('all-urls') === false ) {
excludeMatches.push(...ut.matchesFromHostnames(none));
}
if ( basic.has('all-urls') === false ) {
excludeMatches.push(...ut.matchesFromHostnames(basic));
}
const registered = before.get('css-declarative');
before.delete('css-declarative'); // Important!
const directive = {
id: 'css-declarative',
js,
matches,
allFrames: true,
runAt: 'document_start',
};
if ( excludeMatches.length !== 0 ) {
directive.excludeMatches = excludeMatches;
}
// register
if ( registered === undefined ) {
context.toAdd.push(directive);
return;
}
// update
if (
ut.strArrayEq(registered.js, js, false) === false ||
ut.strArrayEq(registered.matches, matches) === false ||
ut.strArrayEq(registered.excludeMatches, excludeMatches) === false
) {
context.toRemove.push('css-declarative');
context.toAdd.push(directive);
}
}
/******************************************************************************/
function registerSpecific(context) {
const { before, filteringModeDetails, rulesetsDetails } = context;
@ -602,7 +538,6 @@ async function registerInjectables() {
};
await Promise.all([
registerDeclarative(context),
registerProcedural(context),
registerScriptlet(context, scriptletDetails),
registerSpecific(context),

View file

@ -1,116 +0,0 @@
/*******************************************************************************
uBlock Origin Lite - a comprehensive, MV3-compliant content blocker
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
*/
// Important!
// Isolate from global scope
(function uBOL_cssDeclarative() {
/******************************************************************************/
const declarativeImports = self.declarativeImports || [];
self.declarativeImports = undefined;
/******************************************************************************/
const selectors = [];
const exceptions = [];
const lookupHostname = (hostname, details, out) => {
let seqi = details.hostnamesMap.get(hostname);
if ( seqi === undefined ) { return; }
const { argsList, argsSeqs } = details;
for (;;) {
const argi = argsSeqs[seqi++];
const done = argi > 0;
out.push(...argsList[done ? argi : -argi].split('\n'));
if ( done ) { break; }
}
};
const lookupAll = hostname => {
for ( const details of declarativeImports ) {
lookupHostname(hostname, details, selectors);
lookupHostname(`~${hostname}`, details, exceptions);
}
};
self.isolatedAPI.forEachHostname(lookupAll, {
hasEntities: declarativeImports.some(a => a.hasEntities)
});
declarativeImports.length = 0;
const exceptedSelectors = exceptions.length !== 0
? selectors.filter(a => exceptions.includes(a) === false)
: selectors;
if ( exceptedSelectors.length === 0 ) { return; }
/******************************************************************************/
const cssRuleFromProcedural = details => {
const { tasks, action } = details;
let mq, selector;
if ( Array.isArray(tasks) ) {
if ( tasks[0][0] !== 'matches-media' ) { return; }
mq = tasks[0][1];
if ( tasks.length > 2 ) { return; }
if ( tasks.length === 2 ) {
if ( tasks[1][0] !== 'spath' ) { return; }
selector = tasks[1][1];
}
}
let style;
if ( Array.isArray(action) ) {
if ( action[0] !== 'style' ) { return; }
selector = selector || details.selector;
style = action[1];
}
if ( mq === undefined && style === undefined && selector === undefined ) { return; }
if ( mq === undefined ) {
return `${selector}\n{${style}}`;
}
if ( style === undefined ) {
return `@media ${mq} {\n${selector}\n{display:none!important;}\n}`;
}
return `@media ${mq} {\n${selector}\n{${style}}\n}`;
};
const sheetText = [];
for ( const selector of exceptedSelectors ) {
const details = JSON.parse(selector);
const ruleText = cssRuleFromProcedural(details);
if ( ruleText === undefined ) { continue; }
sheetText.push(ruleText);
}
if ( sheetText.length === 0 ) { return; }
chrome.runtime.sendMessage({
what: 'insertCSS',
css: sheetText.join('\n'),
}).catch(( ) => {
});
/******************************************************************************/
})();
void 0;

View file

@ -577,8 +577,7 @@ class ProceduralFilterer {
}
addSelectors(selectors) {
for ( const json of selectors ) {
const selector = JSON.parse(json);
for ( const selector of selectors ) {
const pselector = new PSelectorRoot(selector);
this.primeProceduralSelector(pselector);
this.selectors.push(pselector);

View file

@ -21,7 +21,7 @@
// Important!
// Isolate from global scope
(async function uBOL_cssProcedural() {
(function uBOL_cssProcedural() {
/******************************************************************************/
@ -40,7 +40,7 @@ const lookupHostname = (hostname, details, out) => {
for (;;) {
const argi = argsSeqs[seqi++];
const done = argi > 0;
out.push(...argsList[done ? argi : -argi]);
out.push(...JSON.parse(argsList[done ? argi : -argi]));
if ( done ) { break; }
}
};
@ -48,38 +48,88 @@ const lookupHostname = (hostname, details, out) => {
const lookupAll = hostname => {
for ( const details of proceduralImports ) {
lookupHostname(hostname, details, selectors);
lookupHostname(`~${hostname}`, details, exceptions);
const matches = [];
lookupHostname(`~${hostname}`, details, matches);
if ( matches.length === 0 ) { continue; }
exceptions.push(...matches.map(a => JSON.stringify(a)));
}
};
self.isolatedAPI.forEachHostname(lookupAll, {
hasEntities: proceduralImports.some(a => a.hasEntities)
});
proceduralImports.length = 0;
const exceptedSelectors = exceptions.length !== 0
? selectors.filter(a => exceptions.includes(a) === false)
? selectors.filter(a => exceptions.includes(JSON.stringify(a)) === false)
: selectors;
if ( exceptedSelectors.length === 0 ) { return; }
if ( self.cssProceduralAPI === undefined ) {
self.cssProceduralAPI = chrome.runtime.sendMessage({
what: 'injectCSSProceduralAPI'
}).catch(( ) => {
});
const declaratives = exceptedSelectors.filter(a => a.cssable);
if ( declaratives.length !== 0 ) {
const cssRuleFromProcedural = details => {
const { tasks, action } = details;
let mq, selector;
if ( Array.isArray(tasks) ) {
if ( tasks[0][0] !== 'matches-media' ) { return; }
mq = tasks[0][1];
if ( tasks.length > 2 ) { return; }
if ( tasks.length === 2 ) {
if ( tasks[1][0] !== 'spath' ) { return; }
selector = tasks[1][1];
}
}
let style;
if ( Array.isArray(action) ) {
if ( action[0] !== 'style' ) { return; }
selector = selector || details.selector;
style = action[1];
}
if ( mq === undefined && style === undefined && selector === undefined ) { return; }
if ( mq === undefined ) {
return `${selector}\n{${style}}`;
}
if ( style === undefined ) {
return `@media ${mq} {\n${selector}\n{display:none!important;}\n}`;
}
return `@media ${mq} {\n${selector}\n{${style}}\n}`;
};
const sheetText = [];
for ( const details of declaratives ) {
const ruleText = cssRuleFromProcedural(details);
if ( ruleText === undefined ) { continue; }
sheetText.push(ruleText);
}
if ( sheetText.length !== 0 ) {
chrome.runtime.sendMessage({
what: 'insertCSS',
css: sheetText.join('\n'),
}).catch(( ) => {
});
}
}
if ( self.cssProceduralAPI instanceof Promise ) {
await self.cssProceduralAPI;
}
if ( self.cssProceduralAPI instanceof Object === false ) { return; }
self.cssProceduralAPI.addSelectors(exceptedSelectors);
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.cssProceduralAPI === undefined ) {
self.cssProceduralAPI = chrome.runtime.sendMessage({
what: 'injectCSSProceduralAPI'
}).catch(( ) => {
});
}
if ( self.cssProceduralAPI instanceof Promise ) {
self.cssProceduralAPI.then(( ) => { addSelectors(procedurals); });
} else {
addSelectors(procedurals);
}
}
/******************************************************************************/
})();
/******************************************************************************/
void 0;

View file

@ -1059,84 +1059,12 @@ async function processCosmeticFilters(assetDetails, mapin) {
/******************************************************************************/
async function processDeclarativeCosmeticFilters(assetDetails, mapin) {
if ( mapin === undefined ) { return 0; }
if ( mapin.size === 0 ) { return 0; }
// Distinguish declarative-compiled-as-procedural from actual procedural.
const declaratives = new Map();
mapin.forEach((details, jsonSelector) => {
const selector = JSON.parse(jsonSelector);
if ( selector.cssable !== true ) { return; }
selector.cssable = undefined;
declaratives.set(JSON.stringify(selector), details);
});
if ( declaratives.size === 0 ) { return 0; }
const contentArray = groupHostnamesBySelectors(
groupSelectorsByHostnames(declaratives)
);
const argsMap = contentArray.map(entry => [
entry[0],
entry[1].a ? entry[1].a.join('\n') : undefined,
]);
const hostnamesMap = new Map();
let hasEntities = false;
for ( const [ id, details ] of contentArray ) {
if ( details.y ) {
scriptletHostnameToIdMap(details.y, id, hostnamesMap);
hasEntities ||= details.y.some(a => a.endsWith('.*'));
}
if ( details.n ) {
scriptletHostnameToIdMap(details.n.map(a => `~${a}`), id, hostnamesMap);
hasEntities ||= details.n.some(a => a.endsWith('.*'));
}
}
const { argsList, argsSeqs } = argsMap2List(argsMap, hostnamesMap);
const originalScriptletMap = await loadAllSourceScriptlets();
let patchedScriptlet = originalScriptletMap.get('css-declarative').replace(
'$rulesetId$',
assetDetails.id
);
patchedScriptlet = safeReplace(patchedScriptlet,
/\bself\.\$argsList\$/,
`${JSON.stringify(argsList, scriptletJsonReplacer)}`
);
patchedScriptlet = safeReplace(patchedScriptlet,
/\bself\.\$argsSeqs\$/,
`${JSON.stringify(argsSeqs, scriptletJsonReplacer)}`
);
patchedScriptlet = safeReplace(patchedScriptlet,
/\bself\.\$hostnamesMap\$/,
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
);
patchedScriptlet = safeReplace(patchedScriptlet,
'self.$hasEntities$',
JSON.stringify(hasEntities)
);
writeFile(`${scriptletDir}/declarative/${assetDetails.id}.js`, patchedScriptlet);
if ( contentArray.length !== 0 ) {
log(`CSS-declarative: ${declaratives.size} distinct filters`);
log(`\tCombined into ${hostnamesMap.size} distinct hostnames`);
}
return hostnamesMap.size;
}
/******************************************************************************/
async function processProceduralCosmeticFilters(assetDetails, mapin) {
if ( mapin === undefined ) { return 0; }
if ( mapin.size === 0 ) { return 0; }
// Distinguish declarative-compiled-as-procedural from actual procedural.
const procedurals = new Map();
mapin.forEach((details, jsonSelector) => {
const selector = JSON.parse(jsonSelector);
if ( selector.cssable ) { return; }
procedurals.set(jsonSelector, details);
});
if ( procedurals.size === 0 ) { return 0; }
@ -1162,6 +1090,14 @@ async function processProceduralCosmeticFilters(assetDetails, mapin) {
}
}
const { argsList, argsSeqs } = argsMap2List(argsMap, hostnamesMap);
const argsListAfter = [];
for ( const a of argsList ) {
const aAfter = [];
for ( let b of a ) {
aAfter.push(JSON.parse(b));
}
argsListAfter.push(JSON.stringify(aAfter));
}
const originalScriptletMap = await loadAllSourceScriptlets();
let patchedScriptlet = originalScriptletMap.get('css-procedural').replace(
'$rulesetId$',
@ -1169,7 +1105,7 @@ async function processProceduralCosmeticFilters(assetDetails, mapin) {
);
patchedScriptlet = safeReplace(patchedScriptlet,
/\bself\.\$argsList\$/,
`${JSON.stringify(argsList, scriptletJsonReplacer)}`
`${JSON.stringify(argsListAfter, scriptletJsonReplacer)}`
);
patchedScriptlet = safeReplace(patchedScriptlet,
/\bself\.\$argsSeqs\$/,
@ -1333,10 +1269,6 @@ async function rulesetFromURLs(assetDetails) {
assetDetails,
declarativeCosmetic
);
const declarativeStats = await processDeclarativeCosmeticFilters(
assetDetails,
proceduralCosmetic
);
const proceduralStats = await processProceduralCosmeticFilters(
assetDetails,
proceduralCosmetic
@ -1376,7 +1308,6 @@ async function rulesetFromURLs(assetDetails) {
generic: genericCosmeticStats,
generichigh: genericHighCosmeticStats,
specific: specificCosmeticStats,
declarative: declarativeStats,
procedural: proceduralStats,
},
scriptlets: scriptletStats,

View file

@ -1,42 +0,0 @@
/*******************************************************************************
uBlock Origin Lite - a comprehensive, MV3-compliant content blocker
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
*/
// ruleset: $rulesetId$
// Important!
// Isolate from global scope
(function uBOL_cssDeclarativeImport() {
/******************************************************************************/
const argsList = self.$argsList$;
const argsSeqs = self.$argsSeqs$;
const hostnamesMap = new Map(self.$hostnamesMap$);
const hasEntities = self.$hasEntities$;
self.declarativeImports = self.declarativeImports || [];
self.declarativeImports.push({ argsList, argsSeqs, hostnamesMap, hasEntities });
/******************************************************************************/
})();
/******************************************************************************/