mirror of
https://github.com/gorhill/uBlock.git
synced 2024-11-20 01:12:38 +01:00
[mv3] Add support for procedural cosmetic filtering
This commit is contained in:
parent
a71b71e4c8
commit
966a157d19
@ -105,14 +105,19 @@ const toRegisterable = (fname, entry) => {
|
||||
directive.excludeMatches = matchesFromHostnames(entry.excludeMatches);
|
||||
}
|
||||
directive.js = [ `/rulesets/js/${fname.slice(0,2)}/${fname.slice(2)}.js` ];
|
||||
if ( (fidFromFileName(fname) & RUN_AT_BIT) !== 0 ) {
|
||||
directive.runAt = 'document_end';
|
||||
} else {
|
||||
directive.runAt = 'document_start';
|
||||
}
|
||||
if ( (fidFromFileName(fname) & MAIN_WORLD_BIT) !== 0 ) {
|
||||
directive.world = 'MAIN';
|
||||
}
|
||||
return directive;
|
||||
};
|
||||
|
||||
const MAIN_WORLD_BIT = 0b1;
|
||||
const RUN_AT_BIT = 0b10;
|
||||
const MAIN_WORLD_BIT = 0b01;
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
|
@ -367,48 +367,53 @@ function addScriptingAPIResources(id, entry, prop, fid) {
|
||||
}
|
||||
}
|
||||
|
||||
const toCSSFileId = s => uidint32(s) & ~0b1;
|
||||
const toJSFileId = s => uidint32(s) | 0b1;
|
||||
const toCSSFileId = s => (uidint32(s) & ~0b11) | 0b00;
|
||||
const toJSFileId = s => (uidint32(s) & ~0b11) | 0b01;
|
||||
const toProceduralFileId = s => (uidint32(s) & ~0b11) | 0b10;
|
||||
|
||||
const pathFromFileName = fname => `${scriptletDir}/${fname.slice(0,2)}/${fname.slice(2)}.js`;
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
async function processCosmeticFilters(assetDetails, mapin) {
|
||||
if ( mapin === undefined ) { return 0; }
|
||||
const COSMETIC_FILES_PER_RULESET = 12;
|
||||
const PROCEDURAL_FILES_PER_RULESET = 4;
|
||||
|
||||
// This groups together selectors which are used by the same hostname.
|
||||
const optimized = (filters => {
|
||||
if ( filters === undefined ) { return []; }
|
||||
const merge = new Map();
|
||||
for ( const [ selector, details ] of filters ) {
|
||||
// This merges selectors which are used by the same hostnames
|
||||
|
||||
function groupCosmeticByHostnames(mapin) {
|
||||
if ( mapin === undefined ) { return []; }
|
||||
const merged = new Map();
|
||||
for ( const [ selector, details ] of mapin ) {
|
||||
const json = JSON.stringify(details);
|
||||
let entries = merge.get(json);
|
||||
let entries = merged.get(json);
|
||||
if ( entries === undefined ) {
|
||||
entries = new Set();
|
||||
merge.set(json, entries);
|
||||
merged.set(json, entries);
|
||||
}
|
||||
entries.add(selector);
|
||||
}
|
||||
const out = [];
|
||||
for ( const [ json, entries ] of merge ) {
|
||||
for ( const [ json, entries ] of merged ) {
|
||||
const details = JSON.parse(json);
|
||||
details.selector = Array.from(entries).join(',');
|
||||
details.selectors = Array.from(entries).sort();
|
||||
out.push(details);
|
||||
}
|
||||
return out;
|
||||
})(mapin);
|
||||
}
|
||||
|
||||
// This creates a map of unique selectorset => all hostnames
|
||||
// including/excluding the selectorset. This allows to avoid duplication
|
||||
// of css content.
|
||||
const cssContentMap = new Map();
|
||||
for ( const entry of optimized ) {
|
||||
// ends-with 0 = css resource
|
||||
const id = uidint32(entry.selector);
|
||||
let details = cssContentMap.get(id);
|
||||
// This merges hostnames which have the same set of selectors.
|
||||
//
|
||||
// Also, we sort the hostnames to increase likelihood that selector with
|
||||
// same hostnames will end up in same generated scriptlet.
|
||||
|
||||
function groupCosmeticBySelectors(arrayin) {
|
||||
const contentMap = new Map();
|
||||
for ( const entry of arrayin ) {
|
||||
const id = uidint32(JSON.stringify(entry.selectors));
|
||||
let details = contentMap.get(id);
|
||||
if ( details === undefined ) {
|
||||
details = { a: entry.selector };
|
||||
cssContentMap.set(id, details);
|
||||
details = { a: entry.selectors };
|
||||
contentMap.set(id, details);
|
||||
}
|
||||
if ( entry.matches !== undefined ) {
|
||||
if ( details.y === undefined ) {
|
||||
@ -427,20 +432,38 @@ async function processCosmeticFilters(assetDetails, mapin) {
|
||||
}
|
||||
}
|
||||
}
|
||||
const hnSort = (a, b) =>
|
||||
a.split('.').reverse().join('.').localeCompare(
|
||||
b.split('.').reverse().join('.')
|
||||
);
|
||||
const out = Array.from(contentMap).map(a => [
|
||||
a[0], {
|
||||
a: a[1].a,
|
||||
y: a[1].y ? Array.from(a[1].y).sort(hnSort) : undefined,
|
||||
n: a[1].n ? Array.from(a[1].n) : undefined,
|
||||
}
|
||||
]).sort((a, b) => {
|
||||
const ha = Array.isArray(a[1].y) ? a[1].y[0] : '*';
|
||||
const hb = Array.isArray(b[1].y) ? b[1].y[0] : '*';
|
||||
return hnSort(ha, hb);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
// We do not want more than 16 CSS files per subscription, so we will
|
||||
// group multiple unrelated selectors in the same file, and distinct
|
||||
// css declarations will be injected programmatically according to the
|
||||
// hostname of the current document.
|
||||
//
|
||||
// The cosmetic filters will be injected programmatically as content
|
||||
// script and the decisions to activate the cosmetic filters will be
|
||||
// done at injection time according to the document's hostname.
|
||||
const originalScriptletMap = await loadAllSourceScriptlets();
|
||||
const contentPerFile = Math.ceil(cssContentMap.size / 16);
|
||||
const cssContentArray = Array.from(cssContentMap);
|
||||
const scriptletHostnameToIdMap = (hostnames, id, map) => {
|
||||
for ( const hn of hostnames ) {
|
||||
const existing = map.get(hn);
|
||||
if ( existing === undefined ) {
|
||||
map.set(hn, id);
|
||||
} else if ( Array.isArray(existing) ) {
|
||||
existing.push(id);
|
||||
} else {
|
||||
map.set(hn, [ existing, id ]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const jsonReplacer = (k, v) => {
|
||||
const scriptletJsonReplacer = (k, v) => {
|
||||
if ( k === 'n' ) {
|
||||
if ( v === undefined || v.size === 0 ) { return; }
|
||||
return Array.from(v);
|
||||
@ -452,40 +475,53 @@ async function processCosmeticFilters(assetDetails, mapin) {
|
||||
return v;
|
||||
};
|
||||
|
||||
const toHostnamesMap = (hostnames, id, out) => {
|
||||
for ( const hn of hostnames ) {
|
||||
const existing = out.get(hn);
|
||||
if ( existing === undefined ) {
|
||||
out.set(hn, id);
|
||||
} else if ( Array.isArray(existing) ) {
|
||||
existing.push(id);
|
||||
} else {
|
||||
out.set(hn, [ existing, id ]);
|
||||
}
|
||||
}
|
||||
};
|
||||
/******************************************************************************/
|
||||
|
||||
async function processCosmeticFilters(assetDetails, mapin) {
|
||||
if ( mapin === undefined ) { return 0; }
|
||||
|
||||
const contentArray = groupCosmeticBySelectors(
|
||||
groupCosmeticByHostnames(mapin)
|
||||
);
|
||||
const contentPerFile =
|
||||
Math.ceil(contentArray.length / COSMETIC_FILES_PER_RULESET);
|
||||
|
||||
// We do not want more than n CSS files per subscription, so we will
|
||||
// group multiple unrelated selectors in the same file, and distinct
|
||||
// css declarations will be injected programmatically according to the
|
||||
// hostname of the current document.
|
||||
//
|
||||
// The cosmetic filters will be injected programmatically as content
|
||||
// script and the decisions to activate the cosmetic filters will be
|
||||
// done at injection time according to the document's hostname.
|
||||
const originalScriptletMap = await loadAllSourceScriptlets();
|
||||
const generatedFiles = [];
|
||||
|
||||
for ( let i = 0; i < cssContentArray.length; i += contentPerFile ) {
|
||||
const slice = cssContentArray.slice(i, i + contentPerFile);
|
||||
for ( let i = 0; i < contentArray.length; i += contentPerFile ) {
|
||||
const slice = contentArray.slice(i, i + contentPerFile);
|
||||
const argsMap = slice.map(entry => [
|
||||
entry[0], { a: entry[1].a, n: entry[1].n }
|
||||
entry[0],
|
||||
{
|
||||
a: entry[1].a ? entry[1].a.join(',\n') : undefined,
|
||||
n: entry[1].n
|
||||
}
|
||||
]);
|
||||
const hostnamesMap = new Map();
|
||||
for ( const [ id, details ] of slice ) {
|
||||
if ( details.y === undefined ) { continue; }
|
||||
toHostnamesMap(details.y, id, hostnamesMap);
|
||||
scriptletHostnameToIdMap(details.y, id, hostnamesMap);
|
||||
}
|
||||
const patchedScriptlet = originalScriptletMap.get('css-specific')
|
||||
.replace(
|
||||
'$rulesetId$',
|
||||
assetDetails.id
|
||||
).replace(
|
||||
/\bself\.\$argsMap\$/m,
|
||||
`${JSON.stringify(argsMap, jsonReplacer)}`
|
||||
`${JSON.stringify(argsMap, scriptletJsonReplacer)}`
|
||||
).replace(
|
||||
/\bself\.\$hostnamesMap\$/m,
|
||||
`${JSON.stringify(hostnamesMap, jsonReplacer)}`
|
||||
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
|
||||
);
|
||||
// ends-with 0 = css resource
|
||||
const fid = toCSSFileId(patchedScriptlet);
|
||||
if ( globalPatchedScriptletsSet.has(fid) === false ) {
|
||||
globalPatchedScriptletsSet.add(fid);
|
||||
@ -510,12 +546,91 @@ async function processCosmeticFilters(assetDetails, mapin) {
|
||||
}
|
||||
|
||||
if ( generatedFiles.length !== 0 ) {
|
||||
log(`CSS-related distinct filters: ${cssContentArray.length} distinct combined selectors`);
|
||||
log(`CSS-related distinct filters: ${contentArray.length} distinct combined selectors`);
|
||||
log(`CSS-related injectable files: ${generatedFiles.length}`);
|
||||
log(`\t${generatedFiles.join(', ')}`);
|
||||
}
|
||||
|
||||
return cssContentArray.length;
|
||||
return contentArray.length;
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
async function processProceduralCosmeticFilters(assetDetails, mapin) {
|
||||
if ( mapin === undefined ) { return 0; }
|
||||
|
||||
const contentArray = groupCosmeticBySelectors(
|
||||
groupCosmeticByHostnames(mapin)
|
||||
);
|
||||
const contentPerFile =
|
||||
Math.ceil(contentArray.length / PROCEDURAL_FILES_PER_RULESET);
|
||||
|
||||
// We do not want more than n CSS files per subscription, so we will
|
||||
// group multiple unrelated selectors in the same file, and distinct
|
||||
// css declarations will be injected programmatically according to the
|
||||
// hostname of the current document.
|
||||
//
|
||||
// The cosmetic filters will be injected programmatically as content
|
||||
// script and the decisions to activate the cosmetic filters will be
|
||||
// done at injection time according to the document's hostname.
|
||||
const originalScriptletMap = await loadAllSourceScriptlets();
|
||||
const generatedFiles = [];
|
||||
|
||||
for ( let i = 0; i < contentArray.length; i += contentPerFile ) {
|
||||
const slice = contentArray.slice(i, i + contentPerFile);
|
||||
const argsMap = slice.map(entry => [
|
||||
entry[0],
|
||||
{
|
||||
a: entry[1].a ? entry[1].a.map(v => JSON.parse(v)) : undefined,
|
||||
n: entry[1].n
|
||||
}
|
||||
]);
|
||||
const hostnamesMap = new Map();
|
||||
for ( const [ id, details ] of slice ) {
|
||||
if ( details.y === undefined ) { continue; }
|
||||
scriptletHostnameToIdMap(details.y, id, hostnamesMap);
|
||||
}
|
||||
const patchedScriptlet = originalScriptletMap.get('css-specific-procedural')
|
||||
.replace(
|
||||
'$rulesetId$',
|
||||
assetDetails.id
|
||||
).replace(
|
||||
/\bself\.\$argsMap\$/m,
|
||||
`${JSON.stringify(argsMap, scriptletJsonReplacer)}`
|
||||
).replace(
|
||||
/\bself\.\$hostnamesMap\$/m,
|
||||
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
|
||||
);
|
||||
const fid = toProceduralFileId(patchedScriptlet);
|
||||
if ( globalPatchedScriptletsSet.has(fid) === false ) {
|
||||
globalPatchedScriptletsSet.add(fid);
|
||||
const fname = fnameFromFileId(fid);
|
||||
writeFile(pathFromFileName(fname), patchedScriptlet, {});
|
||||
generatedFiles.push(fname);
|
||||
}
|
||||
for ( const entry of slice ) {
|
||||
addScriptingAPIResources(
|
||||
assetDetails.id,
|
||||
{ matches: entry[1].y },
|
||||
'matches',
|
||||
fid
|
||||
);
|
||||
addScriptingAPIResources(
|
||||
assetDetails.id,
|
||||
{ excludeMatches: entry[1].n },
|
||||
'excludeMatches',
|
||||
fid
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ( generatedFiles.length !== 0 ) {
|
||||
log(`Pprocedural-related distinct filters: ${contentArray.length} distinct combined selectors`);
|
||||
log(`Pprocedural-related injectable files: ${generatedFiles.length}`);
|
||||
log(`\t${generatedFiles.join(', ')}`);
|
||||
}
|
||||
|
||||
return contentArray.length;
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
@ -605,31 +720,6 @@ async function processScriptletFilters(assetDetails, mapin) {
|
||||
|
||||
const generatedFiles = [];
|
||||
|
||||
const jsonReplacer = (k, v) => {
|
||||
if ( k === 'n' ) {
|
||||
if ( v.size === 0 ) { return; }
|
||||
return Array.from(v);
|
||||
}
|
||||
if ( v instanceof Set || v instanceof Map ) {
|
||||
if ( v.size === 0 ) { return; }
|
||||
return Array.from(v);
|
||||
}
|
||||
return v;
|
||||
};
|
||||
|
||||
const toHostnamesMap = (hostnames, hash, out) => {
|
||||
for ( const hn of hostnames ) {
|
||||
const existing = out.get(hn);
|
||||
if ( existing === undefined ) {
|
||||
out.set(hn, hash);
|
||||
} else if ( Array.isArray(existing) ) {
|
||||
existing.push(hash);
|
||||
} else {
|
||||
out.set(hn, [ existing, hash ]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for ( const [ token, argsDetails ] of scriptletDetails ) {
|
||||
const argsMap = Array.from(argsDetails).map(entry => [
|
||||
uidint32(entry[0]),
|
||||
@ -637,15 +727,18 @@ async function processScriptletFilters(assetDetails, mapin) {
|
||||
]);
|
||||
const hostnamesMap = new Map();
|
||||
for ( const [ argsHash, details ] of argsDetails ) {
|
||||
toHostnamesMap(details.y, uidint32(argsHash), hostnamesMap);
|
||||
scriptletHostnameToIdMap(details.y, uidint32(argsHash), hostnamesMap);
|
||||
}
|
||||
const patchedScriptlet = originalScriptletMap.get(token)
|
||||
.replace(
|
||||
'$rulesetId$',
|
||||
assetDetails.id
|
||||
).replace(
|
||||
/\bself\.\$argsMap\$/m,
|
||||
`${JSON.stringify(argsMap, jsonReplacer)}`
|
||||
`${JSON.stringify(argsMap, scriptletJsonReplacer)}`
|
||||
).replace(
|
||||
/\bself\.\$hostnamesMap\$/m,
|
||||
`${JSON.stringify(hostnamesMap, jsonReplacer)}`
|
||||
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
|
||||
);
|
||||
// ends-with 1 = scriptlet resource
|
||||
const fid = toJSFileId(patchedScriptlet);
|
||||
@ -684,28 +777,6 @@ async function processScriptletFilters(assetDetails, mapin) {
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
async function main() {
|
||||
|
||||
// Get manifest content
|
||||
const manifest = await fs.readFile(
|
||||
`${outputDir}/manifest.json`,
|
||||
{ encoding: 'utf8' }
|
||||
).then(text =>
|
||||
JSON.parse(text)
|
||||
);
|
||||
|
||||
// Create unique version number according to build time
|
||||
let version = manifest.version;
|
||||
{
|
||||
const now = new Date();
|
||||
const yearPart = now.getUTCFullYear() - 2000;
|
||||
const monthPart = (now.getUTCMonth() + 1) * 1000;
|
||||
const dayPart = now.getUTCDate() * 10;
|
||||
const hourPart = Math.floor(now.getUTCHours() / 3) + 1;
|
||||
version += `.${yearPart}.${monthPart + dayPart + hourPart}`;
|
||||
}
|
||||
log(`Version: ${version}`);
|
||||
|
||||
const rulesetFromURLS = async function(assetDetails) {
|
||||
log('============================');
|
||||
log(`Listset for '${assetDetails.id}':`);
|
||||
@ -722,10 +793,37 @@ async function main() {
|
||||
results.network
|
||||
);
|
||||
|
||||
// Split cosmetic filters into two groups: declarative and procedural
|
||||
const declarativeCosmetic = new Map();
|
||||
const proceduralCosmetic = new Map();
|
||||
const rejectedCosmetic = [];
|
||||
if ( results.cosmetic ) {
|
||||
for ( const [ selector, details ] of results.cosmetic ) {
|
||||
if ( details.rejected ) {
|
||||
rejectedCosmetic.push(selector);
|
||||
continue;
|
||||
}
|
||||
if ( selector.startsWith('{') === false ) {
|
||||
declarativeCosmetic.set(selector, details);
|
||||
continue;
|
||||
}
|
||||
const parsed = JSON.parse(selector);
|
||||
parsed.raw = undefined;
|
||||
proceduralCosmetic.set(JSON.stringify(parsed), details);
|
||||
}
|
||||
}
|
||||
const cosmeticStats = await processCosmeticFilters(
|
||||
assetDetails,
|
||||
results.cosmetic
|
||||
declarativeCosmetic
|
||||
);
|
||||
const proceduralStats = await processProceduralCosmeticFilters(
|
||||
assetDetails,
|
||||
proceduralCosmetic
|
||||
);
|
||||
if ( rejectedCosmetic.length !== 0 ) {
|
||||
log(`Rejected cosmetic filters: ${rejectedCosmetic.length}`);
|
||||
log(rejectedCosmetic.map(line => `\t${line}`).join('\n'));
|
||||
}
|
||||
|
||||
const scriptletStats = await processScriptletFilters(
|
||||
assetDetails,
|
||||
@ -752,6 +850,7 @@ async function main() {
|
||||
},
|
||||
css: {
|
||||
specific: cosmeticStats,
|
||||
procedural: proceduralStats,
|
||||
},
|
||||
scriptlets: {
|
||||
total: scriptletStats,
|
||||
@ -765,6 +864,30 @@ async function main() {
|
||||
});
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
async function main() {
|
||||
|
||||
// Get manifest content
|
||||
const manifest = await fs.readFile(
|
||||
`${outputDir}/manifest.json`,
|
||||
{ encoding: 'utf8' }
|
||||
).then(text =>
|
||||
JSON.parse(text)
|
||||
);
|
||||
|
||||
// Create unique version number according to build time
|
||||
let version = manifest.version;
|
||||
{
|
||||
const now = new Date();
|
||||
const yearPart = now.getUTCFullYear() - 2000;
|
||||
const monthPart = (now.getUTCMonth() + 1) * 1000;
|
||||
const dayPart = now.getUTCDate() * 10;
|
||||
const hourPart = Math.floor(now.getUTCHours() / 3) + 1;
|
||||
version += `.${yearPart}.${monthPart + dayPart + hourPart}`;
|
||||
}
|
||||
log(`Version: ${version}`);
|
||||
|
||||
// Get assets.json content
|
||||
const assets = await fs.readFile(
|
||||
`./assets.json`,
|
||||
|
@ -41,6 +41,14 @@
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// $rulesetId$
|
||||
|
||||
const argsMap = new Map(self.$argsMap$);
|
||||
|
||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// Issues to mind before changing anything:
|
||||
// https://github.com/uBlockOrigin/uBlock-issues/issues/2154
|
||||
|
||||
@ -145,10 +153,6 @@ const scriptlet = (
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const argsMap = new Map(self.$argsMap$);
|
||||
|
||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||
|
||||
let hn;
|
||||
try { hn = document.location.hostname; } catch(ex) { }
|
||||
while ( hn ) {
|
||||
@ -161,10 +165,19 @@ while ( hn ) {
|
||||
try { scriptlet(...details.a); } catch(ex) {}
|
||||
}
|
||||
}
|
||||
if ( hn === '*' ) { break; }
|
||||
const pos = hn.indexOf('.');
|
||||
if ( pos === -1 ) { break; }
|
||||
if ( pos !== -1 ) {
|
||||
hn = hn.slice(pos + 1);
|
||||
} else {
|
||||
hn = '*';
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
argsMap.clear();
|
||||
hostnamesMap.clear();
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
|
@ -39,6 +39,14 @@
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// $rulesetId$
|
||||
|
||||
const argsMap = new Map(self.$argsMap$);
|
||||
|
||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const ObjGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
|
||||
const ObjDefineProperty = Object.defineProperty;
|
||||
|
||||
@ -103,10 +111,6 @@ const scriptlet = (
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const argsMap = new Map(self.$argsMap$);
|
||||
|
||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||
|
||||
let hn;
|
||||
try { hn = document.location.hostname; } catch(ex) { }
|
||||
while ( hn ) {
|
||||
@ -119,10 +123,19 @@ while ( hn ) {
|
||||
try { scriptlet(...details.a); } catch(ex) {}
|
||||
}
|
||||
}
|
||||
if ( hn === '*' ) { break; }
|
||||
const pos = hn.indexOf('.');
|
||||
if ( pos === -1 ) { break; }
|
||||
if ( pos !== -1 ) {
|
||||
hn = hn.slice(pos + 1);
|
||||
} else {
|
||||
hn = '*';
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
argsMap.clear();
|
||||
hostnamesMap.clear();
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
|
683
platform/mv3/scriptlets/css-specific-procedural.js
Normal file
683
platform/mv3/scriptlets/css-specific-procedural.js
Normal file
@ -0,0 +1,683 @@
|
||||
/*******************************************************************************
|
||||
|
||||
uBlock Origin - a browser extension to block requests.
|
||||
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
|
||||
*/
|
||||
|
||||
/* jshint esversion:11 */
|
||||
|
||||
'use strict';
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
/// name css-specific-procedural
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// Important!
|
||||
// Isolate from global scope
|
||||
(function() {
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// $rulesetId$
|
||||
|
||||
const argsMap = new Map(self.$argsMap$);
|
||||
|
||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const addStylesheet = text => {
|
||||
try {
|
||||
const sheet = new CSSStyleSheet();
|
||||
sheet.replace(`@layer{${text}}`);
|
||||
document.adoptedStyleSheets = [
|
||||
...document.adoptedStyleSheets,
|
||||
sheet
|
||||
];
|
||||
} catch(ex) {
|
||||
}
|
||||
};
|
||||
|
||||
const nonVisualElements = {
|
||||
script: true,
|
||||
style: true,
|
||||
};
|
||||
|
||||
// 'P' stands for 'Procedural'
|
||||
|
||||
class PSelectorTask {
|
||||
begin() {
|
||||
}
|
||||
end() {
|
||||
}
|
||||
}
|
||||
|
||||
class PSelectorHasTextTask extends PSelectorTask {
|
||||
constructor(task) {
|
||||
super();
|
||||
let arg0 = task[1], arg1;
|
||||
if ( Array.isArray(task[1]) ) {
|
||||
arg1 = arg0[1]; arg0 = arg0[0];
|
||||
}
|
||||
this.needle = new RegExp(arg0, arg1);
|
||||
}
|
||||
transpose(node, output) {
|
||||
if ( this.needle.test(node.textContent) ) {
|
||||
output.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PSelectorIfTask extends PSelectorTask {
|
||||
constructor(task) {
|
||||
super();
|
||||
this.pselector = new PSelector(task[1]);
|
||||
}
|
||||
transpose(node, output) {
|
||||
if ( this.pselector.test(node) === this.target ) {
|
||||
output.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
PSelectorIfTask.prototype.target = true;
|
||||
|
||||
class PSelectorIfNotTask extends PSelectorIfTask {
|
||||
}
|
||||
PSelectorIfNotTask.prototype.target = false;
|
||||
|
||||
class PSelectorMatchesCSSTask extends PSelectorTask {
|
||||
constructor(task) {
|
||||
super();
|
||||
this.name = task[1].name;
|
||||
this.pseudo = task[1].pseudo ? `::${task[1].pseudo}` : null;
|
||||
let arg0 = task[1].value, arg1;
|
||||
if ( Array.isArray(arg0) ) {
|
||||
arg1 = arg0[1]; arg0 = arg0[0];
|
||||
}
|
||||
this.value = new RegExp(arg0, arg1);
|
||||
}
|
||||
transpose(node, output) {
|
||||
const style = window.getComputedStyle(node, this.pseudo);
|
||||
if ( style !== null && this.value.test(style[this.name]) ) {
|
||||
output.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PSelectorMatchesMediaTask extends PSelectorTask {
|
||||
constructor(task) {
|
||||
super();
|
||||
this.mql = window.matchMedia(task[1]);
|
||||
if ( this.mql.media === 'not all' ) { return; }
|
||||
this.mql.addEventListener('change', ( ) => {
|
||||
if ( typeof vAPI !== 'object' ) { return; }
|
||||
if ( vAPI === null ) { return; }
|
||||
const filterer = vAPI.domFilterer && vAPI.domFilterer.proceduralFilterer;
|
||||
if ( filterer instanceof Object === false ) { return; }
|
||||
filterer.onDOMChanged([ null ]);
|
||||
});
|
||||
}
|
||||
transpose(node, output) {
|
||||
if ( this.mql.matches === false ) { return; }
|
||||
output.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
class PSelectorMatchesPathTask extends PSelectorTask {
|
||||
constructor(task) {
|
||||
super();
|
||||
let arg0 = task[1], arg1;
|
||||
if ( Array.isArray(task[1]) ) {
|
||||
arg1 = arg0[1]; arg0 = arg0[0];
|
||||
}
|
||||
this.needle = new RegExp(arg0, arg1);
|
||||
}
|
||||
transpose(node, output) {
|
||||
if ( this.needle.test(self.location.pathname + self.location.search) ) {
|
||||
output.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PSelectorMinTextLengthTask extends PSelectorTask {
|
||||
constructor(task) {
|
||||
super();
|
||||
this.min = task[1];
|
||||
}
|
||||
transpose(node, output) {
|
||||
if ( node.textContent.length >= this.min ) {
|
||||
output.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PSelectorOthersTask extends PSelectorTask {
|
||||
constructor() {
|
||||
super();
|
||||
this.targets = new Set();
|
||||
}
|
||||
begin() {
|
||||
this.targets.clear();
|
||||
}
|
||||
end(output) {
|
||||
const toKeep = new Set(this.targets);
|
||||
const toDiscard = new Set();
|
||||
const body = document.body;
|
||||
let discard = null;
|
||||
for ( let keep of this.targets ) {
|
||||
while ( keep !== null && keep !== body ) {
|
||||
toKeep.add(keep);
|
||||
toDiscard.delete(keep);
|
||||
discard = keep.previousElementSibling;
|
||||
while ( discard !== null ) {
|
||||
if (
|
||||
nonVisualElements[discard.localName] !== true &&
|
||||
toKeep.has(discard) === false
|
||||
) {
|
||||
toDiscard.add(discard);
|
||||
}
|
||||
discard = discard.previousElementSibling;
|
||||
}
|
||||
discard = keep.nextElementSibling;
|
||||
while ( discard !== null ) {
|
||||
if (
|
||||
nonVisualElements[discard.localName] !== true &&
|
||||
toKeep.has(discard) === false
|
||||
) {
|
||||
toDiscard.add(discard);
|
||||
}
|
||||
discard = discard.nextElementSibling;
|
||||
}
|
||||
keep = keep.parentElement;
|
||||
}
|
||||
}
|
||||
for ( discard of toDiscard ) {
|
||||
output.push(discard);
|
||||
}
|
||||
this.targets.clear();
|
||||
}
|
||||
transpose(candidate) {
|
||||
for ( const target of this.targets ) {
|
||||
if ( target.contains(candidate) ) { return; }
|
||||
if ( candidate.contains(target) ) {
|
||||
this.targets.delete(target);
|
||||
}
|
||||
}
|
||||
this.targets.add(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/AdguardTeam/ExtendedCss/issues/31#issuecomment-302391277
|
||||
// Prepend `:scope ` if needed.
|
||||
class PSelectorSpathTask extends PSelectorTask {
|
||||
constructor(task) {
|
||||
super();
|
||||
this.spath = task[1];
|
||||
this.nth = /^(?:\s*[+~]|:)/.test(this.spath);
|
||||
if ( this.nth ) { return; }
|
||||
if ( /^\s*>/.test(this.spath) ) {
|
||||
this.spath = `:scope ${this.spath.trim()}`;
|
||||
}
|
||||
}
|
||||
qsa(node) {
|
||||
if ( this.nth === false ) {
|
||||
return node.querySelectorAll(this.spath);
|
||||
}
|
||||
const parent = node.parentElement;
|
||||
if ( parent === null ) { return; }
|
||||
let pos = 1;
|
||||
for (;;) {
|
||||
node = node.previousElementSibling;
|
||||
if ( node === null ) { break; }
|
||||
pos += 1;
|
||||
}
|
||||
return parent.querySelectorAll(
|
||||
`:scope > :nth-child(${pos})${this.spath}`
|
||||
);
|
||||
}
|
||||
transpose(node, output) {
|
||||
const nodes = this.qsa(node);
|
||||
if ( nodes === undefined ) { return; }
|
||||
for ( const node of nodes ) {
|
||||
output.push(node);
|
||||
}
|
||||
}
|
||||
// Helper method for other operators.
|
||||
static qsa(node, selector) {
|
||||
const parent = node.parentElement;
|
||||
if ( parent === null ) { return []; }
|
||||
let pos = 1;
|
||||
for (;;) {
|
||||
node = node.previousElementSibling;
|
||||
if ( node === null ) { break; }
|
||||
pos += 1;
|
||||
}
|
||||
return parent.querySelectorAll(
|
||||
`:scope > :nth-child(${pos})${selector}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PSelectorUpwardTask extends PSelectorTask {
|
||||
constructor(task) {
|
||||
super();
|
||||
const arg = task[1];
|
||||
if ( typeof arg === 'number' ) {
|
||||
this.i = arg;
|
||||
} else {
|
||||
this.s = arg;
|
||||
}
|
||||
}
|
||||
transpose(node, output) {
|
||||
if ( this.s !== '' ) {
|
||||
const parent = node.parentElement;
|
||||
if ( parent === null ) { return; }
|
||||
node = parent.closest(this.s);
|
||||
if ( node === null ) { return; }
|
||||
} else {
|
||||
let nth = this.i;
|
||||
for (;;) {
|
||||
node = node.parentElement;
|
||||
if ( node === null ) { return; }
|
||||
nth -= 1;
|
||||
if ( nth === 0 ) { break; }
|
||||
}
|
||||
}
|
||||
output.push(node);
|
||||
}
|
||||
}
|
||||
PSelectorUpwardTask.prototype.i = 0;
|
||||
PSelectorUpwardTask.prototype.s = '';
|
||||
|
||||
class PSelectorWatchAttrs extends PSelectorTask {
|
||||
constructor(task) {
|
||||
super();
|
||||
this.observer = null;
|
||||
this.observed = new WeakSet();
|
||||
this.observerOptions = {
|
||||
attributes: true,
|
||||
subtree: true,
|
||||
};
|
||||
const attrs = task[1];
|
||||
if ( Array.isArray(attrs) && attrs.length !== 0 ) {
|
||||
this.observerOptions.attributeFilter = task[1];
|
||||
}
|
||||
}
|
||||
// TODO: Is it worth trying to re-apply only the current selector?
|
||||
handler() {
|
||||
const filterer =
|
||||
vAPI.domFilterer && vAPI.domFilterer.proceduralFilterer;
|
||||
if ( filterer instanceof Object ) {
|
||||
filterer.onDOMChanged([ null ]);
|
||||
}
|
||||
}
|
||||
transpose(node, output) {
|
||||
output.push(node);
|
||||
if ( this.observed.has(node) ) { return; }
|
||||
if ( this.observer === null ) {
|
||||
this.observer = new MutationObserver(this.handler);
|
||||
}
|
||||
this.observer.observe(node, this.observerOptions);
|
||||
this.observed.add(node);
|
||||
}
|
||||
}
|
||||
|
||||
class PSelectorXpathTask extends PSelectorTask {
|
||||
constructor(task) {
|
||||
super();
|
||||
this.xpe = document.createExpression(task[1], null);
|
||||
this.xpr = null;
|
||||
}
|
||||
transpose(node, output) {
|
||||
this.xpr = this.xpe.evaluate(
|
||||
node,
|
||||
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
|
||||
this.xpr
|
||||
);
|
||||
let j = this.xpr.snapshotLength;
|
||||
while ( j-- ) {
|
||||
const node = this.xpr.snapshotItem(j);
|
||||
if ( node.nodeType === 1 ) {
|
||||
output.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PSelector {
|
||||
constructor(o) {
|
||||
if ( PSelector.prototype.operatorToTaskMap === undefined ) {
|
||||
PSelector.prototype.operatorToTaskMap = new Map([
|
||||
[ 'has', PSelectorIfTask ],
|
||||
[ 'has-text', PSelectorHasTextTask ],
|
||||
[ 'if', PSelectorIfTask ],
|
||||
[ 'if-not', PSelectorIfNotTask ],
|
||||
[ 'matches-css', PSelectorMatchesCSSTask ],
|
||||
[ 'matches-media', PSelectorMatchesMediaTask ],
|
||||
[ 'matches-path', PSelectorMatchesPathTask ],
|
||||
[ 'min-text-length', PSelectorMinTextLengthTask ],
|
||||
[ 'not', PSelectorIfNotTask ],
|
||||
[ 'others', PSelectorOthersTask ],
|
||||
[ 'spath', PSelectorSpathTask ],
|
||||
[ 'upward', PSelectorUpwardTask ],
|
||||
[ 'watch-attr', PSelectorWatchAttrs ],
|
||||
[ 'xpath', PSelectorXpathTask ],
|
||||
]);
|
||||
}
|
||||
this.raw = o.raw;
|
||||
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]);
|
||||
if ( ctor === undefined ) { return; }
|
||||
tasks.push(new ctor(task));
|
||||
}
|
||||
// Initialize only after all tasks have been successfully instantiated
|
||||
this.tasks = tasks;
|
||||
}
|
||||
prime(input) {
|
||||
const root = input || document;
|
||||
if ( this.selector === '' ) { return [ root ]; }
|
||||
let selector = this.selector;
|
||||
if ( input !== document && /^ [>+~]/.test(this.selector) ) {
|
||||
return Array.from(PSelectorSpathTask.qsa(input, this.selector));
|
||||
}
|
||||
const elems = root.querySelectorAll(selector);
|
||||
return Array.from(elems);
|
||||
}
|
||||
exec(input) {
|
||||
let nodes = this.prime(input);
|
||||
for ( const task of this.tasks ) {
|
||||
if ( nodes.length === 0 ) { break; }
|
||||
const transposed = [];
|
||||
task.begin();
|
||||
for ( const node of nodes ) {
|
||||
task.transpose(node, transposed);
|
||||
}
|
||||
task.end(transposed);
|
||||
nodes = transposed;
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
test(input) {
|
||||
const nodes = this.prime(input);
|
||||
for ( const node of nodes ) {
|
||||
let output = [ node ];
|
||||
for ( const task of this.tasks ) {
|
||||
const transposed = [];
|
||||
task.begin();
|
||||
for ( const node of output ) {
|
||||
task.transpose(node, transposed);
|
||||
}
|
||||
task.end(transposed);
|
||||
output = transposed;
|
||||
if ( output.length === 0 ) { break; }
|
||||
}
|
||||
if ( output.length !== 0 ) { return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
PSelector.prototype.operatorToTaskMap = undefined;
|
||||
|
||||
class PSelectorRoot extends PSelector {
|
||||
constructor(o, styleToken) {
|
||||
super(o);
|
||||
this.budget = 200; // I arbitrary picked a 1/5 second
|
||||
this.raw = o.raw;
|
||||
this.cost = 0;
|
||||
this.lastAllowanceTime = 0;
|
||||
this.styleToken = styleToken;
|
||||
}
|
||||
prime(input) {
|
||||
try {
|
||||
return super.prime(input);
|
||||
} catch (ex) {
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
class ProceduralFilterer {
|
||||
constructor(selectors) {
|
||||
this.selectors = [];
|
||||
this.masterToken = this.randomToken();
|
||||
this.styleTokenMap = new Map();
|
||||
this.styledNodes = new Set();
|
||||
this.timer = undefined;
|
||||
this.addSelectors(selectors);
|
||||
}
|
||||
|
||||
addSelectors() {
|
||||
for ( const selector of selectors ) {
|
||||
let style, styleToken;
|
||||
if ( selector.action === undefined ) {
|
||||
style = 'display:none!important;';
|
||||
} else if ( selector.action[0] === 'style' ) {
|
||||
style = selector.action[1];
|
||||
}
|
||||
if ( style !== undefined ) {
|
||||
styleToken = this.styleTokenFromStyle(style);
|
||||
}
|
||||
const pselector = new PSelectorRoot(selector, styleToken);
|
||||
this.selectors.push(pselector);
|
||||
}
|
||||
this.onDOMChanged();
|
||||
}
|
||||
|
||||
commitNow() {
|
||||
//console.time('procedural selectors/dom layout changed');
|
||||
|
||||
// https://github.com/uBlockOrigin/uBlock-issues/issues/341
|
||||
// Be ready to unhide nodes which no longer matches any of
|
||||
// the procedural selectors.
|
||||
const toUnstyle = this.styledNodes;
|
||||
this.styledNodes = new Set();
|
||||
|
||||
let t0 = Date.now();
|
||||
|
||||
for ( const pselector of this.selectors.values() ) {
|
||||
const allowance = Math.floor((t0 - pselector.lastAllowanceTime) / 2000);
|
||||
if ( allowance >= 1 ) {
|
||||
pselector.budget += allowance * 50;
|
||||
if ( pselector.budget > 200 ) { pselector.budget = 200; }
|
||||
pselector.lastAllowanceTime = t0;
|
||||
}
|
||||
if ( pselector.budget <= 0 ) { continue; }
|
||||
const nodes = pselector.exec();
|
||||
const t1 = Date.now();
|
||||
pselector.budget += t0 - t1;
|
||||
if ( pselector.budget < -500 ) {
|
||||
console.info('uBOL: disabling %s', pselector.raw);
|
||||
pselector.budget = -0x7FFFFFFF;
|
||||
}
|
||||
t0 = t1;
|
||||
if ( nodes.length === 0 ) { continue; }
|
||||
this.styleNodes(nodes, pselector.styleToken);
|
||||
}
|
||||
|
||||
this.unstyleNodes(toUnstyle);
|
||||
}
|
||||
|
||||
styleTokenFromStyle(style) {
|
||||
if ( style === undefined ) { return; }
|
||||
let styleToken = this.styleTokenMap.get(style);
|
||||
if ( styleToken !== undefined ) { return styleToken; }
|
||||
styleToken = this.randomToken();
|
||||
this.styleTokenMap.set(style, styleToken);
|
||||
addStylesheet(
|
||||
`[${this.masterToken}][${styleToken}]\n{${style}}\n`,
|
||||
);
|
||||
return styleToken;
|
||||
}
|
||||
|
||||
styleNodes(nodes, styleToken) {
|
||||
if ( styleToken === undefined ) {
|
||||
for ( const node of nodes ) {
|
||||
node.textContent = '';
|
||||
node.remove();
|
||||
}
|
||||
return;
|
||||
}
|
||||
for ( const node of nodes ) {
|
||||
node.setAttribute(this.masterToken, '');
|
||||
node.setAttribute(styleToken, '');
|
||||
this.styledNodes.add(node);
|
||||
}
|
||||
}
|
||||
|
||||
unstyleNodes(nodes) {
|
||||
for ( const node of nodes ) {
|
||||
if ( this.styledNodes.has(node) ) { continue; }
|
||||
node.removeAttribute(this.masterToken);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
onDOMChanged() {
|
||||
if ( this.timer !== undefined ) { return; }
|
||||
this.timer = self.requestAnimationFrame(( ) => {
|
||||
this.timer = undefined;
|
||||
this.commitNow();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
let hn;
|
||||
try { hn = document.location.hostname; } catch(ex) { }
|
||||
const selectors = [];
|
||||
while ( hn ) {
|
||||
if ( hostnamesMap.has(hn) ) {
|
||||
let argsHashes = hostnamesMap.get(hn);
|
||||
if ( typeof argsHashes === 'number' ) { argsHashes = [ argsHashes ]; }
|
||||
for ( const argsHash of argsHashes ) {
|
||||
const details = argsMap.get(argsHash);
|
||||
if ( details.n && details.n.includes(hn) ) { continue; }
|
||||
selectors.push(...details.a);
|
||||
}
|
||||
}
|
||||
if ( hn === '*' ) { break; }
|
||||
const pos = hn.indexOf('.');
|
||||
if ( pos !== -1 ) {
|
||||
hn = hn.slice(pos + 1);
|
||||
} else {
|
||||
hn = '*';
|
||||
}
|
||||
}
|
||||
|
||||
const proceduralSelectors = [];
|
||||
const styleSelectors = [];
|
||||
for ( const selector of selectors ) {
|
||||
if ( selector.cssable ) {
|
||||
styleSelectors.push(selector);
|
||||
} else {
|
||||
proceduralSelectors.push(selector);
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// Declarative selectors
|
||||
|
||||
if ( styleSelectors.length !== 0 ) {
|
||||
const cssRuleFromProcedural = details => {
|
||||
const { tasks, action } = details;
|
||||
let mq;
|
||||
if ( tasks !== undefined ) {
|
||||
if ( tasks.length > 1 ) { return; }
|
||||
if ( tasks[0][0] !== 'matches-media' ) { return; }
|
||||
mq = tasks[0][1];
|
||||
}
|
||||
let style;
|
||||
if ( Array.isArray(action) ) {
|
||||
if ( action[0] !== 'style' ) { return; }
|
||||
style = action[1];
|
||||
}
|
||||
if ( mq === undefined && style === undefined ) { return; }
|
||||
if ( mq === undefined ) {
|
||||
return `${details.selector}\n{${style}}`;
|
||||
}
|
||||
if ( style === undefined ) {
|
||||
return `@media ${mq} {\n${details.selector}\n{display:none!important;}\n}`;
|
||||
}
|
||||
return `@media ${mq} {\n${details.selector}\n{${style}}\n}`;
|
||||
};
|
||||
const sheetText = [];
|
||||
for ( const selector of styleSelectors ) {
|
||||
const ruleText = cssRuleFromProcedural(selector);
|
||||
if ( ruleText === undefined ) { continue; }
|
||||
sheetText.push(ruleText);
|
||||
}
|
||||
if ( sheetText.length !== 0 ) {
|
||||
addStylesheet(sheetText.join('\n'));
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// Procedural selectors
|
||||
|
||||
if ( proceduralSelectors.length !== 0 ) {
|
||||
const filterer = new ProceduralFilterer(proceduralSelectors);
|
||||
const observer = new MutationObserver(mutations => {
|
||||
let domChanged = false;
|
||||
for ( let i = 0; i < mutations.length && !domChanged; i++ ) {
|
||||
const mutation = mutations[i];
|
||||
for ( const added of mutation.addedNodes ) {
|
||||
if ( added.nodeType !== 1 ) { continue; }
|
||||
domChanged = true;
|
||||
}
|
||||
for ( const removed of mutation.removedNodes ) {
|
||||
if ( removed.nodeType !== 1 ) { continue; }
|
||||
domChanged = true;
|
||||
}
|
||||
}
|
||||
if ( domChanged === false ) { return; }
|
||||
filterer.onDOMChanged();
|
||||
});
|
||||
observer.observe(document, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
argsMap.clear();
|
||||
hostnamesMap.clear();
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
})();
|
||||
|
||||
/******************************************************************************/
|
@ -17,9 +17,6 @@
|
||||
along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
|
||||
Home: https://github.com/gorhill/uBlock
|
||||
|
||||
The scriptlets below are meant to be injected only into a
|
||||
web page context.
|
||||
*/
|
||||
|
||||
/* jshint esversion:11 */
|
||||
@ -38,10 +35,14 @@
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// $rulesetId$
|
||||
|
||||
const argsMap = new Map(self.$argsMap$);
|
||||
|
||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
let hn;
|
||||
try { hn = document.location.hostname; } catch(ex) { }
|
||||
const styles = [];
|
||||
@ -55,9 +56,13 @@ while ( hn ) {
|
||||
styles.push(details.a);
|
||||
}
|
||||
}
|
||||
if ( hn === '*' ) { break; }
|
||||
const pos = hn.indexOf('.');
|
||||
if ( pos === -1 ) { break; }
|
||||
if ( pos !== -1 ) {
|
||||
hn = hn.slice(pos + 1);
|
||||
} else {
|
||||
hn = '*';
|
||||
}
|
||||
}
|
||||
|
||||
if ( styles.length === 0 ) { return; }
|
||||
@ -74,6 +79,11 @@ try {
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
argsMap.clear();
|
||||
hostnamesMap.clear();
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
})();
|
||||
|
||||
/******************************************************************************/
|
||||
|
@ -38,6 +38,14 @@
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// $rulesetId$
|
||||
|
||||
const argsMap = new Map(self.$argsMap$);
|
||||
|
||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// https://github.com/uBlockOrigin/uBlock-issues/issues/1545
|
||||
// - Add support for "remove everything if needle matches" case
|
||||
|
||||
@ -121,10 +129,6 @@ const scriptlet = (
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const argsMap = new Map(self.$argsMap$);
|
||||
|
||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||
|
||||
let hn;
|
||||
try { hn = document.location.hostname; } catch(ex) { }
|
||||
while ( hn ) {
|
||||
@ -137,10 +141,19 @@ while ( hn ) {
|
||||
try { scriptlet(...details.a); } catch(ex) {}
|
||||
}
|
||||
}
|
||||
if ( hn === '*' ) { break; }
|
||||
const pos = hn.indexOf('.');
|
||||
if ( pos === -1 ) { break; }
|
||||
if ( pos !== -1 ) {
|
||||
hn = hn.slice(pos + 1);
|
||||
} else {
|
||||
hn = '*';
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
argsMap.clear();
|
||||
hostnamesMap.clear();
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
|
@ -39,6 +39,14 @@
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// $rulesetId$
|
||||
|
||||
const argsMap = new Map(self.$argsMap$);
|
||||
|
||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const scriptlet = (
|
||||
chain = '',
|
||||
cValue = ''
|
||||
@ -163,10 +171,6 @@ const scriptlet = (
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const argsMap = new Map(self.$argsMap$);
|
||||
|
||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||
|
||||
let hn;
|
||||
try { hn = document.location.hostname; } catch(ex) { }
|
||||
while ( hn ) {
|
||||
@ -179,10 +183,19 @@ while ( hn ) {
|
||||
try { scriptlet(...details.a); } catch(ex) {}
|
||||
}
|
||||
}
|
||||
if ( hn === '*' ) { break; }
|
||||
const pos = hn.indexOf('.');
|
||||
if ( pos === -1 ) { break; }
|
||||
if ( pos !== -1 ) {
|
||||
hn = hn.slice(pos + 1);
|
||||
} else {
|
||||
hn = '*';
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
argsMap.clear();
|
||||
hostnamesMap.clear();
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
|
@ -37,10 +37,11 @@ import {
|
||||
function addExtendedToDNR(context, parser) {
|
||||
if ( parser.category !== parser.CATStaticExtFilter ) { return false; }
|
||||
|
||||
if ( (parser.flavorBits & parser.BITFlavorUnsupported) !== 0 ) { return; }
|
||||
|
||||
// Scriptlet injection
|
||||
if ( (parser.flavorBits & parser.BITFlavorExtScriptlet) !== 0 ) {
|
||||
if ( (parser.flavorBits & parser.BITFlavorUnsupported) !== 0 ) {
|
||||
return;
|
||||
}
|
||||
if ( parser.hasOptions() === false ) { return; }
|
||||
if ( context.scriptletFilters === undefined ) {
|
||||
context.scriptletFilters = new Map();
|
||||
@ -95,16 +96,24 @@ function addExtendedToDNR(context, parser) {
|
||||
// of same filter OR globally if there is no non-negated hostnames.
|
||||
for ( const { hn, not, bad } of parser.extOptions() ) {
|
||||
if ( bad ) { continue; }
|
||||
if ( hn.endsWith('.*') ) { continue; }
|
||||
const { compiled, exception } = parser.result;
|
||||
if ( typeof compiled !== 'string' ) { continue; }
|
||||
if ( compiled.startsWith('{') ) { continue; }
|
||||
let { compiled, exception, raw } = parser.result;
|
||||
if ( exception ) { continue; }
|
||||
let rejected;
|
||||
if ( compiled === undefined ) {
|
||||
rejected = `Invalid filter: ${hn}##${raw}`;
|
||||
} else if ( hn.endsWith('.*') ) {
|
||||
rejected = `Entity not supported: ${hn}##${raw}`;
|
||||
}
|
||||
if ( rejected ) {
|
||||
compiled = rejected;
|
||||
}
|
||||
let details = context.cosmeticFilters.get(compiled);
|
||||
if ( details === undefined ) {
|
||||
details = {};
|
||||
if ( rejected ) { details.rejected = true; }
|
||||
context.cosmeticFilters.set(compiled, details);
|
||||
}
|
||||
if ( rejected ) { continue; }
|
||||
if ( not ) {
|
||||
if ( details.excludeMatches === undefined ) {
|
||||
details.excludeMatches = [];
|
||||
|
@ -1436,14 +1436,13 @@ Parser.prototype.SelectorCompiler = class {
|
||||
}
|
||||
|
||||
out.compiled = this.compileSelector(raw);
|
||||
if ( out.compiled === undefined ) {
|
||||
console.log('Error:', raw);
|
||||
return false;
|
||||
}
|
||||
if ( out.compiled === undefined ) { return false; }
|
||||
|
||||
if ( out.compiled instanceof Object ) {
|
||||
out.compiled.raw = raw;
|
||||
out.compiled = JSON.stringify(out.compiled);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user