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.excludeMatches = matchesFromHostnames(entry.excludeMatches);
|
||||||
}
|
}
|
||||||
directive.js = [ `/rulesets/js/${fname.slice(0,2)}/${fname.slice(2)}.js` ];
|
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';
|
directive.runAt = 'document_start';
|
||||||
|
}
|
||||||
if ( (fidFromFileName(fname) & MAIN_WORLD_BIT) !== 0 ) {
|
if ( (fidFromFileName(fname) & MAIN_WORLD_BIT) !== 0 ) {
|
||||||
directive.world = 'MAIN';
|
directive.world = 'MAIN';
|
||||||
}
|
}
|
||||||
return directive;
|
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 toCSSFileId = s => (uidint32(s) & ~0b11) | 0b00;
|
||||||
const toJSFileId = s => uidint32(s) | 0b1;
|
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`;
|
const pathFromFileName = fname => `${scriptletDir}/${fname.slice(0,2)}/${fname.slice(2)}.js`;
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
async function processCosmeticFilters(assetDetails, mapin) {
|
const COSMETIC_FILES_PER_RULESET = 12;
|
||||||
if ( mapin === undefined ) { return 0; }
|
const PROCEDURAL_FILES_PER_RULESET = 4;
|
||||||
|
|
||||||
// This groups together selectors which are used by the same hostname.
|
// This merges selectors which are used by the same hostnames
|
||||||
const optimized = (filters => {
|
|
||||||
if ( filters === undefined ) { return []; }
|
function groupCosmeticByHostnames(mapin) {
|
||||||
const merge = new Map();
|
if ( mapin === undefined ) { return []; }
|
||||||
for ( const [ selector, details ] of filters ) {
|
const merged = new Map();
|
||||||
|
for ( const [ selector, details ] of mapin ) {
|
||||||
const json = JSON.stringify(details);
|
const json = JSON.stringify(details);
|
||||||
let entries = merge.get(json);
|
let entries = merged.get(json);
|
||||||
if ( entries === undefined ) {
|
if ( entries === undefined ) {
|
||||||
entries = new Set();
|
entries = new Set();
|
||||||
merge.set(json, entries);
|
merged.set(json, entries);
|
||||||
}
|
}
|
||||||
entries.add(selector);
|
entries.add(selector);
|
||||||
}
|
}
|
||||||
const out = [];
|
const out = [];
|
||||||
for ( const [ json, entries ] of merge ) {
|
for ( const [ json, entries ] of merged ) {
|
||||||
const details = JSON.parse(json);
|
const details = JSON.parse(json);
|
||||||
details.selector = Array.from(entries).join(',');
|
details.selectors = Array.from(entries).sort();
|
||||||
out.push(details);
|
out.push(details);
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
})(mapin);
|
}
|
||||||
|
|
||||||
// This creates a map of unique selectorset => all hostnames
|
// This merges hostnames which have the same set of selectors.
|
||||||
// including/excluding the selectorset. This allows to avoid duplication
|
//
|
||||||
// of css content.
|
// Also, we sort the hostnames to increase likelihood that selector with
|
||||||
const cssContentMap = new Map();
|
// same hostnames will end up in same generated scriptlet.
|
||||||
for ( const entry of optimized ) {
|
|
||||||
// ends-with 0 = css resource
|
function groupCosmeticBySelectors(arrayin) {
|
||||||
const id = uidint32(entry.selector);
|
const contentMap = new Map();
|
||||||
let details = cssContentMap.get(id);
|
for ( const entry of arrayin ) {
|
||||||
|
const id = uidint32(JSON.stringify(entry.selectors));
|
||||||
|
let details = contentMap.get(id);
|
||||||
if ( details === undefined ) {
|
if ( details === undefined ) {
|
||||||
details = { a: entry.selector };
|
details = { a: entry.selectors };
|
||||||
cssContentMap.set(id, details);
|
contentMap.set(id, details);
|
||||||
}
|
}
|
||||||
if ( entry.matches !== undefined ) {
|
if ( entry.matches !== undefined ) {
|
||||||
if ( details.y === 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
|
const scriptletHostnameToIdMap = (hostnames, id, map) => {
|
||||||
// group multiple unrelated selectors in the same file, and distinct
|
for ( const hn of hostnames ) {
|
||||||
// css declarations will be injected programmatically according to the
|
const existing = map.get(hn);
|
||||||
// hostname of the current document.
|
if ( existing === undefined ) {
|
||||||
//
|
map.set(hn, id);
|
||||||
// The cosmetic filters will be injected programmatically as content
|
} else if ( Array.isArray(existing) ) {
|
||||||
// script and the decisions to activate the cosmetic filters will be
|
existing.push(id);
|
||||||
// done at injection time according to the document's hostname.
|
} else {
|
||||||
const originalScriptletMap = await loadAllSourceScriptlets();
|
map.set(hn, [ existing, id ]);
|
||||||
const contentPerFile = Math.ceil(cssContentMap.size / 16);
|
}
|
||||||
const cssContentArray = Array.from(cssContentMap);
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const jsonReplacer = (k, v) => {
|
const scriptletJsonReplacer = (k, v) => {
|
||||||
if ( k === 'n' ) {
|
if ( k === 'n' ) {
|
||||||
if ( v === undefined || v.size === 0 ) { return; }
|
if ( v === undefined || v.size === 0 ) { return; }
|
||||||
return Array.from(v);
|
return Array.from(v);
|
||||||
@ -452,40 +475,53 @@ async function processCosmeticFilters(assetDetails, mapin) {
|
|||||||
return v;
|
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 = [];
|
const generatedFiles = [];
|
||||||
|
|
||||||
for ( let i = 0; i < cssContentArray.length; i += contentPerFile ) {
|
for ( let i = 0; i < contentArray.length; i += contentPerFile ) {
|
||||||
const slice = cssContentArray.slice(i, i + contentPerFile);
|
const slice = contentArray.slice(i, i + contentPerFile);
|
||||||
const argsMap = slice.map(entry => [
|
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();
|
const hostnamesMap = new Map();
|
||||||
for ( const [ id, details ] of slice ) {
|
for ( const [ id, details ] of slice ) {
|
||||||
if ( details.y === undefined ) { continue; }
|
if ( details.y === undefined ) { continue; }
|
||||||
toHostnamesMap(details.y, id, hostnamesMap);
|
scriptletHostnameToIdMap(details.y, id, hostnamesMap);
|
||||||
}
|
}
|
||||||
const patchedScriptlet = originalScriptletMap.get('css-specific')
|
const patchedScriptlet = originalScriptletMap.get('css-specific')
|
||||||
.replace(
|
.replace(
|
||||||
|
'$rulesetId$',
|
||||||
|
assetDetails.id
|
||||||
|
).replace(
|
||||||
/\bself\.\$argsMap\$/m,
|
/\bself\.\$argsMap\$/m,
|
||||||
`${JSON.stringify(argsMap, jsonReplacer)}`
|
`${JSON.stringify(argsMap, scriptletJsonReplacer)}`
|
||||||
).replace(
|
).replace(
|
||||||
/\bself\.\$hostnamesMap\$/m,
|
/\bself\.\$hostnamesMap\$/m,
|
||||||
`${JSON.stringify(hostnamesMap, jsonReplacer)}`
|
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
|
||||||
);
|
);
|
||||||
// ends-with 0 = css resource
|
|
||||||
const fid = toCSSFileId(patchedScriptlet);
|
const fid = toCSSFileId(patchedScriptlet);
|
||||||
if ( globalPatchedScriptletsSet.has(fid) === false ) {
|
if ( globalPatchedScriptletsSet.has(fid) === false ) {
|
||||||
globalPatchedScriptletsSet.add(fid);
|
globalPatchedScriptletsSet.add(fid);
|
||||||
@ -510,12 +546,91 @@ async function processCosmeticFilters(assetDetails, mapin) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ( generatedFiles.length !== 0 ) {
|
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(`CSS-related injectable files: ${generatedFiles.length}`);
|
||||||
log(`\t${generatedFiles.join(', ')}`);
|
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 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 ) {
|
for ( const [ token, argsDetails ] of scriptletDetails ) {
|
||||||
const argsMap = Array.from(argsDetails).map(entry => [
|
const argsMap = Array.from(argsDetails).map(entry => [
|
||||||
uidint32(entry[0]),
|
uidint32(entry[0]),
|
||||||
@ -637,15 +727,18 @@ async function processScriptletFilters(assetDetails, mapin) {
|
|||||||
]);
|
]);
|
||||||
const hostnamesMap = new Map();
|
const hostnamesMap = new Map();
|
||||||
for ( const [ argsHash, details ] of argsDetails ) {
|
for ( const [ argsHash, details ] of argsDetails ) {
|
||||||
toHostnamesMap(details.y, uidint32(argsHash), hostnamesMap);
|
scriptletHostnameToIdMap(details.y, uidint32(argsHash), hostnamesMap);
|
||||||
}
|
}
|
||||||
const patchedScriptlet = originalScriptletMap.get(token)
|
const patchedScriptlet = originalScriptletMap.get(token)
|
||||||
.replace(
|
.replace(
|
||||||
|
'$rulesetId$',
|
||||||
|
assetDetails.id
|
||||||
|
).replace(
|
||||||
/\bself\.\$argsMap\$/m,
|
/\bself\.\$argsMap\$/m,
|
||||||
`${JSON.stringify(argsMap, jsonReplacer)}`
|
`${JSON.stringify(argsMap, scriptletJsonReplacer)}`
|
||||||
).replace(
|
).replace(
|
||||||
/\bself\.\$hostnamesMap\$/m,
|
/\bself\.\$hostnamesMap\$/m,
|
||||||
`${JSON.stringify(hostnamesMap, jsonReplacer)}`
|
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
|
||||||
);
|
);
|
||||||
// ends-with 1 = scriptlet resource
|
// ends-with 1 = scriptlet resource
|
||||||
const fid = toJSFileId(patchedScriptlet);
|
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) {
|
const rulesetFromURLS = async function(assetDetails) {
|
||||||
log('============================');
|
log('============================');
|
||||||
log(`Listset for '${assetDetails.id}':`);
|
log(`Listset for '${assetDetails.id}':`);
|
||||||
@ -722,10 +793,37 @@ async function main() {
|
|||||||
results.network
|
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(
|
const cosmeticStats = await processCosmeticFilters(
|
||||||
assetDetails,
|
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(
|
const scriptletStats = await processScriptletFilters(
|
||||||
assetDetails,
|
assetDetails,
|
||||||
@ -752,6 +850,7 @@ async function main() {
|
|||||||
},
|
},
|
||||||
css: {
|
css: {
|
||||||
specific: cosmeticStats,
|
specific: cosmeticStats,
|
||||||
|
procedural: proceduralStats,
|
||||||
},
|
},
|
||||||
scriptlets: {
|
scriptlets: {
|
||||||
total: scriptletStats,
|
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
|
// Get assets.json content
|
||||||
const assets = await fs.readFile(
|
const assets = await fs.readFile(
|
||||||
`./assets.json`,
|
`./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:
|
// Issues to mind before changing anything:
|
||||||
// https://github.com/uBlockOrigin/uBlock-issues/issues/2154
|
// 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;
|
let hn;
|
||||||
try { hn = document.location.hostname; } catch(ex) { }
|
try { hn = document.location.hostname; } catch(ex) { }
|
||||||
while ( hn ) {
|
while ( hn ) {
|
||||||
@ -161,10 +165,19 @@ while ( hn ) {
|
|||||||
try { scriptlet(...details.a); } catch(ex) {}
|
try { scriptlet(...details.a); } catch(ex) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ( hn === '*' ) { break; }
|
||||||
const pos = hn.indexOf('.');
|
const pos = hn.indexOf('.');
|
||||||
if ( pos === -1 ) { break; }
|
if ( pos !== -1 ) {
|
||||||
hn = hn.slice(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 ObjGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
|
||||||
const ObjDefineProperty = Object.defineProperty;
|
const ObjDefineProperty = Object.defineProperty;
|
||||||
|
|
||||||
@ -103,10 +111,6 @@ const scriptlet = (
|
|||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
const argsMap = new Map(self.$argsMap$);
|
|
||||||
|
|
||||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
|
||||||
|
|
||||||
let hn;
|
let hn;
|
||||||
try { hn = document.location.hostname; } catch(ex) { }
|
try { hn = document.location.hostname; } catch(ex) { }
|
||||||
while ( hn ) {
|
while ( hn ) {
|
||||||
@ -119,10 +123,19 @@ while ( hn ) {
|
|||||||
try { scriptlet(...details.a); } catch(ex) {}
|
try { scriptlet(...details.a); } catch(ex) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ( hn === '*' ) { break; }
|
||||||
const pos = hn.indexOf('.');
|
const pos = hn.indexOf('.');
|
||||||
if ( pos === -1 ) { break; }
|
if ( pos !== -1 ) {
|
||||||
hn = hn.slice(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/}.
|
along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||||
|
|
||||||
Home: https://github.com/gorhill/uBlock
|
Home: https://github.com/gorhill/uBlock
|
||||||
|
|
||||||
The scriptlets below are meant to be injected only into a
|
|
||||||
web page context.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* jshint esversion:11 */
|
/* jshint esversion:11 */
|
||||||
@ -38,10 +35,14 @@
|
|||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
|
// $rulesetId$
|
||||||
|
|
||||||
const argsMap = new Map(self.$argsMap$);
|
const argsMap = new Map(self.$argsMap$);
|
||||||
|
|
||||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||||
|
|
||||||
|
/******************************************************************************/
|
||||||
|
|
||||||
let hn;
|
let hn;
|
||||||
try { hn = document.location.hostname; } catch(ex) { }
|
try { hn = document.location.hostname; } catch(ex) { }
|
||||||
const styles = [];
|
const styles = [];
|
||||||
@ -55,9 +56,13 @@ while ( hn ) {
|
|||||||
styles.push(details.a);
|
styles.push(details.a);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ( hn === '*' ) { break; }
|
||||||
const pos = hn.indexOf('.');
|
const pos = hn.indexOf('.');
|
||||||
if ( pos === -1 ) { break; }
|
if ( pos !== -1 ) {
|
||||||
hn = hn.slice(pos + 1);
|
hn = hn.slice(pos + 1);
|
||||||
|
} else {
|
||||||
|
hn = '*';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( styles.length === 0 ) { return; }
|
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
|
// https://github.com/uBlockOrigin/uBlock-issues/issues/1545
|
||||||
// - Add support for "remove everything if needle matches" case
|
// - 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;
|
let hn;
|
||||||
try { hn = document.location.hostname; } catch(ex) { }
|
try { hn = document.location.hostname; } catch(ex) { }
|
||||||
while ( hn ) {
|
while ( hn ) {
|
||||||
@ -137,10 +141,19 @@ while ( hn ) {
|
|||||||
try { scriptlet(...details.a); } catch(ex) {}
|
try { scriptlet(...details.a); } catch(ex) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ( hn === '*' ) { break; }
|
||||||
const pos = hn.indexOf('.');
|
const pos = hn.indexOf('.');
|
||||||
if ( pos === -1 ) { break; }
|
if ( pos !== -1 ) {
|
||||||
hn = hn.slice(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 = (
|
const scriptlet = (
|
||||||
chain = '',
|
chain = '',
|
||||||
cValue = ''
|
cValue = ''
|
||||||
@ -163,10 +171,6 @@ const scriptlet = (
|
|||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
const argsMap = new Map(self.$argsMap$);
|
|
||||||
|
|
||||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
|
||||||
|
|
||||||
let hn;
|
let hn;
|
||||||
try { hn = document.location.hostname; } catch(ex) { }
|
try { hn = document.location.hostname; } catch(ex) { }
|
||||||
while ( hn ) {
|
while ( hn ) {
|
||||||
@ -179,10 +183,19 @@ while ( hn ) {
|
|||||||
try { scriptlet(...details.a); } catch(ex) {}
|
try { scriptlet(...details.a); } catch(ex) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ( hn === '*' ) { break; }
|
||||||
const pos = hn.indexOf('.');
|
const pos = hn.indexOf('.');
|
||||||
if ( pos === -1 ) { break; }
|
if ( pos !== -1 ) {
|
||||||
hn = hn.slice(pos + 1);
|
hn = hn.slice(pos + 1);
|
||||||
|
} else {
|
||||||
|
hn = '*';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************************************************************/
|
||||||
|
|
||||||
|
argsMap.clear();
|
||||||
|
hostnamesMap.clear();
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
|
@ -37,10 +37,11 @@ import {
|
|||||||
function addExtendedToDNR(context, parser) {
|
function addExtendedToDNR(context, parser) {
|
||||||
if ( parser.category !== parser.CATStaticExtFilter ) { return false; }
|
if ( parser.category !== parser.CATStaticExtFilter ) { return false; }
|
||||||
|
|
||||||
if ( (parser.flavorBits & parser.BITFlavorUnsupported) !== 0 ) { return; }
|
|
||||||
|
|
||||||
// Scriptlet injection
|
// Scriptlet injection
|
||||||
if ( (parser.flavorBits & parser.BITFlavorExtScriptlet) !== 0 ) {
|
if ( (parser.flavorBits & parser.BITFlavorExtScriptlet) !== 0 ) {
|
||||||
|
if ( (parser.flavorBits & parser.BITFlavorUnsupported) !== 0 ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if ( parser.hasOptions() === false ) { return; }
|
if ( parser.hasOptions() === false ) { return; }
|
||||||
if ( context.scriptletFilters === undefined ) {
|
if ( context.scriptletFilters === undefined ) {
|
||||||
context.scriptletFilters = new Map();
|
context.scriptletFilters = new Map();
|
||||||
@ -95,16 +96,24 @@ function addExtendedToDNR(context, parser) {
|
|||||||
// of same filter OR globally if there is no non-negated hostnames.
|
// of same filter OR globally if there is no non-negated hostnames.
|
||||||
for ( const { hn, not, bad } of parser.extOptions() ) {
|
for ( const { hn, not, bad } of parser.extOptions() ) {
|
||||||
if ( bad ) { continue; }
|
if ( bad ) { continue; }
|
||||||
if ( hn.endsWith('.*') ) { continue; }
|
let { compiled, exception, raw } = parser.result;
|
||||||
const { compiled, exception } = parser.result;
|
|
||||||
if ( typeof compiled !== 'string' ) { continue; }
|
|
||||||
if ( compiled.startsWith('{') ) { continue; }
|
|
||||||
if ( exception ) { continue; }
|
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);
|
let details = context.cosmeticFilters.get(compiled);
|
||||||
if ( details === undefined ) {
|
if ( details === undefined ) {
|
||||||
details = {};
|
details = {};
|
||||||
|
if ( rejected ) { details.rejected = true; }
|
||||||
context.cosmeticFilters.set(compiled, details);
|
context.cosmeticFilters.set(compiled, details);
|
||||||
}
|
}
|
||||||
|
if ( rejected ) { continue; }
|
||||||
if ( not ) {
|
if ( not ) {
|
||||||
if ( details.excludeMatches === undefined ) {
|
if ( details.excludeMatches === undefined ) {
|
||||||
details.excludeMatches = [];
|
details.excludeMatches = [];
|
||||||
|
@ -1436,14 +1436,13 @@ Parser.prototype.SelectorCompiler = class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
out.compiled = this.compileSelector(raw);
|
out.compiled = this.compileSelector(raw);
|
||||||
if ( out.compiled === undefined ) {
|
if ( out.compiled === undefined ) { return false; }
|
||||||
console.log('Error:', raw);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if ( out.compiled instanceof Object ) {
|
if ( out.compiled instanceof Object ) {
|
||||||
out.compiled.raw = raw;
|
out.compiled.raw = raw;
|
||||||
out.compiled = JSON.stringify(out.compiled);
|
out.compiled = JSON.stringify(out.compiled);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user