diff --git a/platform/mv3/extension/js/scripting-manager.js b/platform/mv3/extension/js/scripting-manager.js index 1d9d42876..f1f949016 100644 --- a/platform/mv3/extension/js/scripting-manager.js +++ b/platform/mv3/extension/js/scripting-manager.js @@ -79,7 +79,12 @@ const arrayEq = (a = [], b = [], sort = true) => { const normalizeRegisteredContentScripts = registered => { for ( const entry of registered ) { - const { js } = entry; + const { css = [], js = [] } = entry; + for ( let i = 0; i < css.length; i++ ) { + const path = css[i]; + if ( path.startsWith('/') ) { continue; } + css[i] = `/${path}`; + } for ( let i = 0; i < js.length; i++ ) { const path = js[i]; if ( path.startsWith('/') ) { continue; } @@ -91,6 +96,78 @@ const normalizeRegisteredContentScripts = registered => { /******************************************************************************/ +function registerHighGeneric(context, genericDetails) { + const { before, filteringModeDetails, rulesetsDetails } = context; + + const excludeHostnames = []; + const css = []; + for ( const details of rulesetsDetails ) { + const hostnames = genericDetails.get(details.id); + if ( hostnames !== undefined ) { + excludeHostnames.push(...hostnames); + } + const count = details.css?.generichigh || 0; + if ( count === 0 ) { continue; } + css.push(`/rulesets/scripting/generichigh/${details.id}.css`); + } + + if ( css.length === 0 ) { return; } + + const { none, basic, optimal, complete } = filteringModeDetails; + const matches = []; + const excludeMatches = []; + if ( complete.has('all-urls') ) { + excludeMatches.push(...ut.matchesFromHostnames(none)); + excludeMatches.push(...ut.matchesFromHostnames(basic)); + excludeMatches.push(...ut.matchesFromHostnames(optimal)); + excludeMatches.push(...ut.matchesFromHostnames(excludeHostnames)); + matches.push(''); + } else { + matches.push( + ...ut.matchesFromHostnames( + ut.subtractHostnameIters( + Array.from(complete), + excludeHostnames + ) + ) + ); + } + + if ( matches.length === 0 ) { return; } + + const registered = before.get('css-generichigh'); + before.delete('css-generichigh'); // Important! + + // https://github.com/w3c/webextensions/issues/414#issuecomment-1623992885 + // Once supported, add: + // cssOrigin: 'USER', + const directive = { + id: 'css-generichigh', + css, + matches, + excludeMatches, + runAt: 'document_end', + }; + + // register + if ( registered === undefined ) { + context.toAdd.push(directive); + return; + } + + // update + if ( + arrayEq(registered.css, css, false) === false || + arrayEq(registered.matches, matches) === false || + arrayEq(registered.excludeMatches, excludeMatches) === false + ) { + context.toRemove.push('css-generichigh'); + context.toAdd.push(directive); + } +} + +/******************************************************************************/ + function registerGeneric(context, genericDetails) { const { before, filteringModeDetails, rulesetsDetails } = context; @@ -459,6 +536,7 @@ async function registerInjectables(origins) { registerScriptlet(context, scriptletDetails); registerSpecific(context); registerGeneric(context, genericDetails); + registerHighGeneric(context, genericDetails); toRemove.push(...Array.from(before.keys())); diff --git a/platform/mv3/make-rulesets.js b/platform/mv3/make-rulesets.js index 611d80245..4c1f27959 100644 --- a/platform/mv3/make-rulesets.js +++ b/platform/mv3/make-rulesets.js @@ -382,7 +382,8 @@ function loadAllSourceScriptlets() { const originalScriptletMap = new Map(); for ( const details of results ) { originalScriptletMap.set( - details.file.replace('.template.js', ''), + details.file.replace('.template.js', '') + .replace('.template.css', ''), details.text ); } @@ -395,7 +396,7 @@ function loadAllSourceScriptlets() { /******************************************************************************/ -async function processGenericCosmeticFilters(assetDetails, bucketsMap, exclusions) { +async function processGenericCosmeticFilters(assetDetails, bucketsMap) { if ( bucketsMap === undefined ) { return 0; } if ( bucketsMap.size === 0 ) { return 0; } const bucketsList = Array.from(bucketsMap); @@ -419,8 +420,6 @@ async function processGenericCosmeticFilters(assetDetails, bucketsMap, exclusion patchedScriptlet ); - genericDetails.set(assetDetails.id, exclusions.sort()); - log(`CSS-generic: ${count} plain CSS selectors`); return count; @@ -428,6 +427,33 @@ async function processGenericCosmeticFilters(assetDetails, bucketsMap, exclusion /******************************************************************************/ +async function processGenericHighCosmeticFilters(assetDetails, selectorSet) { + if ( selectorSet === undefined ) { return 0; } + if ( selectorSet.size === 0 ) { return 0; } + const selectorLists = Array.from(selectorSet).sort().join(',\n'); + const originalScriptletMap = await loadAllSourceScriptlets(); + + let patchedScriptlet = originalScriptletMap.get('css-generichigh').replace( + '$rulesetId$', + assetDetails.id + ); + patchedScriptlet = safeReplace(patchedScriptlet, + /\$selectorList\$/, + selectorLists + ); + + writeFile( + `${scriptletDir}/generichigh/${assetDetails.id}.css`, + patchedScriptlet + ); + + log(`CSS-generic-high: ${selectorSet.size} plain CSS selectors`); + + return selectorSet.size; +} + +/******************************************************************************/ + // This merges selectors which are used by the same hostnames function groupSelectorsByHostnames(mapin) { @@ -887,10 +913,23 @@ async function rulesetFromURLs(assetDetails) { log(rejectedCosmetic.map(line => `\t${line}`).join('\n'), true); } + if ( + Array.isArray(results.network.generichideExclusions) && + results.network.generichideExclusions.length !== 0 + ) { + genericDetails.set( + assetDetails.id, + results.network.generichideExclusions.filter(hn => hn.endsWith('.*') === false).sort() + ); + } + const genericCosmeticStats = await processGenericCosmeticFilters( assetDetails, - results.genericCosmetic, - results.network.generichideExclusions.filter(hn => hn.endsWith('.*') === false) + results.genericCosmetic + ); + const genericHighCosmeticStats = await processGenericHighCosmeticFilters( + assetDetails, + results.genericHighCosmetic ); const specificCosmeticStats = await processCosmeticFilters( assetDetails, @@ -933,6 +972,7 @@ async function rulesetFromURLs(assetDetails) { }, css: { generic: genericCosmeticStats, + generichigh: genericHighCosmeticStats, specific: specificCosmeticStats, declarative: declarativeStats, procedural: proceduralStats, diff --git a/src/js/static-dnr-filtering.js b/src/js/static-dnr-filtering.js index 99fa9241b..4177991b9 100644 --- a/src/js/static-dnr-filtering.js +++ b/src/js/static-dnr-filtering.js @@ -147,17 +147,23 @@ function addExtendedToDNR(context, parser) { // Generic cosmetic filtering if ( parser.hasOptions() === false ) { - if ( context.genericCosmeticFilters === undefined ) { - context.genericCosmeticFilters = new Map(); - } const { compiled } = parser.result; if ( compiled === undefined ) { return; } if ( compiled.length <= 1 ) { return; } if ( compiled.charCodeAt(0) === 0x7B /* '{' */ ) { return; } const key = keyFromSelector(compiled); - if ( key === undefined ) { return; } + if ( key === undefined ) { + if ( context.genericHighCosmeticFilters === undefined ) { + context.genericHighCosmeticFilters = new Set(); + } + context.genericHighCosmeticFilters.add(compiled); + return; + } const type = key.charCodeAt(0); const hash = hashFromStr(type, key.slice(1)); + if ( context.genericCosmeticFilters === undefined ) { + context.genericCosmeticFilters = new Map(); + } let bucket = context.genericCosmeticFilters.get(hash); if ( bucket === undefined ) { context.genericCosmeticFilters.set(hash, bucket = []); @@ -291,6 +297,7 @@ async function dnrRulesetFromRawLists(lists, options = {}) { return { network: staticNetFilteringEngine.dnrFromCompiled('end', context), genericCosmetic: context.genericCosmeticFilters, + genericHighCosmetic: context.genericHighCosmeticFilters, specificCosmetic: context.specificCosmeticFilters, scriptlet: context.scriptletFilters, };