mirror of
https://github.com/gorhill/uBlock.git
synced 2025-01-31 12:11:36 +01:00
[mv3] General code review
Re-arranged resources in a more tidy way. General code review of various code paths.
This commit is contained in:
parent
30bd6c7bb8
commit
1db3748ab1
@ -42,7 +42,6 @@ import {
|
||||
} from './ruleset-manager.js';
|
||||
|
||||
import {
|
||||
getInjectableCount,
|
||||
registerInjectables,
|
||||
} from './scripting-manager.js';
|
||||
|
||||
@ -63,6 +62,8 @@ const rulesetConfig = {
|
||||
firstRun: false,
|
||||
};
|
||||
|
||||
const UBOL_ORIGIN = runtime.getURL('').replace(/\/$/, '');
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function getCurrentVersion() {
|
||||
@ -165,6 +166,9 @@ function onPermissionsRemoved(permissions) {
|
||||
/******************************************************************************/
|
||||
|
||||
function onMessage(request, sender, callback) {
|
||||
|
||||
if ( sender.origin !== UBOL_ORIGIN ) { return; }
|
||||
|
||||
switch ( request.what ) {
|
||||
|
||||
case 'applyRulesets': {
|
||||
@ -214,7 +218,6 @@ function onMessage(request, sender, callback) {
|
||||
hasOmnipotence(),
|
||||
hasGreatPowers(request.origin),
|
||||
getEnabledRulesetsDetails(),
|
||||
getInjectableCount(request.origin),
|
||||
]).then(results => {
|
||||
callback({
|
||||
level: results[0],
|
||||
@ -222,7 +225,6 @@ function onMessage(request, sender, callback) {
|
||||
hasOmnipotence: results[1],
|
||||
hasGreatPowers: results[2],
|
||||
rulesetDetails: results[3],
|
||||
injectableCount: results[4],
|
||||
});
|
||||
});
|
||||
return true;
|
||||
|
@ -101,7 +101,7 @@ async function updateRegexRules() {
|
||||
const toFetch = [];
|
||||
for ( const details of rulesetDetails ) {
|
||||
if ( details.rules.regexes === 0 ) { continue; }
|
||||
toFetch.push(fetchJSON(`/rulesets/${details.id}.regexes`));
|
||||
toFetch.push(fetchJSON(`/rulesets/regex/${details.id}.regexes`));
|
||||
}
|
||||
const regexRulesets = await Promise.all(toFetch);
|
||||
|
||||
@ -196,7 +196,7 @@ async function updateRemoveparamRules() {
|
||||
const toFetch = [];
|
||||
for ( const details of rulesetDetails ) {
|
||||
if ( details.rules.removeparams === 0 ) { continue; }
|
||||
toFetch.push(fetchJSON(`/rulesets/${details.id}.removeparams`));
|
||||
toFetch.push(fetchJSON(`/rulesets/removeparam/${details.id}.removeparams`));
|
||||
}
|
||||
const removeparamRulesets = await Promise.all(toFetch);
|
||||
|
||||
|
@ -25,7 +25,7 @@
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
import { browser, dnr } from './ext.js';
|
||||
import { browser } from './ext.js';
|
||||
import { fetchJSON } from './fetch.js';
|
||||
import { getFilteringModeDetails } from './mode-manager.js';
|
||||
import { getEnabledRulesetsDetails } from './ruleset-manager.js';
|
||||
@ -34,29 +34,69 @@ import * as ut from './utils.js';
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
let scriptingDetailsPromise;
|
||||
const resourceDetailPromises = new Map();
|
||||
|
||||
function getScriptingDetails() {
|
||||
if ( scriptingDetailsPromise !== undefined ) {
|
||||
return scriptingDetailsPromise;
|
||||
}
|
||||
scriptingDetailsPromise = fetchJSON('/rulesets/scripting-details').then(entries => {
|
||||
function getSpecificDetails() {
|
||||
let promise = resourceDetailPromises.get('specific');
|
||||
if ( promise !== undefined ) { return promise; }
|
||||
promise = fetchJSON('/rulesets/specific-details').then(entries => {
|
||||
const out = new Map();
|
||||
for ( const entry of entries ) {
|
||||
out.set(entry[0], new Map(entry[1]));
|
||||
}
|
||||
return out;
|
||||
});
|
||||
return scriptingDetailsPromise;
|
||||
resourceDetailPromises.set('specific', promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
function getDeclarativeDetails() {
|
||||
let promise = resourceDetailPromises.get('declarative');
|
||||
if ( promise !== undefined ) { return promise; }
|
||||
promise = fetchJSON('/rulesets/declarative-details').then(
|
||||
entries => new Map(entries)
|
||||
);
|
||||
resourceDetailPromises.set('declarative', promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
function getProceduralDetails() {
|
||||
let promise = resourceDetailPromises.get('procedural');
|
||||
if ( promise !== undefined ) { return promise; }
|
||||
promise = fetchJSON('/rulesets/procedural-details').then(
|
||||
entries => new Map(entries)
|
||||
);
|
||||
resourceDetailPromises.set('procedural', promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
function getScriptletDetails() {
|
||||
let promise = resourceDetailPromises.get('scriptlet');
|
||||
if ( promise !== undefined ) { return promise; }
|
||||
promise = fetchJSON('/rulesets/scriptlet-details').then(
|
||||
entries => new Map(entries)
|
||||
);
|
||||
resourceDetailPromises.set('scriptlet', promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
function getGenericDetails() {
|
||||
let promise = resourceDetailPromises.get('generic');
|
||||
if ( promise !== undefined ) { return promise; }
|
||||
promise = fetchJSON('/rulesets/generic-details').then(
|
||||
entries => new Map(entries)
|
||||
);
|
||||
resourceDetailPromises.set('generic', promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// Important: We need to sort the arrays for fast comparison
|
||||
const arrayEq = (a = [], b = []) => {
|
||||
const arrayEq = (a = [], b = [], sort = true) => {
|
||||
const alen = a.length;
|
||||
if ( alen !== b.length ) { return false; }
|
||||
a.sort(); b.sort();
|
||||
if ( sort ) { a.sort(); b.sort(); }
|
||||
for ( let i = 0; i < alen; i++ ) {
|
||||
if ( a[i] !== b[i] ) { return false; }
|
||||
}
|
||||
@ -65,81 +105,41 @@ const arrayEq = (a = [], b = []) => {
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const toRegisterableScript = (context, fname, hostnames) => {
|
||||
if ( context.before.has(fname) ) {
|
||||
return toUpdatableScript(context, fname, hostnames);
|
||||
}
|
||||
const matches = hostnames
|
||||
? ut.matchesFromHostnames(hostnames)
|
||||
: [ '<all_urls>' ];
|
||||
const excludeMatches = matches.length === 1 && matches[0] === '<all_urls>'
|
||||
? ut.matchesFromHostnames(context.filteringModeDetails.none)
|
||||
: [];
|
||||
const runAt = (ut.fidFromFileName(fname) & RUN_AT_END_BIT) !== 0
|
||||
? 'document_end'
|
||||
: 'document_start';
|
||||
const directive = {
|
||||
id: fname,
|
||||
allFrames: true,
|
||||
matches,
|
||||
excludeMatches,
|
||||
js: [ `/rulesets/js/${fname.slice(0,2)}/${fname.slice(2)}.js` ],
|
||||
runAt,
|
||||
};
|
||||
if ( (ut.fidFromFileName(fname) & MAIN_WORLD_BIT) !== 0 ) {
|
||||
directive.world = 'MAIN';
|
||||
}
|
||||
context.toAdd.push(directive);
|
||||
};
|
||||
// The extensions API does not always return exactly what we fed it, so we
|
||||
// need to normalize some entries to be sure we properly detect changes when
|
||||
// comparing registered entries vs. entries to register.
|
||||
|
||||
const toUpdatableScript = (context, fname, hostnames) => {
|
||||
const registered = context.before.get(fname);
|
||||
context.before.delete(fname); // Important!
|
||||
const directive = { id: fname };
|
||||
const matches = hostnames
|
||||
? ut.matchesFromHostnames(hostnames)
|
||||
: [ '<all_urls>' ];
|
||||
if ( arrayEq(registered.matches, matches) === false ) {
|
||||
directive.matches = matches;
|
||||
const normalizeRegisteredContentScripts = registered => {
|
||||
for ( const entry of registered ) {
|
||||
const { js } = entry;
|
||||
for ( let i = 0; i < js.length; i++ ) {
|
||||
const path = js[i];
|
||||
if ( path.startsWith('/') ) { continue; }
|
||||
js[i] = `/${path}`;
|
||||
}
|
||||
const excludeMatches = matches.length === 1 && matches[0] === '<all_urls>'
|
||||
? ut.matchesFromHostnames(context.filteringModeDetails.none)
|
||||
: [];
|
||||
if ( arrayEq(registered.excludeMatches, excludeMatches) === false ) {
|
||||
directive.excludeMatches = excludeMatches;
|
||||
}
|
||||
if ( directive.matches || directive.excludeMatches ) {
|
||||
context.toUpdate.push(directive);
|
||||
}
|
||||
return registered;
|
||||
};
|
||||
|
||||
const RUN_AT_END_BIT = 0b10;
|
||||
const MAIN_WORLD_BIT = 0b01;
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
async function registerGeneric(context, args) {
|
||||
const { before } = context;
|
||||
const registered = before.get('css-generic');
|
||||
before.delete('css-generic'); // Important!
|
||||
|
||||
const {
|
||||
filteringModeDetails,
|
||||
rulesetsDetails,
|
||||
} = args;
|
||||
function registerGeneric(context, genericDetails) {
|
||||
const { before, filteringModeDetails, rulesetsDetails } = context;
|
||||
|
||||
const excludeHostnames = [];
|
||||
const js = [];
|
||||
for ( const details of rulesetsDetails ) {
|
||||
const hostnames = genericDetails.get(details.id);
|
||||
if ( hostnames !== undefined ) {
|
||||
excludeHostnames.push(...hostnames);
|
||||
}
|
||||
if ( details.css.generic.count === 0 ) { continue; }
|
||||
js.push(`/rulesets/js/${details.id}.generic.js`);
|
||||
js.push(`/rulesets/scripting/generic/${details.id}.generic.js`);
|
||||
}
|
||||
|
||||
if ( js.length === 0 ) {
|
||||
if ( registered !== undefined ) {
|
||||
context.toRemove.push('css-generic');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ( js.length === 0 ) { return; }
|
||||
|
||||
js.push('/js/scripting/css-generic.js');
|
||||
|
||||
const matches = [];
|
||||
const excludeMatches = [];
|
||||
@ -147,17 +147,23 @@ async function registerGeneric(context, args) {
|
||||
excludeMatches.push(...ut.matchesFromHostnames(filteringModeDetails.none));
|
||||
excludeMatches.push(...ut.matchesFromHostnames(filteringModeDetails.network));
|
||||
excludeMatches.push(...ut.matchesFromHostnames(filteringModeDetails.extendedSpecific));
|
||||
excludeMatches.push(...ut.matchesFromHostnames(excludeHostnames));
|
||||
matches.push('<all_urls>');
|
||||
} else {
|
||||
matches.push(...ut.matchesFromHostnames(filteringModeDetails.extendedGeneric));
|
||||
matches.push(
|
||||
...ut.matchesFromHostnames(
|
||||
ut.subtractHostnameIters(
|
||||
Array.from(filteringModeDetails.extendedGeneric),
|
||||
excludeHostnames
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( matches.length === 0 ) {
|
||||
if ( registered !== undefined ) {
|
||||
context.toRemove.push('css-generic');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ( matches.length === 0 ) { return; }
|
||||
|
||||
const registered = before.get('css-generic');
|
||||
before.delete('css-generic'); // Important!
|
||||
|
||||
// register
|
||||
if ( registered === undefined ) {
|
||||
@ -173,7 +179,7 @@ async function registerGeneric(context, args) {
|
||||
|
||||
// update
|
||||
const directive = { id: 'css-generic' };
|
||||
if ( arrayEq(registered.js, js) === false ) {
|
||||
if ( arrayEq(registered.js, js, false) === false ) {
|
||||
directive.js = js;
|
||||
}
|
||||
if ( arrayEq(registered.matches, matches) === false ) {
|
||||
@ -189,62 +195,258 @@ async function registerGeneric(context, args) {
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
async function getInjectableCount(origin) {
|
||||
const url = ut.parsedURLromOrigin(origin);
|
||||
if ( url === undefined ) { return 0; }
|
||||
function registerProcedural(context, proceduralDetails) {
|
||||
const { before, filteringModeDetails, rulesetsDetails } = context;
|
||||
|
||||
const [
|
||||
rulesetIds,
|
||||
scriptingDetails,
|
||||
] = await Promise.all([
|
||||
dnr.getEnabledRulesets(),
|
||||
getScriptingDetails(),
|
||||
]);
|
||||
|
||||
let total = 0;
|
||||
|
||||
for ( const rulesetId of rulesetIds ) {
|
||||
const hostnamesToFidsMap = scriptingDetails.get(rulesetId);
|
||||
if ( hostnamesToFidsMap === undefined ) { continue; }
|
||||
let hn = url.hostname;
|
||||
while ( hn !== '' ) {
|
||||
const fids = hostnamesToFidsMap.get(hn);
|
||||
if ( typeof fids === 'number' ) {
|
||||
total += 1;
|
||||
} else if ( Array.isArray(fids) ) {
|
||||
total += fids.length;
|
||||
}
|
||||
hn = ut.toBroaderHostname(hn);
|
||||
const js = [];
|
||||
const hostnameMatches = [];
|
||||
for ( const details of rulesetsDetails ) {
|
||||
if ( details.css.procedural === 0 ) { continue; }
|
||||
js.push(`/rulesets/scripting/procedural/${details.id}.procedural.js`);
|
||||
if ( proceduralDetails.has(details.id) ) {
|
||||
hostnameMatches.push(...proceduralDetails.get(details.id));
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
if ( js.length === 0 ) { return; }
|
||||
|
||||
js.push('/js/scripting/css-procedural.js');
|
||||
|
||||
const {
|
||||
none,
|
||||
network,
|
||||
extendedSpecific,
|
||||
extendedGeneric,
|
||||
} = filteringModeDetails;
|
||||
|
||||
const matches = [];
|
||||
const excludeMatches = [];
|
||||
if ( extendedSpecific.has('all-urls') || extendedGeneric.has('all-urls') ) {
|
||||
excludeMatches.push(...ut.matchesFromHostnames(none));
|
||||
excludeMatches.push(...ut.matchesFromHostnames(network));
|
||||
matches.push(...ut.matchesFromHostnames(hostnameMatches));
|
||||
} else if ( extendedSpecific.size !== 0 || extendedGeneric.size !== 0 ) {
|
||||
matches.push(
|
||||
...ut.matchesFromHostnames(
|
||||
ut.intersectHostnameIters(
|
||||
[ ...extendedSpecific, ...extendedGeneric ],
|
||||
hostnameMatches
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( matches.length === 0 ) { return; }
|
||||
|
||||
const registered = before.get('css-procedural');
|
||||
before.delete('css-procedural'); // Important!
|
||||
|
||||
// register
|
||||
if ( registered === undefined ) {
|
||||
context.toAdd.push({
|
||||
id: 'css-procedural',
|
||||
js,
|
||||
matches,
|
||||
excludeMatches,
|
||||
runAt: 'document_end',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// update
|
||||
const directive = { id: 'css-procedural' };
|
||||
if ( arrayEq(registered.js, js, false) === false ) {
|
||||
directive.js = js;
|
||||
}
|
||||
if ( arrayEq(registered.matches, matches) === false ) {
|
||||
directive.matches = matches;
|
||||
}
|
||||
if ( arrayEq(registered.excludeMatches, excludeMatches) === false ) {
|
||||
directive.excludeMatches = excludeMatches;
|
||||
}
|
||||
if ( directive.js || directive.matches || directive.excludeMatches ) {
|
||||
context.toUpdate.push(directive);
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function registerSpecific(args) {
|
||||
const {
|
||||
filteringModeDetails,
|
||||
rulesetsDetails,
|
||||
scriptingDetails,
|
||||
} = args;
|
||||
function registerDeclarative(context, declarativeDetails) {
|
||||
const { before, filteringModeDetails, rulesetsDetails } = context;
|
||||
|
||||
// Combined both specific and generic sets
|
||||
const js = [];
|
||||
const hostnameMatches = [];
|
||||
for ( const details of rulesetsDetails ) {
|
||||
if ( details.css.declarative === 0 ) { continue; }
|
||||
js.push(`/rulesets/scripting/declarative/${details.id}.declarative.js`);
|
||||
if ( declarativeDetails.has(details.id) ) {
|
||||
hostnameMatches.push(...declarativeDetails.get(details.id));
|
||||
}
|
||||
}
|
||||
|
||||
if ( js.length === 0 ) { return; }
|
||||
|
||||
js.push('/js/scripting/css-declarative.js');
|
||||
|
||||
const {
|
||||
none,
|
||||
network,
|
||||
extendedSpecific,
|
||||
extendedGeneric,
|
||||
} = filteringModeDetails;
|
||||
|
||||
const matches = [];
|
||||
const excludeMatches = [];
|
||||
if ( extendedSpecific.has('all-urls') || extendedGeneric.has('all-urls') ) {
|
||||
excludeMatches.push(...ut.matchesFromHostnames(none));
|
||||
excludeMatches.push(...ut.matchesFromHostnames(network));
|
||||
matches.push(...ut.matchesFromHostnames(hostnameMatches));
|
||||
} else if ( extendedSpecific.size !== 0 || extendedGeneric.size !== 0 ) {
|
||||
matches.push(
|
||||
...ut.matchesFromHostnames(
|
||||
ut.intersectHostnameIters(
|
||||
[ ...extendedSpecific, ...extendedGeneric ],
|
||||
hostnameMatches
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( matches.length === 0 ) { return; }
|
||||
|
||||
const registered = before.get('css-declarative');
|
||||
before.delete('css-declarative'); // Important!
|
||||
|
||||
// register
|
||||
if ( registered === undefined ) {
|
||||
context.toAdd.push({
|
||||
id: 'css-declarative',
|
||||
js,
|
||||
matches,
|
||||
excludeMatches,
|
||||
runAt: 'document_start',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// update
|
||||
const directive = { id: 'css-declarative' };
|
||||
if ( arrayEq(registered.js, js, false) === false ) {
|
||||
directive.js = js;
|
||||
}
|
||||
if ( arrayEq(registered.matches, matches) === false ) {
|
||||
directive.matches = matches;
|
||||
}
|
||||
if ( arrayEq(registered.excludeMatches, excludeMatches) === false ) {
|
||||
directive.excludeMatches = excludeMatches;
|
||||
}
|
||||
if ( directive.js || directive.matches || directive.excludeMatches ) {
|
||||
context.toUpdate.push(directive);
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function registerScriptlet(context, scriptletDetails) {
|
||||
const { before, filteringModeDetails, rulesetsDetails } = context;
|
||||
|
||||
const hasBroadHostPermission =
|
||||
filteringModeDetails.extendedSpecific.has('all-urls') ||
|
||||
filteringModeDetails.extendedGeneric.has('all-urls');
|
||||
|
||||
const permissionRevokedMatches = [
|
||||
...ut.matchesFromHostnames(filteringModeDetails.none),
|
||||
...ut.matchesFromHostnames(filteringModeDetails.network),
|
||||
];
|
||||
const permissionGrantedHostnames = [
|
||||
...filteringModeDetails.extendedSpecific,
|
||||
...filteringModeDetails.extendedGeneric,
|
||||
];
|
||||
|
||||
for ( const rulesetId of rulesetsDetails.map(v => v.id) ) {
|
||||
const scriptletList = scriptletDetails.get(rulesetId);
|
||||
if ( scriptletList === undefined ) { continue; }
|
||||
|
||||
for ( const [ token, scriptletHostnames ] of scriptletList ) {
|
||||
const id = `${rulesetId}.${token}`;
|
||||
const registered = before.get(id);
|
||||
|
||||
const matches = [];
|
||||
const excludeMatches = [];
|
||||
if ( hasBroadHostPermission ) {
|
||||
excludeMatches.push(...permissionRevokedMatches);
|
||||
matches.push(...ut.matchesFromHostnames(scriptletHostnames));
|
||||
} else if ( permissionGrantedHostnames.length !== 0 ) {
|
||||
matches.push(
|
||||
...ut.matchesFromHostnames(
|
||||
ut.intersectHostnameIters(
|
||||
permissionGrantedHostnames,
|
||||
scriptletHostnames
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
if ( matches.length === 0 ) { continue; }
|
||||
|
||||
before.delete(id); // Important!
|
||||
|
||||
// register
|
||||
if ( registered === undefined ) {
|
||||
context.toAdd.push({
|
||||
id,
|
||||
js: [ `/rulesets/scripting/scriptlet/${id}.js` ],
|
||||
matches,
|
||||
excludeMatches,
|
||||
runAt: 'document_start',
|
||||
world: 'MAIN',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// update
|
||||
const directive = { id };
|
||||
if ( arrayEq(registered.matches, matches) === false ) {
|
||||
directive.matches = matches;
|
||||
}
|
||||
if ( arrayEq(registered.excludeMatches, excludeMatches) === false ) {
|
||||
directive.excludeMatches = excludeMatches;
|
||||
}
|
||||
if ( directive.matches || directive.excludeMatches ) {
|
||||
context.toUpdate.push(directive);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function registerSpecific(context, specificDetails) {
|
||||
const { filteringModeDetails } = context;
|
||||
|
||||
let toRegisterMap;
|
||||
if (
|
||||
filteringModeDetails.extendedSpecific.has('all-urls') ||
|
||||
filteringModeDetails.extendedGeneric.has('all-urls')
|
||||
) {
|
||||
return registerAllSpecific(args);
|
||||
toRegisterMap = registerSpecificAll(context, specificDetails);
|
||||
} else {
|
||||
toRegisterMap = registerSpecificSome(context, specificDetails);
|
||||
}
|
||||
|
||||
for ( const [ fname, hostnames ] of toRegisterMap ) {
|
||||
toRegisterableScript(context, fname, hostnames);
|
||||
}
|
||||
}
|
||||
|
||||
function registerSpecificSome(context, specificDetails) {
|
||||
const { filteringModeDetails, rulesetsDetails } = context;
|
||||
const toRegisterMap = new Map();
|
||||
|
||||
const targetHostnames = [
|
||||
...filteringModeDetails.extendedSpecific,
|
||||
...filteringModeDetails.extendedGeneric,
|
||||
];
|
||||
|
||||
const toRegisterMap = new Map();
|
||||
|
||||
const checkMatches = (hostnamesToFidsMap, hn) => {
|
||||
let fids = hostnamesToFidsMap.get(hn);
|
||||
if ( fids === undefined ) { return; }
|
||||
@ -266,7 +468,7 @@ function registerSpecific(args) {
|
||||
};
|
||||
|
||||
for ( const rulesetDetails of rulesetsDetails ) {
|
||||
const hostnamesToFidsMap = scriptingDetails.get(rulesetDetails.id);
|
||||
const hostnamesToFidsMap = specificDetails.get(rulesetDetails.id);
|
||||
if ( hostnamesToFidsMap === undefined ) { continue; }
|
||||
for ( let hn of targetHostnames ) {
|
||||
while ( hn ) {
|
||||
@ -279,21 +481,17 @@ function registerSpecific(args) {
|
||||
return toRegisterMap;
|
||||
}
|
||||
|
||||
function registerAllSpecific(args) {
|
||||
const {
|
||||
filteringModeDetails,
|
||||
rulesetsDetails,
|
||||
scriptingDetails,
|
||||
} = args;
|
||||
|
||||
function registerSpecificAll(context, specificDetails) {
|
||||
const { filteringModeDetails, rulesetsDetails } = context;
|
||||
const toRegisterMap = new Map();
|
||||
|
||||
const excludeSet = new Set([
|
||||
...filteringModeDetails.network,
|
||||
...filteringModeDetails.none,
|
||||
]);
|
||||
|
||||
for ( const rulesetDetails of rulesetsDetails ) {
|
||||
const hostnamesToFidsMap = scriptingDetails.get(rulesetDetails.id);
|
||||
const hostnamesToFidsMap = specificDetails.get(rulesetDetails.id);
|
||||
if ( hostnamesToFidsMap === undefined ) { continue; }
|
||||
for ( let [ hn, fids ] of hostnamesToFidsMap ) {
|
||||
if ( excludeSet.has(hn) ) { continue; }
|
||||
@ -319,6 +517,48 @@ function registerAllSpecific(args) {
|
||||
return toRegisterMap;
|
||||
}
|
||||
|
||||
const toRegisterableScript = (context, fname, hostnames) => {
|
||||
if ( context.before.has(fname) ) {
|
||||
return toUpdatableScript(context, fname, hostnames);
|
||||
}
|
||||
const matches = hostnames
|
||||
? ut.matchesFromHostnames(hostnames)
|
||||
: [ '<all_urls>' ];
|
||||
const excludeMatches = matches.length === 1 && matches[0] === '<all_urls>'
|
||||
? ut.matchesFromHostnames(context.filteringModeDetails.none)
|
||||
: [];
|
||||
const directive = {
|
||||
id: fname,
|
||||
allFrames: true,
|
||||
matches,
|
||||
excludeMatches,
|
||||
js: [ `/rulesets/scripting/specific/${fname.slice(-1)}/${fname.slice(0,-1)}.js` ],
|
||||
runAt: 'document_start',
|
||||
};
|
||||
context.toAdd.push(directive);
|
||||
};
|
||||
|
||||
const toUpdatableScript = (context, fname, hostnames) => {
|
||||
const registered = context.before.get(fname);
|
||||
context.before.delete(fname); // Important!
|
||||
const directive = { id: fname };
|
||||
const matches = hostnames
|
||||
? ut.matchesFromHostnames(hostnames)
|
||||
: [ '<all_urls>' ];
|
||||
if ( arrayEq(registered.matches, matches) === false ) {
|
||||
directive.matches = matches;
|
||||
}
|
||||
const excludeMatches = matches.length === 1 && matches[0] === '<all_urls>'
|
||||
? ut.matchesFromHostnames(context.filteringModeDetails.none)
|
||||
: [];
|
||||
if ( arrayEq(registered.excludeMatches, excludeMatches) === false ) {
|
||||
directive.excludeMatches = excludeMatches;
|
||||
}
|
||||
if ( directive.matches || directive.excludeMatches ) {
|
||||
context.toUpdate.push(directive);
|
||||
}
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
async function registerInjectables(origins) {
|
||||
@ -329,37 +569,44 @@ async function registerInjectables(origins) {
|
||||
const [
|
||||
filteringModeDetails,
|
||||
rulesetsDetails,
|
||||
scriptingDetails,
|
||||
declarativeDetails,
|
||||
proceduralDetails,
|
||||
scriptletDetails,
|
||||
specificDetails,
|
||||
genericDetails,
|
||||
registered,
|
||||
] = await Promise.all([
|
||||
getFilteringModeDetails(),
|
||||
getEnabledRulesetsDetails(),
|
||||
getScriptingDetails(),
|
||||
getDeclarativeDetails(),
|
||||
getProceduralDetails(),
|
||||
getScriptletDetails(),
|
||||
getSpecificDetails(),
|
||||
getGenericDetails(),
|
||||
browser.scripting.getRegisteredContentScripts(),
|
||||
]);
|
||||
|
||||
const before = new Map(registered.map(entry => [ entry.id, entry ]));
|
||||
const before = new Map(
|
||||
normalizeRegisteredContentScripts(registered).map(
|
||||
entry => [ entry.id, entry ]
|
||||
)
|
||||
);
|
||||
const toAdd = [], toUpdate = [], toRemove = [];
|
||||
const promises = [];
|
||||
const context = {
|
||||
filteringModeDetails,
|
||||
rulesetsDetails,
|
||||
before,
|
||||
toAdd,
|
||||
toUpdate,
|
||||
toRemove,
|
||||
};
|
||||
|
||||
await registerGeneric(context, { filteringModeDetails, rulesetsDetails, });
|
||||
registerDeclarative(context, declarativeDetails);
|
||||
registerProcedural(context, proceduralDetails);
|
||||
registerScriptlet(context, scriptletDetails);
|
||||
registerSpecific(context, specificDetails);
|
||||
registerGeneric(context, genericDetails);
|
||||
|
||||
const toRegisterMap = registerSpecific({
|
||||
filteringModeDetails,
|
||||
rulesetsDetails,
|
||||
scriptingDetails,
|
||||
});
|
||||
|
||||
for ( const [ fname, hostnames ] of toRegisterMap ) {
|
||||
toRegisterableScript(context, fname, hostnames);
|
||||
}
|
||||
toRemove.push(...Array.from(before.keys()));
|
||||
|
||||
if ( toRemove.length !== 0 ) {
|
||||
@ -391,6 +638,5 @@ async function registerInjectables(origins) {
|
||||
/******************************************************************************/
|
||||
|
||||
export {
|
||||
getInjectableCount,
|
||||
registerInjectables
|
||||
};
|
||||
|
115
platform/mv3/extension/js/scripting/css-declarative.js
Normal file
115
platform/mv3/extension/js/scripting/css-declarative.js
Normal file
@ -0,0 +1,115 @@
|
||||
/*******************************************************************************
|
||||
|
||||
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';
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// Important!
|
||||
// Isolate from global scope
|
||||
(function uBOL_cssDeclarative() {
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const declarativeImports = self.declarativeImports || [];
|
||||
|
||||
const lookupSelectors = (hn, out) => {
|
||||
for ( const { argsList, hostnamesMap } of declarativeImports ) {
|
||||
let argsIndices = hostnamesMap.get(hn);
|
||||
if ( argsIndices === undefined ) { continue; }
|
||||
if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
|
||||
for ( const argsIndex of argsIndices ) {
|
||||
const details = argsList[argsIndex];
|
||||
if ( details.n && details.n.includes(hn) ) { continue; }
|
||||
out.push(...details.a.map(json => JSON.parse(json)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let hn;
|
||||
try { hn = document.location.hostname; } catch(ex) { }
|
||||
const selectors = [];
|
||||
while ( hn ) {
|
||||
lookupSelectors(hn, selectors);
|
||||
if ( hn === '*' ) { break; }
|
||||
const pos = hn.indexOf('.');
|
||||
if ( pos !== -1 ) {
|
||||
hn = hn.slice(pos + 1);
|
||||
} else {
|
||||
hn = '*';
|
||||
}
|
||||
}
|
||||
|
||||
declarativeImports.length = 0;
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
if ( selectors.length === 0 ) { return; }
|
||||
|
||||
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 selectors ) {
|
||||
const ruleText = cssRuleFromProcedural(selector);
|
||||
if ( ruleText === undefined ) { continue; }
|
||||
sheetText.push(ruleText);
|
||||
}
|
||||
|
||||
if ( sheetText.length === 0 ) { return; }
|
||||
|
||||
try {
|
||||
const sheet = new CSSStyleSheet();
|
||||
sheet.replace(`@layer{${sheetText.join('\n')}}`);
|
||||
document.adoptedStyleSheets = [
|
||||
...document.adoptedStyleSheets,
|
||||
sheet
|
||||
];
|
||||
} catch(ex) {
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
})();
|
||||
|
||||
/******************************************************************************/
|
241
platform/mv3/extension/js/scripting/css-generic.js
Normal file
241
platform/mv3/extension/js/scripting/css-generic.js
Normal file
@ -0,0 +1,241 @@
|
||||
/*******************************************************************************
|
||||
|
||||
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';
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// Important!
|
||||
// Isolate from global scope
|
||||
(function uBOL_cssGeneric() {
|
||||
|
||||
const genericSelectorMap = self.genericSelectorMap || new Map();
|
||||
if ( genericSelectorMap.size === 0 ) { return; }
|
||||
|
||||
self.genericSelectorMap = undefined;
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const maxSurveyTimeSlice = 4;
|
||||
const maxSurveyNodeSlice = 64;
|
||||
const styleSheetSelectors = [];
|
||||
const stopAllRatio = 0.95; // To be investigated
|
||||
|
||||
let surveyCount = 0;
|
||||
let surveyMissCount = 0;
|
||||
let styleSheetTimer;
|
||||
let processTimer;
|
||||
let domChangeTimer;
|
||||
let lastDomChange = Date.now();
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// https://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
|
||||
const hashFromStr = (type, s) => {
|
||||
const len = s.length;
|
||||
const step = len + 7 >>> 3;
|
||||
let hash = type;
|
||||
for ( let i = 0; i < len; i += step ) {
|
||||
hash = (hash << 5) - hash + s.charCodeAt(i) | 0;
|
||||
}
|
||||
return hash & 0x00FFFFFF;
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// Extract all classes/ids: these will be passed to the cosmetic
|
||||
// filtering engine, and in return we will obtain only the relevant
|
||||
// CSS selectors.
|
||||
|
||||
// https://github.com/gorhill/uBlock/issues/672
|
||||
// http://www.w3.org/TR/2014/REC-html5-20141028/infrastructure.html#space-separated-tokens
|
||||
// http://jsperf.com/enumerate-classes/6
|
||||
|
||||
const uBOL_idFromNode = (node, out) => {
|
||||
const raw = node.id;
|
||||
if ( typeof raw !== 'string' || raw.length === 0 ) { return; }
|
||||
out.push(hashFromStr(0x23 /* '#' */, raw.trim()));
|
||||
};
|
||||
|
||||
// https://github.com/uBlockOrigin/uBlock-issues/discussions/2076
|
||||
// Performance: avoid using Element.classList
|
||||
const uBOL_classesFromNode = (node, out) => {
|
||||
const s = node.getAttribute('class');
|
||||
if ( typeof s !== 'string' ) { return; }
|
||||
const len = s.length;
|
||||
for ( let beg = 0, end = 0; beg < len; beg += 1 ) {
|
||||
end = s.indexOf(' ', beg);
|
||||
if ( end === beg ) { continue; }
|
||||
if ( end === -1 ) { end = len; }
|
||||
out.push(hashFromStr(0x2E /* '.' */, s.slice(beg, end)));
|
||||
beg = end;
|
||||
}
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const pendingNodes = {
|
||||
addedNodes: [],
|
||||
nodeSet: new Set(),
|
||||
add(node) {
|
||||
this.addedNodes.push(node);
|
||||
},
|
||||
next(out) {
|
||||
for ( const added of this.addedNodes ) {
|
||||
if ( this.nodeSet.has(added) ) { continue; }
|
||||
if ( added.nodeType === 1 ) {
|
||||
this.nodeSet.add(added);
|
||||
}
|
||||
if ( added.firstElementChild === null ) { continue; }
|
||||
for ( const descendant of added.querySelectorAll('[id],[class]') ) {
|
||||
this.nodeSet.add(descendant);
|
||||
}
|
||||
}
|
||||
this.addedNodes.length = 0;
|
||||
for ( const node of this.nodeSet ) {
|
||||
this.nodeSet.delete(node);
|
||||
out.push(node);
|
||||
if ( out.length === maxSurveyNodeSlice ) { break; }
|
||||
}
|
||||
},
|
||||
hasNodes() {
|
||||
return this.addedNodes.length !== 0 || this.nodeSet.size !== 0;
|
||||
},
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const uBOL_processNodes = ( ) => {
|
||||
const t0 = Date.now();
|
||||
const hashes = [];
|
||||
const nodes = [];
|
||||
const deadline = t0 + maxSurveyTimeSlice;
|
||||
for (;;) {
|
||||
pendingNodes.next(nodes);
|
||||
if ( nodes.length === 0 ) { break; }
|
||||
for ( const node of nodes ) {
|
||||
uBOL_idFromNode(node, hashes);
|
||||
uBOL_classesFromNode(node, hashes);
|
||||
}
|
||||
nodes.length = 0;
|
||||
if ( performance.now() >= deadline ) { break; }
|
||||
}
|
||||
for ( const hash of hashes ) {
|
||||
const selectorList = genericSelectorMap.get(hash);
|
||||
if ( selectorList === undefined ) { continue; }
|
||||
styleSheetSelectors.push(selectorList);
|
||||
genericSelectorMap.delete(hash);
|
||||
}
|
||||
surveyCount += 1;
|
||||
if ( styleSheetSelectors.length === 0 ) {
|
||||
surveyMissCount += 1;
|
||||
if (
|
||||
surveyCount >= 100 &&
|
||||
(surveyMissCount / surveyCount) >= stopAllRatio
|
||||
) {
|
||||
stopAll(`too many misses in surveyor (${surveyMissCount}/${surveyCount})`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ( styleSheetTimer !== undefined ) { return; }
|
||||
styleSheetTimer = self.requestAnimationFrame(( ) => {
|
||||
styleSheetTimer = undefined;
|
||||
uBOL_injectStyleSheet();
|
||||
});
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const uBOL_processChanges = mutations => {
|
||||
for ( let i = 0; i < mutations.length; i++ ) {
|
||||
const mutation = mutations[i];
|
||||
for ( const added of mutation.addedNodes ) {
|
||||
if ( added.nodeType !== 1 ) { continue; }
|
||||
pendingNodes.add(added);
|
||||
}
|
||||
}
|
||||
if ( pendingNodes.hasNodes() === false ) { return; }
|
||||
lastDomChange = Date.now();
|
||||
if ( processTimer !== undefined ) { return; }
|
||||
processTimer = self.setTimeout(( ) => {
|
||||
processTimer = undefined;
|
||||
uBOL_processNodes();
|
||||
}, 64);
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const uBOL_injectStyleSheet = ( ) => {
|
||||
try {
|
||||
const sheet = new CSSStyleSheet();
|
||||
sheet.replace(`@layer{${styleSheetSelectors.join(',')}{display:none!important;}}`);
|
||||
document.adoptedStyleSheets = [
|
||||
...document.adoptedStyleSheets,
|
||||
sheet
|
||||
];
|
||||
} catch(ex) {
|
||||
}
|
||||
styleSheetSelectors.length = 0;
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
pendingNodes.add(document);
|
||||
uBOL_processNodes();
|
||||
|
||||
let domMutationObserver = new MutationObserver(uBOL_processChanges);
|
||||
domMutationObserver.observe(document, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
const needDomChangeObserver = ( ) => {
|
||||
domChangeTimer = undefined;
|
||||
if ( domMutationObserver === undefined ) { return; }
|
||||
if ( (Date.now() - lastDomChange) > 20000 ) {
|
||||
return stopAll('no more DOM changes');
|
||||
}
|
||||
domChangeTimer = self.setTimeout(needDomChangeObserver, 20000);
|
||||
};
|
||||
|
||||
needDomChangeObserver();
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const stopAll = reason => {
|
||||
if ( domChangeTimer !== undefined ) {
|
||||
self.clearTimeout(domChangeTimer);
|
||||
domChangeTimer = undefined;
|
||||
}
|
||||
domMutationObserver.disconnect();
|
||||
domMutationObserver.takeRecords();
|
||||
domMutationObserver = undefined;
|
||||
genericSelectorMap.clear();
|
||||
console.info(`uBOL: Generic cosmetic filtering stopped because ${reason}`);
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
})();
|
||||
|
||||
/******************************************************************************/
|
662
platform/mv3/extension/js/scripting/css-procedural.js
Normal file
662
platform/mv3/extension/js/scripting/css-procedural.js
Normal file
@ -0,0 +1,662 @@
|
||||
/*******************************************************************************
|
||||
|
||||
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';
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// Important!
|
||||
// Isolate from global scope
|
||||
(function uBOL_cssProcedural() {
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
let proceduralFilterer;
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
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 PSelectorVoidTask extends PSelectorTask {
|
||||
constructor(task) {
|
||||
super();
|
||||
console.info(`uBO: :${task[0]}() operator does not exist`);
|
||||
}
|
||||
transpose() {
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
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 PSelectorMatchesCSSAfterTask extends PSelectorMatchesCSSTask {
|
||||
constructor(task) {
|
||||
super(task);
|
||||
this.pseudo = '::after';
|
||||
}
|
||||
}
|
||||
|
||||
class PSelectorMatchesCSSBeforeTask extends PSelectorMatchesCSSTask {
|
||||
constructor(task) {
|
||||
super(task);
|
||||
this.pseudo = '::before';
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
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 ( proceduralFilterer instanceof Object === false ) { return; }
|
||||
proceduralFilterer.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()}`;
|
||||
}
|
||||
}
|
||||
transpose(node, output) {
|
||||
const nodes = this.nth
|
||||
? PSelectorSpathTask.qsa(node, this.spath)
|
||||
: node.querySelectorAll(this.spath);
|
||||
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() {
|
||||
if ( proceduralFilterer instanceof Object ) {
|
||||
proceduralFilterer.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) {
|
||||
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]) || PSelectorVoidTask;
|
||||
tasks.push(new ctor(task));
|
||||
}
|
||||
this.tasks = tasks;
|
||||
}
|
||||
prime(input) {
|
||||
const root = input || document;
|
||||
if ( this.selector === '' ) { return [ root ]; }
|
||||
if ( input !== document && /^ [>+~]/.test(this.selector) ) {
|
||||
return Array.from(PSelectorSpathTask.qsa(input, this.selector));
|
||||
}
|
||||
return Array.from(root.querySelectorAll(this.selector));
|
||||
}
|
||||
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 = new Map([
|
||||
[ 'has', PSelectorIfTask ],
|
||||
[ 'has-text', PSelectorHasTextTask ],
|
||||
[ 'if', PSelectorIfTask ],
|
||||
[ 'if-not', PSelectorIfNotTask ],
|
||||
[ 'matches-css', PSelectorMatchesCSSTask ],
|
||||
[ 'matches-css-after', PSelectorMatchesCSSAfterTask ],
|
||||
[ 'matches-css-before', PSelectorMatchesCSSBeforeTask ],
|
||||
[ 'matches-media', PSelectorMatchesMediaTask ],
|
||||
[ 'matches-path', PSelectorMatchesPathTask ],
|
||||
[ 'min-text-length', PSelectorMinTextLengthTask ],
|
||||
[ 'not', PSelectorIfNotTask ],
|
||||
[ 'others', PSelectorOthersTask ],
|
||||
[ 'spath', PSelectorSpathTask ],
|
||||
[ 'upward', PSelectorUpwardTask ],
|
||||
[ 'watch-attr', PSelectorWatchAttrs ],
|
||||
[ 'xpath', PSelectorXpathTask ],
|
||||
]);
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
uBOL_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.uBOL_commitNow();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const proceduralImports = self.proceduralImports || [];
|
||||
|
||||
const lookupSelectors = (hn, out) => {
|
||||
for ( const { argsList, hostnamesMap } of proceduralImports ) {
|
||||
let argsIndices = hostnamesMap.get(hn);
|
||||
if ( argsIndices === undefined ) { continue; }
|
||||
if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
|
||||
for ( const argsIndex of argsIndices ) {
|
||||
const details = argsList[argsIndex];
|
||||
if ( details.n && details.n.includes(hn) ) { continue; }
|
||||
out.push(...details.a.map(json => JSON.parse(json)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let hn;
|
||||
try { hn = document.location.hostname; } catch(ex) { }
|
||||
const selectors = [];
|
||||
while ( hn ) {
|
||||
lookupSelectors(hn, selectors);
|
||||
if ( hn === '*' ) { break; }
|
||||
const pos = hn.indexOf('.');
|
||||
if ( pos !== -1 ) {
|
||||
hn = hn.slice(pos + 1);
|
||||
} else {
|
||||
hn = '*';
|
||||
}
|
||||
}
|
||||
|
||||
proceduralImports.length = 0;
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
if ( selectors.length === 0 ) { return; }
|
||||
|
||||
proceduralFilterer = new ProceduralFilterer(selectors);
|
||||
|
||||
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;
|
||||
break;
|
||||
}
|
||||
if ( domChanged === false ) {
|
||||
for ( const removed of mutation.removedNodes ) {
|
||||
if ( removed.nodeType !== 1 ) { continue; }
|
||||
domChanged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( domChanged === false ) { return; }
|
||||
proceduralFilterer.onDOMChanged();
|
||||
});
|
||||
|
||||
observer.observe(document, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
})();
|
||||
|
||||
/******************************************************************************/
|
@ -42,22 +42,52 @@ const toBroaderHostname = hn => {
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// Is a descendant hostname of b?
|
||||
// Is hna descendant hostname of hnb?
|
||||
|
||||
const isDescendantHostname = (a, b) => {
|
||||
if ( b === 'all-urls' ) { return true; }
|
||||
if ( a.endsWith(b) === false ) { return false; }
|
||||
if ( a === b ) { return false; }
|
||||
return a.charCodeAt(a.length - b.length - 1) === 0x2E /* '.' */;
|
||||
const isDescendantHostname = (hna, hnb) => {
|
||||
if ( hnb === 'all-urls' ) { return true; }
|
||||
if ( hna.endsWith(hnb) === false ) { return false; }
|
||||
if ( hna === hnb ) { return false; }
|
||||
return hna.charCodeAt(hna.length - hnb.length - 1) === 0x2E /* '.' */;
|
||||
};
|
||||
|
||||
const isDescendantHostnameOfIter = (a, iter) => {
|
||||
for ( const b of iter ) {
|
||||
if ( isDescendantHostname(a, b) ) { return true; }
|
||||
const isDescendantHostnameOfIter = (hna, iterb) => {
|
||||
const setb = iterb instanceof Set ? iterb : new Set(iterb);
|
||||
if ( setb.has('all-urls') || setb.has('*') ) { return true; }
|
||||
let hn = hna;
|
||||
while ( hn ) {
|
||||
const pos = hn.indexOf('.');
|
||||
if ( pos === -1 ) { break; }
|
||||
hn = hn.slice(pos + 1);
|
||||
if ( setb.has(hn) ) { return true; }
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const intersectHostnameIters = (itera, iterb) => {
|
||||
const setb = iterb instanceof Set ? iterb : new Set(iterb);
|
||||
if ( setb.has('all-urls') || setb.has('*') ) { return Array.from(itera); }
|
||||
const out = [];
|
||||
for ( const hna of itera ) {
|
||||
if ( setb.has(hna) || isDescendantHostnameOfIter(hna, setb) ) {
|
||||
out.push(hna);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
const subtractHostnameIters = (itera, iterb) => {
|
||||
const setb = iterb instanceof Set ? iterb : new Set(iterb);
|
||||
if ( setb.has('all-urls') || setb.has('*') ) { return []; }
|
||||
const out = [];
|
||||
for ( const hna of itera ) {
|
||||
if ( setb.has(hna) ) { continue; }
|
||||
if ( isDescendantHostnameOfIter(hna, setb) ) { continue; }
|
||||
out.push(hna);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const matchesFromHostnames = hostnames => {
|
||||
@ -102,6 +132,8 @@ export {
|
||||
toBroaderHostname,
|
||||
isDescendantHostname,
|
||||
isDescendantHostnameOfIter,
|
||||
intersectHostnameIters,
|
||||
subtractHostnameIters,
|
||||
matchesFromHostnames,
|
||||
hostnamesFromMatches,
|
||||
fnameFromFileId,
|
||||
|
@ -54,7 +54,7 @@ const commandLineArgs = (( ) => {
|
||||
const outputDir = commandLineArgs.get('output') || '.';
|
||||
const cacheDir = `${outputDir}/../mv3-data`;
|
||||
const rulesetDir = `${outputDir}/rulesets`;
|
||||
const scriptletDir = `${rulesetDir}/js`;
|
||||
const scriptletDir = `${rulesetDir}/scripting`;
|
||||
const env = [
|
||||
'chromium',
|
||||
'mv3',
|
||||
@ -148,7 +148,11 @@ const writeOps = [];
|
||||
|
||||
const ruleResources = [];
|
||||
const rulesetDetails = [];
|
||||
const scriptingDetails = new Map();
|
||||
const declarativeDetails = new Map();
|
||||
const proceduralDetails = new Map();
|
||||
const scriptletStats = new Map();
|
||||
const specificDetails = new Map();
|
||||
const genericDetails = new Map();
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
@ -284,20 +288,20 @@ async function processNetworkFilters(assetDetails, network) {
|
||||
log(bad.map(rule => rule._error.map(v => `\t\t${v}`)).join('\n'), true);
|
||||
|
||||
writeFile(
|
||||
`${rulesetDir}/${assetDetails.id}.json`,
|
||||
`${rulesetDir}/main/${assetDetails.id}.json`,
|
||||
`${JSON.stringify(plainGood, replacer)}\n`
|
||||
);
|
||||
|
||||
if ( regexes.length !== 0 ) {
|
||||
writeFile(
|
||||
`${rulesetDir}/${assetDetails.id}.regexes.json`,
|
||||
`${rulesetDir}/regex/${assetDetails.id}.regexes.json`,
|
||||
`${JSON.stringify(regexes, replacer)}\n`
|
||||
);
|
||||
}
|
||||
|
||||
if ( removeparamsGood.length !== 0 ) {
|
||||
writeFile(
|
||||
`${rulesetDir}/${assetDetails.id}.removeparams.json`,
|
||||
`${rulesetDir}/removeparam/${assetDetails.id}.removeparams.json`,
|
||||
`${JSON.stringify(removeparamsGood, replacer)}\n`
|
||||
);
|
||||
}
|
||||
@ -366,10 +370,10 @@ const globalPatchedScriptletsSet = new Set();
|
||||
function addScriptingAPIResources(id, hostnames, fid) {
|
||||
if ( hostnames === undefined ) { return; }
|
||||
for ( const hn of hostnames ) {
|
||||
let hostnamesToFidMap = scriptingDetails.get(id);
|
||||
let hostnamesToFidMap = specificDetails.get(id);
|
||||
if ( hostnamesToFidMap === undefined ) {
|
||||
hostnamesToFidMap = new Map();
|
||||
scriptingDetails.set(id, hostnamesToFidMap);
|
||||
specificDetails.set(id, hostnamesToFidMap);
|
||||
}
|
||||
let fids = hostnamesToFidMap.get(hn);
|
||||
if ( fids === undefined ) {
|
||||
@ -383,11 +387,9 @@ function addScriptingAPIResources(id, hostnames, fid) {
|
||||
}
|
||||
}
|
||||
|
||||
const toIsolatedStartFileId = s => (uidint32(s) & ~0b11) | 0b00;
|
||||
const toMainStartFileId = s => (uidint32(s) & ~0b11) | 0b01;
|
||||
const toIsolatedEndFileId = s => (uidint32(s) & ~0b11) | 0b10;
|
||||
const toCSSSpecific = s => (uidint32(s) & ~0b11) | 0b00;
|
||||
|
||||
const pathFromFileName = fname => `${scriptletDir}/${fname.slice(0,2)}/${fname.slice(2)}.js`;
|
||||
const pathFromFileName = fname => `${fname.slice(-1)}/${fname.slice(0,-1)}.js`;
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
@ -411,18 +413,17 @@ async function processGenericCosmeticFilters(assetDetails, bucketsMap, exclusion
|
||||
'$rulesetId$',
|
||||
assetDetails.id
|
||||
).replace(
|
||||
/\bself\.\$excludeHostnameSet\$/m,
|
||||
`${JSON.stringify(exclusions, scriptletJsonReplacer)}`
|
||||
).replace(
|
||||
/\bself\.\$genericSelectorLists\$/m,
|
||||
/\bself\.\$genericSelectorMap\$/m,
|
||||
`${JSON.stringify(selectorLists, scriptletJsonReplacer)}`
|
||||
);
|
||||
|
||||
writeFile(
|
||||
`${scriptletDir}/${assetDetails.id}.generic.js`,
|
||||
`${scriptletDir}/generic/${assetDetails.id}.generic.js`,
|
||||
patchedScriptlet
|
||||
);
|
||||
|
||||
genericDetails.set(assetDetails.id, exclusions.sort());
|
||||
|
||||
log(`CSS-generic: ${count} plain CSS selectors`);
|
||||
|
||||
return out;
|
||||
@ -434,10 +435,11 @@ const MAX_COSMETIC_FILTERS_PER_FILE = 256;
|
||||
|
||||
// This merges selectors which are used by the same hostnames
|
||||
|
||||
function groupCosmeticByHostnames(mapin) {
|
||||
function groupSelectorsByHostnames(mapin) {
|
||||
if ( mapin === undefined ) { return []; }
|
||||
const merged = new Map();
|
||||
for ( const [ selector, details ] of mapin ) {
|
||||
if ( details.rejected ) { continue; }
|
||||
const json = JSON.stringify(details);
|
||||
let entries = merged.get(json);
|
||||
if ( entries === undefined ) {
|
||||
@ -460,7 +462,7 @@ function groupCosmeticByHostnames(mapin) {
|
||||
// Also, we sort the hostnames to increase likelihood that selector with
|
||||
// same hostnames will end up in same generated scriptlet.
|
||||
|
||||
function groupCosmeticBySelectors(arrayin) {
|
||||
function groupHostnamesBySelectors(arrayin) {
|
||||
const contentMap = new Map();
|
||||
for ( const entry of arrayin ) {
|
||||
const id = uidint32(JSON.stringify(entry.selectors));
|
||||
@ -527,11 +529,32 @@ const scriptletJsonReplacer = (k, v) => {
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function argsMap2List(argsMap, hostnamesMap) {
|
||||
const argsList = [];
|
||||
const indexMap = new Map();
|
||||
for ( const [ id, details ] of argsMap ) {
|
||||
indexMap.set(id, argsList.length);
|
||||
argsList.push(details);
|
||||
}
|
||||
for ( const [ hn, ids ] of hostnamesMap ) {
|
||||
if ( typeof ids === 'number' ) {
|
||||
hostnamesMap.set(hn, indexMap.get(ids));
|
||||
continue;
|
||||
}
|
||||
for ( let i = 0; i < ids.length; i++ ) {
|
||||
ids[i] = indexMap.get(ids[i]);
|
||||
}
|
||||
}
|
||||
return argsList;
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
async function processCosmeticFilters(assetDetails, mapin) {
|
||||
if ( mapin === undefined ) { return 0; }
|
||||
|
||||
const contentArray = groupCosmeticBySelectors(
|
||||
groupCosmeticByHostnames(mapin)
|
||||
const contentArray = groupHostnamesBySelectors(
|
||||
groupSelectorsByHostnames(mapin)
|
||||
);
|
||||
|
||||
// We do not want more than n CSS files per subscription, so we will
|
||||
@ -559,22 +582,23 @@ async function processCosmeticFilters(assetDetails, mapin) {
|
||||
if ( details.y === undefined ) { continue; }
|
||||
scriptletHostnameToIdMap(details.y, id, hostnamesMap);
|
||||
}
|
||||
const argsList = argsMap2List(argsMap, hostnamesMap);
|
||||
const patchedScriptlet = originalScriptletMap.get('css-specific')
|
||||
.replace(
|
||||
'$rulesetId$',
|
||||
assetDetails.id
|
||||
).replace(
|
||||
/\bself\.\$argsMap\$/m,
|
||||
`${JSON.stringify(argsMap, scriptletJsonReplacer)}`
|
||||
/\bself\.\$argsList\$/m,
|
||||
`${JSON.stringify(argsList, scriptletJsonReplacer)}`
|
||||
).replace(
|
||||
/\bself\.\$hostnamesMap\$/m,
|
||||
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
|
||||
);
|
||||
const fid = toIsolatedStartFileId(patchedScriptlet);
|
||||
const fid = toCSSSpecific(patchedScriptlet);
|
||||
if ( globalPatchedScriptletsSet.has(fid) === false ) {
|
||||
globalPatchedScriptletsSet.add(fid);
|
||||
const fname = fnameFromFileId(fid);
|
||||
writeFile(pathFromFileName(fname), patchedScriptlet);
|
||||
writeFile(`${scriptletDir}/specific/${pathFromFileName(fname)}`, patchedScriptlet);
|
||||
generatedFiles.push(fname);
|
||||
}
|
||||
for ( const entry of slice ) {
|
||||
@ -593,65 +617,137 @@ async function processCosmeticFilters(assetDetails, mapin) {
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
async function processProceduralCosmeticFilters(assetDetails, mapin) {
|
||||
async function processDeclarativeCosmeticFilters(assetDetails, mapin) {
|
||||
if ( mapin === undefined ) { return 0; }
|
||||
if ( mapin.size === 0 ) { return 0; }
|
||||
|
||||
const contentArray = groupCosmeticBySelectors(
|
||||
groupCosmeticByHostnames(mapin)
|
||||
// Distinguish declarative-compiled-as-procedural from actual procedural.
|
||||
const declaratives = new Map();
|
||||
mapin.forEach((details, jsonSelector) => {
|
||||
const selector = JSON.parse(jsonSelector);
|
||||
if ( selector.cssable !== true ) { return; }
|
||||
declaratives.set(jsonSelector, details);
|
||||
});
|
||||
if ( declaratives.size === 0 ) { return 0; }
|
||||
|
||||
const contentArray = groupHostnamesBySelectors(
|
||||
groupSelectorsByHostnames(declaratives)
|
||||
);
|
||||
|
||||
// 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 += MAX_COSMETIC_FILTERS_PER_FILE ) {
|
||||
const slice = contentArray.slice(i, i + MAX_COSMETIC_FILTERS_PER_FILE);
|
||||
const argsMap = slice.map(entry => [
|
||||
const argsMap = contentArray.map(entry => [
|
||||
entry[0],
|
||||
{
|
||||
a: entry[1].a ? entry[1].a.map(v => JSON.parse(v)) : undefined,
|
||||
n: entry[1].n
|
||||
a: entry[1].a,
|
||||
n: entry[1].n,
|
||||
}
|
||||
]);
|
||||
const hostnamesMap = new Map();
|
||||
for ( const [ id, details ] of slice ) {
|
||||
for ( const [ id, details ] of contentArray ) {
|
||||
if ( details.y === undefined ) { continue; }
|
||||
scriptletHostnameToIdMap(details.y, id, hostnamesMap);
|
||||
}
|
||||
const patchedScriptlet = originalScriptletMap.get('css-specific-procedural')
|
||||
|
||||
const argsList = argsMap2List(argsMap, hostnamesMap);
|
||||
const originalScriptletMap = await loadAllSourceScriptlets();
|
||||
const patchedScriptlet = originalScriptletMap.get('css-declarative')
|
||||
.replace(
|
||||
'$rulesetId$',
|
||||
assetDetails.id
|
||||
).replace(
|
||||
/\bself\.\$argsMap\$/m,
|
||||
`${JSON.stringify(argsMap, scriptletJsonReplacer)}`
|
||||
/\bself\.\$argsList\$/m,
|
||||
`${JSON.stringify(argsList, scriptletJsonReplacer)}`
|
||||
).replace(
|
||||
/\bself\.\$hostnamesMap\$/m,
|
||||
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
|
||||
);
|
||||
const fid = toIsolatedEndFileId(patchedScriptlet);
|
||||
if ( globalPatchedScriptletsSet.has(fid) === false ) {
|
||||
globalPatchedScriptletsSet.add(fid);
|
||||
const fname = fnameFromFileId(fid);
|
||||
writeFile(pathFromFileName(fname), patchedScriptlet);
|
||||
generatedFiles.push(fname);
|
||||
writeFile(`${scriptletDir}/declarative/${assetDetails.id}.declarative.js`, patchedScriptlet);
|
||||
|
||||
{
|
||||
const hostnames = new Set();
|
||||
for ( const entry of contentArray ) {
|
||||
if ( Array.isArray(entry[1].y) === false ) { continue; }
|
||||
for ( const hn of entry[1].y ) {
|
||||
hostnames.add(hn);
|
||||
}
|
||||
for ( const entry of slice ) {
|
||||
addScriptingAPIResources(assetDetails.id, entry[1].y, fid);
|
||||
}
|
||||
if ( hostnames.has('*') ) {
|
||||
hostnames.clear();
|
||||
hostnames.add('*');
|
||||
}
|
||||
declarativeDetails.set(assetDetails.id, Array.from(hostnames).sort());
|
||||
}
|
||||
|
||||
if ( generatedFiles.length !== 0 ) {
|
||||
if ( contentArray.length !== 0 ) {
|
||||
log(`Declarative-related distinct filters: ${contentArray.length} distinct combined selectors`);
|
||||
}
|
||||
|
||||
return contentArray.length;
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
async function processProceduralCosmeticFilters(assetDetails, mapin) {
|
||||
if ( mapin === undefined ) { return 0; }
|
||||
if ( mapin.size === 0 ) { return 0; }
|
||||
|
||||
// Distinguish declarative-compiled-as-procedural from actual procedural.
|
||||
const procedurals = new Map();
|
||||
mapin.forEach((details, jsonSelector) => {
|
||||
const selector = JSON.parse(jsonSelector);
|
||||
if ( selector.cssable ) { return; }
|
||||
procedurals.set(jsonSelector, details);
|
||||
});
|
||||
if ( procedurals.size === 0 ) { return 0; }
|
||||
|
||||
const contentArray = groupHostnamesBySelectors(
|
||||
groupSelectorsByHostnames(procedurals)
|
||||
);
|
||||
|
||||
const argsMap = contentArray.map(entry => [
|
||||
entry[0],
|
||||
{
|
||||
a: entry[1].a,
|
||||
n: entry[1].n,
|
||||
}
|
||||
]);
|
||||
const hostnamesMap = new Map();
|
||||
for ( const [ id, details ] of contentArray ) {
|
||||
if ( details.y === undefined ) { continue; }
|
||||
scriptletHostnameToIdMap(details.y, id, hostnamesMap);
|
||||
}
|
||||
|
||||
const argsList = argsMap2List(argsMap, hostnamesMap);
|
||||
const originalScriptletMap = await loadAllSourceScriptlets();
|
||||
const patchedScriptlet = originalScriptletMap.get('css-procedural')
|
||||
.replace(
|
||||
'$rulesetId$',
|
||||
assetDetails.id
|
||||
).replace(
|
||||
/\bself\.\$argsList\$/m,
|
||||
`${JSON.stringify(argsList, scriptletJsonReplacer)}`
|
||||
).replace(
|
||||
/\bself\.\$hostnamesMap\$/m,
|
||||
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
|
||||
);
|
||||
writeFile(`${scriptletDir}/procedural/${assetDetails.id}.procedural.js`, patchedScriptlet);
|
||||
|
||||
{
|
||||
const hostnames = new Set();
|
||||
for ( const entry of contentArray ) {
|
||||
if ( Array.isArray(entry[1].y) === false ) { continue; }
|
||||
for ( const hn of entry[1].y ) {
|
||||
hostnames.add(hn);
|
||||
}
|
||||
}
|
||||
if ( hostnames.has('*') ) {
|
||||
hostnames.clear();
|
||||
hostnames.add('*');
|
||||
}
|
||||
proceduralDetails.set(assetDetails.id, Array.from(hostnames).sort());
|
||||
}
|
||||
|
||||
if ( contentArray.length !== 0 ) {
|
||||
log(`Procedural-related distinct filters: ${contentArray.length} distinct combined selectors`);
|
||||
log(`Procedural-related injectable files: ${generatedFiles.length}`);
|
||||
log(`\t${generatedFiles.join(', ')}`);
|
||||
}
|
||||
|
||||
return contentArray.length;
|
||||
@ -753,28 +849,34 @@ async function processScriptletFilters(assetDetails, mapin) {
|
||||
for ( const [ argsHash, details ] of argsDetails ) {
|
||||
scriptletHostnameToIdMap(details.y, uidint32(argsHash), hostnamesMap);
|
||||
}
|
||||
|
||||
const argsList = argsMap2List(argsMap, hostnamesMap);
|
||||
const patchedScriptlet = originalScriptletMap.get(token)
|
||||
.replace(
|
||||
'$rulesetId$',
|
||||
assetDetails.id
|
||||
).replace(
|
||||
/\bself\.\$argsMap\$/m,
|
||||
`${JSON.stringify(argsMap, scriptletJsonReplacer)}`
|
||||
/\bself\.\$argsList\$/m,
|
||||
`${JSON.stringify(argsList, scriptletJsonReplacer)}`
|
||||
).replace(
|
||||
/\bself\.\$hostnamesMap\$/m,
|
||||
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
|
||||
);
|
||||
// ends-with 1 = scriptlet resource
|
||||
const fid = toMainStartFileId(patchedScriptlet);
|
||||
if ( globalPatchedScriptletsSet.has(fid) === false ) {
|
||||
globalPatchedScriptletsSet.add(fid);
|
||||
const fname = fnameFromFileId(fid);
|
||||
writeFile(pathFromFileName(fname), patchedScriptlet);
|
||||
const fname = `${assetDetails.id}.${token}.js`;
|
||||
const fpath = `${scriptletDir}/scriptlet/${fname}`;
|
||||
writeFile(fpath, patchedScriptlet);
|
||||
generatedFiles.push(fname);
|
||||
|
||||
const hostnameMatches = new Set(hostnamesMap.keys());
|
||||
if ( hostnameMatches.has('*') ) {
|
||||
hostnameMatches.clear();
|
||||
hostnameMatches.add('*');
|
||||
}
|
||||
for ( const details of argsDetails.values() ) {
|
||||
addScriptingAPIResources(assetDetails.id, details.y, fid);
|
||||
let rulesetScriptlets = scriptletStats.get(assetDetails.id);
|
||||
if ( rulesetScriptlets === undefined ) {
|
||||
scriptletStats.set(assetDetails.id, rulesetScriptlets = []);
|
||||
}
|
||||
rulesetScriptlets.push([ token, Array.from(hostnameMatches).sort() ]);
|
||||
}
|
||||
|
||||
if ( generatedFiles.length !== 0 ) {
|
||||
@ -790,15 +892,18 @@ async function processScriptletFilters(assetDetails, mapin) {
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const rulesetFromURLS = async function(assetDetails) {
|
||||
async function rulesetFromURLs(assetDetails) {
|
||||
log('============================');
|
||||
log(`Listset for '${assetDetails.id}':`);
|
||||
|
||||
if ( assetDetails.text === undefined ) {
|
||||
const text = await fetchAsset(assetDetails);
|
||||
if ( text === '' ) { return; }
|
||||
assetDetails.text = text;
|
||||
}
|
||||
|
||||
const results = await dnrRulesetFromRawLists(
|
||||
[ { name: assetDetails.id, text } ],
|
||||
[ { name: assetDetails.id, text: assetDetails.text } ],
|
||||
{ env }
|
||||
);
|
||||
|
||||
@ -826,6 +931,11 @@ const rulesetFromURLS = async function(assetDetails) {
|
||||
proceduralCosmetic.set(JSON.stringify(parsed), details);
|
||||
}
|
||||
}
|
||||
if ( rejectedCosmetic.length !== 0 ) {
|
||||
log(`Rejected cosmetic filters: ${rejectedCosmetic.length}`);
|
||||
log(rejectedCosmetic.map(line => `\t${line}`).join('\n'), true);
|
||||
}
|
||||
|
||||
const genericCosmeticStats = await processGenericCosmeticFilters(
|
||||
assetDetails,
|
||||
results.genericCosmetic,
|
||||
@ -835,15 +945,14 @@ const rulesetFromURLS = async function(assetDetails) {
|
||||
assetDetails,
|
||||
declarativeCosmetic
|
||||
);
|
||||
const declarativeStats = await processDeclarativeCosmeticFilters(
|
||||
assetDetails,
|
||||
proceduralCosmetic
|
||||
);
|
||||
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,
|
||||
results.scriptlet
|
||||
@ -871,6 +980,7 @@ const rulesetFromURLS = async function(assetDetails) {
|
||||
css: {
|
||||
generic: genericCosmeticStats,
|
||||
specific: specificCosmeticStats,
|
||||
declarative: declarativeStats,
|
||||
procedural: proceduralStats,
|
||||
},
|
||||
scriptlets: {
|
||||
@ -881,9 +991,9 @@ const rulesetFromURLS = async function(assetDetails) {
|
||||
ruleResources.push({
|
||||
id: assetDetails.id,
|
||||
enabled: assetDetails.enabled,
|
||||
path: `/rulesets/${assetDetails.id}.json`
|
||||
path: `/rulesets/main/${assetDetails.id}.json`
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
@ -925,12 +1035,12 @@ async function main() {
|
||||
'https://ublockorigin.pages.dev/filters/resource-abuse.txt',
|
||||
'https://ublockorigin.pages.dev/filters/unbreak.txt',
|
||||
'https://ublockorigin.pages.dev/filters/quick-fixes.txt',
|
||||
'https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/ubol-filters.txt',
|
||||
'https://ublockorigin.pages.dev/filters/ubol-filters.txt',
|
||||
'https://secure.fanboy.co.nz/easylist.txt',
|
||||
'https://secure.fanboy.co.nz/easyprivacy.txt',
|
||||
'https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&showintro=1&mimetype=plaintext',
|
||||
];
|
||||
await rulesetFromURLS({
|
||||
await rulesetFromURLs({
|
||||
id: 'default',
|
||||
name: 'Ads, trackers, miners, and more' ,
|
||||
enabled: true,
|
||||
@ -967,7 +1077,7 @@ async function main() {
|
||||
}
|
||||
const id = ids[0];
|
||||
const asset = assets[id];
|
||||
await rulesetFromURLS({
|
||||
await rulesetFromURLs({
|
||||
id: id.toLowerCase(),
|
||||
lang: asset.lang,
|
||||
name: asset.title,
|
||||
@ -986,7 +1096,7 @@ async function main() {
|
||||
const contentURL = Array.isArray(asset.contentURL)
|
||||
? asset.contentURL[0]
|
||||
: asset.contentURL;
|
||||
await rulesetFromURLS({
|
||||
await rulesetFromURLs({
|
||||
id: id.toLowerCase(),
|
||||
name: asset.title,
|
||||
enabled: false,
|
||||
@ -996,14 +1106,14 @@ async function main() {
|
||||
}
|
||||
|
||||
// Handpicked rulesets from abroad
|
||||
await rulesetFromURLS({
|
||||
await rulesetFromURLs({
|
||||
id: 'cname-trackers',
|
||||
name: 'AdGuard CNAME-cloaked trackers',
|
||||
enabled: true,
|
||||
urls: [ 'https://raw.githubusercontent.com/AdguardTeam/cname-trackers/master/combined_disguised_trackers.txt' ],
|
||||
homeURL: 'https://github.com/AdguardTeam/cname-trackers#cname-cloaked-trackers',
|
||||
});
|
||||
await rulesetFromURLS({
|
||||
await rulesetFromURLs({
|
||||
id: 'stevenblack-hosts',
|
||||
name: 'Steven Black\'s hosts file',
|
||||
enabled: false,
|
||||
@ -1018,15 +1128,35 @@ async function main() {
|
||||
|
||||
// We sort the hostnames for convenience/performance in the extension's
|
||||
// script manager -- the scripting API does a sort() internally.
|
||||
for ( const [ rulesetId, hostnamesToFidsMap ] of scriptingDetails ) {
|
||||
scriptingDetails.set(
|
||||
for ( const [ rulesetId, hostnamesToFidsMap ] of specificDetails ) {
|
||||
specificDetails.set(
|
||||
rulesetId,
|
||||
Array.from(hostnamesToFidsMap).sort()
|
||||
);
|
||||
}
|
||||
writeFile(
|
||||
`${rulesetDir}/scripting-details.json`,
|
||||
`${JSON.stringify(scriptingDetails, jsonSetMapReplacer)}\n`
|
||||
`${rulesetDir}/specific-details.json`,
|
||||
`${JSON.stringify(specificDetails, jsonSetMapReplacer)}\n`
|
||||
);
|
||||
|
||||
writeFile(
|
||||
`${rulesetDir}/declarative-details.json`,
|
||||
`${JSON.stringify(declarativeDetails, jsonSetMapReplacer, 1)}\n`
|
||||
);
|
||||
|
||||
writeFile(
|
||||
`${rulesetDir}/procedural-details.json`,
|
||||
`${JSON.stringify(proceduralDetails, jsonSetMapReplacer, 1)}\n`
|
||||
);
|
||||
|
||||
writeFile(
|
||||
`${rulesetDir}/scriptlet-details.json`,
|
||||
`${JSON.stringify(scriptletStats, jsonSetMapReplacer, 1)}\n`
|
||||
);
|
||||
|
||||
writeFile(
|
||||
`${rulesetDir}/generic-details.json`,
|
||||
`${JSON.stringify(genericDetails, jsonSetMapReplacer, 1)}\n`
|
||||
);
|
||||
|
||||
await Promise.all(writeOps);
|
||||
|
@ -43,7 +43,7 @@
|
||||
|
||||
// $rulesetId$
|
||||
|
||||
const argsMap = new Map(self.$argsMap$);
|
||||
const argsList = self.$argsList$;
|
||||
|
||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||
|
||||
@ -157,10 +157,10 @@ let hn;
|
||||
try { hn = document.location.hostname; } catch(ex) { }
|
||||
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);
|
||||
let argsIndices = hostnamesMap.get(hn);
|
||||
if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
|
||||
for ( const argsIndex of argsIndices ) {
|
||||
const details = argsList[argsIndex];
|
||||
if ( details.n && details.n.includes(hn) ) { continue; }
|
||||
try { scriptlet(...details.a); } catch(ex) {}
|
||||
}
|
||||
@ -174,9 +174,7 @@ while ( hn ) {
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
argsMap.clear();
|
||||
argsList.length = 0;
|
||||
hostnamesMap.clear();
|
||||
|
||||
/******************************************************************************/
|
||||
|
@ -41,7 +41,7 @@
|
||||
|
||||
// $rulesetId$
|
||||
|
||||
const argsMap = new Map(self.$argsMap$);
|
||||
const argsList = self.$argsList$;
|
||||
|
||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||
|
||||
@ -115,10 +115,10 @@ let hn;
|
||||
try { hn = document.location.hostname; } catch(ex) { }
|
||||
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);
|
||||
let argsIndices = hostnamesMap.get(hn);
|
||||
if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
|
||||
for ( const argsIndex of argsIndices ) {
|
||||
const details = argsList[argsIndex];
|
||||
if ( details.n && details.n.includes(hn) ) { continue; }
|
||||
try { scriptlet(...details.a); } catch(ex) {}
|
||||
}
|
||||
@ -132,9 +132,7 @@ while ( hn ) {
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
argsMap.clear();
|
||||
argsList.length = 0;
|
||||
hostnamesMap.clear();
|
||||
|
||||
/******************************************************************************/
|
||||
|
@ -41,7 +41,7 @@
|
||||
|
||||
// $rulesetId$
|
||||
|
||||
const argsMap = new Map(self.$argsMap$);
|
||||
const argsList = self.$argsList$;
|
||||
|
||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||
|
||||
@ -89,10 +89,10 @@ let hn;
|
||||
try { hn = document.location.hostname; } catch(ex) { }
|
||||
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);
|
||||
let argsIndices = hostnamesMap.get(hn);
|
||||
if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
|
||||
for ( const argsIndex of argsIndices ) {
|
||||
const details = argsList[argsIndex];
|
||||
if ( details.n && details.n.includes(hn) ) { continue; }
|
||||
try { scriptlet(...details.a); } catch(ex) {}
|
||||
}
|
||||
@ -106,9 +106,7 @@ while ( hn ) {
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
argsMap.clear();
|
||||
argsList.length = 0;
|
||||
hostnamesMap.clear();
|
||||
|
||||
/******************************************************************************/
|
||||
|
51
platform/mv3/scriptlets/css-declarative.js
Normal file
51
platform/mv3/scriptlets/css-declarative.js
Normal file
@ -0,0 +1,51 @@
|
||||
/*******************************************************************************
|
||||
|
||||
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-declarative
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// Important!
|
||||
// Isolate from global scope
|
||||
(function uBOL_cssDeclarativeImport() {
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// $rulesetId$
|
||||
|
||||
const argsList = self.$argsList$;
|
||||
|
||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||
|
||||
self.declarativeImports = self.declarativeImports || [];
|
||||
self.declarativeImports.push({ argsList, hostnamesMap });
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
})();
|
||||
|
||||
/******************************************************************************/
|
@ -31,258 +31,32 @@
|
||||
|
||||
// Important!
|
||||
// Isolate from global scope
|
||||
(function uBOL_cssGeneric() {
|
||||
(function uBOL_cssGenericImport() {
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// $rulesetId$
|
||||
|
||||
{
|
||||
const excludeHostnameSet = new Set(self.$excludeHostnameSet$);
|
||||
const toImport = self.$genericSelectorMap$;
|
||||
|
||||
let hn;
|
||||
try { hn = document.location.hostname; } catch(ex) { }
|
||||
while ( hn ) {
|
||||
if ( excludeHostnameSet.has(hn) ) { return; }
|
||||
const pos = hn.indexOf('.');
|
||||
if ( pos === -1 ) { break; }
|
||||
hn = hn.slice(pos+1);
|
||||
}
|
||||
excludeHostnameSet.clear();
|
||||
const genericSelectorMap = self.genericSelectorMap || new Map();
|
||||
|
||||
if ( genericSelectorMap.size === 0 ) {
|
||||
self.genericSelectorMap = new Map(toImport);
|
||||
return;
|
||||
}
|
||||
|
||||
const genericSelectorLists = new Map(self.$genericSelectorLists$);
|
||||
for ( const toImportEntry of toImport ) {
|
||||
const existing = genericSelectorMap.get(toImportEntry[0]);
|
||||
genericSelectorMap.set(
|
||||
toImportEntry[0],
|
||||
existing === undefined
|
||||
? toImportEntry[1]
|
||||
: `${existing},${toImportEntry[1]}`
|
||||
);
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const queriedHashes = new Set();
|
||||
const maxSurveyTimeSlice = 4;
|
||||
const styleSheetSelectors = [];
|
||||
const stopAllRatio = 0.95; // To be investigated
|
||||
|
||||
let surveyCount = 0;
|
||||
let surveyMissCount = 0;
|
||||
let styleSheetTimer;
|
||||
let processTimer;
|
||||
let domChangeTimer;
|
||||
let lastDomChange = Date.now();
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// https://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
|
||||
const hashFromStr = (type, s) => {
|
||||
const len = s.length;
|
||||
const step = len + 7 >>> 3;
|
||||
let hash = type;
|
||||
for ( let i = 0; i < len; i += step ) {
|
||||
hash = (hash << 5) - hash + s.charCodeAt(i) | 0;
|
||||
}
|
||||
return hash & 0x00FFFFFF;
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// Extract all classes/ids: these will be passed to the cosmetic
|
||||
// filtering engine, and in return we will obtain only the relevant
|
||||
// CSS selectors.
|
||||
|
||||
// https://github.com/gorhill/uBlock/issues/672
|
||||
// http://www.w3.org/TR/2014/REC-html5-20141028/infrastructure.html#space-separated-tokens
|
||||
// http://jsperf.com/enumerate-classes/6
|
||||
|
||||
const uBOL_idFromNode = (node, out) => {
|
||||
const raw = node.id;
|
||||
if ( typeof raw !== 'string' || raw.length === 0 ) { return; }
|
||||
const s = raw.trim();
|
||||
const hash = hashFromStr(0x23 /* '#' */, s);
|
||||
if ( queriedHashes.has(hash) ) { return; }
|
||||
out.push(hash);
|
||||
queriedHashes.add(hash);
|
||||
};
|
||||
|
||||
// https://github.com/uBlockOrigin/uBlock-issues/discussions/2076
|
||||
// Performance: avoid using Element.classList
|
||||
const uBOL_classesFromNode = (node, out) => {
|
||||
const s = node.getAttribute('class');
|
||||
if ( typeof s !== 'string' ) { return; }
|
||||
const len = s.length;
|
||||
for ( let beg = 0, end = 0, token = ''; beg < len; beg += 1 ) {
|
||||
end = s.indexOf(' ', beg);
|
||||
if ( end === beg ) { continue; }
|
||||
if ( end === -1 ) { end = len; }
|
||||
token = s.slice(beg, end);
|
||||
beg = end;
|
||||
const hash = hashFromStr(0x2E /* '.' */, token);
|
||||
if ( queriedHashes.has(hash) ) { continue; }
|
||||
out.push(hash);
|
||||
queriedHashes.add(hash);
|
||||
}
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const pendingNodes = {
|
||||
nodeLists: [],
|
||||
buffer: [
|
||||
null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null,
|
||||
],
|
||||
j: 0,
|
||||
add(nodes) {
|
||||
if ( nodes.length === 0 ) { return; }
|
||||
this.nodeLists.push(nodes);
|
||||
},
|
||||
next() {
|
||||
if ( this.nodeLists.length === 0 ) { return 0; }
|
||||
const maxSurveyBuffer = this.buffer.length;
|
||||
const nodeLists = this.nodeLists;
|
||||
let ib = 0;
|
||||
do {
|
||||
const nodeList = nodeLists[0];
|
||||
let j = this.j;
|
||||
let n = j + maxSurveyBuffer - ib;
|
||||
if ( n > nodeList.length ) {
|
||||
n = nodeList.length;
|
||||
}
|
||||
for ( let i = j; i < n; i++ ) {
|
||||
this.buffer[ib++] = nodeList[j++];
|
||||
}
|
||||
if ( j !== nodeList.length ) {
|
||||
this.j = j;
|
||||
break;
|
||||
}
|
||||
this.j = 0;
|
||||
this.nodeLists.shift();
|
||||
} while ( ib < maxSurveyBuffer && nodeLists.length !== 0 );
|
||||
return ib;
|
||||
},
|
||||
hasNodes() {
|
||||
return this.nodeLists.length !== 0;
|
||||
},
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const uBOL_processNodes = ( ) => {
|
||||
const t0 = Date.now();
|
||||
const hashes = [];
|
||||
const nodes = pendingNodes.buffer;
|
||||
const deadline = t0 + maxSurveyTimeSlice;
|
||||
let processed = 0;
|
||||
for (;;) {
|
||||
const n = pendingNodes.next();
|
||||
if ( n === 0 ) { break; }
|
||||
for ( let i = 0; i < n; i++ ) {
|
||||
const node = nodes[i];
|
||||
nodes[i] = null;
|
||||
uBOL_idFromNode(node, hashes);
|
||||
uBOL_classesFromNode(node, hashes);
|
||||
}
|
||||
processed += n;
|
||||
if ( performance.now() >= deadline ) { break; }
|
||||
}
|
||||
for ( const hash of hashes ) {
|
||||
const selectorList = genericSelectorLists.get(hash);
|
||||
if ( selectorList === undefined ) { continue; }
|
||||
styleSheetSelectors.push(selectorList);
|
||||
genericSelectorLists.delete(hash);
|
||||
}
|
||||
surveyCount += 1;
|
||||
if ( styleSheetSelectors.length === 0 ) {
|
||||
surveyMissCount += 1;
|
||||
if (
|
||||
surveyCount >= 100 &&
|
||||
(surveyMissCount / surveyCount) >= stopAllRatio
|
||||
) {
|
||||
stopAll('too many misses in surveyor');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ( styleSheetTimer !== undefined ) { return; }
|
||||
styleSheetTimer = self.requestAnimationFrame(( ) => {
|
||||
styleSheetTimer = undefined;
|
||||
uBOL_injectStyleSheet();
|
||||
});
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const uBOL_processChanges = mutations => {
|
||||
for ( let i = 0; i < mutations.length; i++ ) {
|
||||
const mutation = mutations[i];
|
||||
for ( const added of mutation.addedNodes ) {
|
||||
if ( added.nodeType !== 1 ) { continue; }
|
||||
pendingNodes.add([ added ]);
|
||||
if ( added.firstElementChild === null ) { continue; }
|
||||
pendingNodes.add(added.querySelectorAll('[id],[class]'));
|
||||
}
|
||||
}
|
||||
if ( pendingNodes.hasNodes() === false ) { return; }
|
||||
lastDomChange = Date.now();
|
||||
if ( processTimer !== undefined ) { return; }
|
||||
processTimer = self.setTimeout(( ) => {
|
||||
processTimer = undefined;
|
||||
uBOL_processNodes();
|
||||
}, 64);
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const uBOL_injectStyleSheet = ( ) => {
|
||||
try {
|
||||
const sheet = new CSSStyleSheet();
|
||||
sheet.replace(`@layer{${styleSheetSelectors.join(',')}{display:none!important;}}`);
|
||||
document.adoptedStyleSheets = [
|
||||
...document.adoptedStyleSheets,
|
||||
sheet
|
||||
];
|
||||
} catch(ex) {
|
||||
}
|
||||
styleSheetSelectors.length = 0;
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
pendingNodes.add(document.querySelectorAll('[id],[class]'));
|
||||
uBOL_processNodes();
|
||||
|
||||
let domMutationObserver = new MutationObserver(uBOL_processChanges);
|
||||
domMutationObserver.observe(document, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
const needDomChangeObserver = ( ) => {
|
||||
domChangeTimer = undefined;
|
||||
if ( domMutationObserver === undefined ) { return; }
|
||||
if ( (Date.now() - lastDomChange) > 20000 ) {
|
||||
return stopAll('no more DOM changes');
|
||||
}
|
||||
domChangeTimer = self.setTimeout(needDomChangeObserver, 20000);
|
||||
};
|
||||
|
||||
needDomChangeObserver();
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const stopAll = reason => {
|
||||
if ( domChangeTimer !== undefined ) {
|
||||
self.clearTimeout(domChangeTimer);
|
||||
domChangeTimer = undefined;
|
||||
}
|
||||
domMutationObserver.disconnect();
|
||||
domMutationObserver.takeRecords();
|
||||
domMutationObserver = undefined;
|
||||
genericSelectorLists.clear();
|
||||
queriedHashes.clear();
|
||||
console.info(`uBOL: Generic cosmetic filtering stopped because ${reason}`);
|
||||
};
|
||||
self.genericSelectorMap = genericSelectorMap;
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
|
@ -25,659 +25,24 @@
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
/// name css-specific-procedural
|
||||
/// name css-procedural
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// Important!
|
||||
// Isolate from global scope
|
||||
(function uBOL_cssSpecificProcedural() {
|
||||
(function uBOL_cssProceduralImport() {
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
// $rulesetId$
|
||||
|
||||
const argsMap = new Map(self.$argsMap$);
|
||||
const argsList = self.$argsList$;
|
||||
|
||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
let proceduralFilterer;
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
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 PSelectorVoidTask extends PSelectorTask {
|
||||
constructor(task) {
|
||||
super();
|
||||
console.info(`uBO: :${task[0]}() operator does not exist`);
|
||||
}
|
||||
transpose() {
|
||||
}
|
||||
}
|
||||
|
||||
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 PSelectorMatchesCSSAfterTask extends PSelectorMatchesCSSTask {
|
||||
constructor(task) {
|
||||
super(task);
|
||||
this.pseudo = '::after';
|
||||
}
|
||||
}
|
||||
|
||||
class PSelectorMatchesCSSBeforeTask extends PSelectorMatchesCSSTask {
|
||||
constructor(task) {
|
||||
super(task);
|
||||
this.pseudo = '::before';
|
||||
}
|
||||
}
|
||||
|
||||
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 ( proceduralFilterer instanceof Object === false ) { return; }
|
||||
proceduralFilterer.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()}`;
|
||||
}
|
||||
}
|
||||
transpose(node, output) {
|
||||
const nodes = this.nth
|
||||
? PSelectorSpathTask.qsa(node, this.spath)
|
||||
: node.querySelectorAll(this.spath);
|
||||
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() {
|
||||
if ( proceduralFilterer instanceof Object ) {
|
||||
proceduralFilterer.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-css-after', PSelectorMatchesCSSAfterTask ],
|
||||
[ 'matches-css-before', PSelectorMatchesCSSBeforeTask ],
|
||||
[ '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]) || PSelectorVoidTask;
|
||||
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 ]; }
|
||||
if ( input !== document && /^ [>+~]/.test(this.selector) ) {
|
||||
return Array.from(PSelectorSpathTask.qsa(input, this.selector));
|
||||
}
|
||||
return Array.from(root.querySelectorAll(this.selector));
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
uBOL_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.uBOL_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'));
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
if ( proceduralSelectors.length !== 0 ) {
|
||||
proceduralFilterer = 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; }
|
||||
proceduralFilterer.onDOMChanged();
|
||||
});
|
||||
observer.observe(document, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
argsMap.clear();
|
||||
hostnamesMap.clear();
|
||||
self.proceduralImports = self.proceduralImports || [];
|
||||
self.proceduralImports.push({ argsList, hostnamesMap });
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
|
@ -37,7 +37,7 @@
|
||||
|
||||
// $rulesetId$
|
||||
|
||||
const argsMap = new Map(self.$argsMap$);
|
||||
const argsList = self.$argsList$;
|
||||
|
||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||
|
||||
@ -48,10 +48,10 @@ try { hn = document.location.hostname; } catch(ex) { }
|
||||
const styles = [];
|
||||
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);
|
||||
let argsIndices = hostnamesMap.get(hn);
|
||||
if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
|
||||
for ( const argsIndex of argsIndices ) {
|
||||
const details = argsList[argsIndex];
|
||||
if ( details.n && details.n.includes(hn) ) { continue; }
|
||||
styles.push(details.a);
|
||||
}
|
||||
@ -65,6 +65,9 @@ while ( hn ) {
|
||||
}
|
||||
}
|
||||
|
||||
argsList.length = 0;
|
||||
hostnamesMap.clear();
|
||||
|
||||
if ( styles.length === 0 ) { return; }
|
||||
|
||||
try {
|
||||
@ -79,11 +82,6 @@ try {
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
argsMap.clear();
|
||||
hostnamesMap.clear();
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
})();
|
||||
|
||||
/******************************************************************************/
|
||||
|
@ -40,7 +40,7 @@
|
||||
|
||||
// $rulesetId$
|
||||
|
||||
const argsMap = new Map(self.$argsMap$);
|
||||
const argsList = self.$argsList$;
|
||||
|
||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||
|
||||
@ -133,10 +133,10 @@ let hn;
|
||||
try { hn = document.location.hostname; } catch(ex) { }
|
||||
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);
|
||||
let argsIndices = hostnamesMap.get(hn);
|
||||
if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
|
||||
for ( const argsIndex of argsIndices ) {
|
||||
const details = argsList[argsIndex];
|
||||
if ( details.n && details.n.includes(hn) ) { continue; }
|
||||
try { scriptlet(...details.a); } catch(ex) {}
|
||||
}
|
||||
@ -150,9 +150,7 @@ while ( hn ) {
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
argsMap.clear();
|
||||
argsList.length = 0;
|
||||
hostnamesMap.clear();
|
||||
|
||||
/******************************************************************************/
|
||||
|
@ -28,7 +28,7 @@
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
/// name no-addEventListener-if
|
||||
/// name no-addeventlistener-if
|
||||
/// alias noaelif
|
||||
/// alias aeld
|
||||
|
||||
@ -42,7 +42,7 @@
|
||||
|
||||
// $rulesetId$
|
||||
|
||||
const argsMap = new Map(self.$argsMap$);
|
||||
const argsList = self.$argsList$;
|
||||
|
||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||
|
||||
@ -89,10 +89,10 @@ let hn;
|
||||
try { hn = document.location.hostname; } catch(ex) { }
|
||||
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);
|
||||
let argsIndices = hostnamesMap.get(hn);
|
||||
if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
|
||||
for ( const argsIndex of argsIndices ) {
|
||||
const details = argsList[argsIndex];
|
||||
if ( details.n && details.n.includes(hn) ) { continue; }
|
||||
try { scriptlet(...details.a); } catch(ex) {}
|
||||
}
|
||||
@ -106,9 +106,7 @@ while ( hn ) {
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
argsMap.clear();
|
||||
argsList.length = 0;
|
||||
hostnamesMap.clear();
|
||||
|
||||
/******************************************************************************/
|
||||
|
@ -28,7 +28,8 @@
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
/// name no-setInterval-if
|
||||
/// name no-setinterval-if
|
||||
/// alias no-setInterval-if
|
||||
/// alias nosiif
|
||||
|
||||
/******************************************************************************/
|
||||
@ -41,7 +42,7 @@
|
||||
|
||||
// $rulesetId$
|
||||
|
||||
const argsMap = new Map(self.$argsMap$);
|
||||
const argsList = self.$argsList$;
|
||||
|
||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||
|
||||
@ -92,10 +93,10 @@ let hn;
|
||||
try { hn = document.location.hostname; } catch(ex) { }
|
||||
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);
|
||||
let argsIndices = hostnamesMap.get(hn);
|
||||
if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
|
||||
for ( const argsIndex of argsIndices ) {
|
||||
const details = argsList[argsIndex];
|
||||
if ( details.n && details.n.includes(hn) ) { continue; }
|
||||
try { scriptlet(...details.a); } catch(ex) {}
|
||||
}
|
||||
@ -109,9 +110,7 @@ while ( hn ) {
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
argsMap.clear();
|
||||
argsList.length = 0;
|
||||
hostnamesMap.clear();
|
||||
|
||||
/******************************************************************************/
|
||||
|
@ -28,7 +28,8 @@
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
/// name no-setTimeout-if
|
||||
/// name no-settimeout-if
|
||||
/// alias no-setTimeout-if
|
||||
/// alias nostif
|
||||
|
||||
/******************************************************************************/
|
||||
@ -41,7 +42,7 @@
|
||||
|
||||
// $rulesetId$
|
||||
|
||||
const argsMap = new Map(self.$argsMap$);
|
||||
const argsList = self.$argsList$;
|
||||
|
||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||
|
||||
@ -92,10 +93,10 @@ let hn;
|
||||
try { hn = document.location.hostname; } catch(ex) { }
|
||||
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);
|
||||
let argsIndices = hostnamesMap.get(hn);
|
||||
if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
|
||||
for ( const argsIndex of argsIndices ) {
|
||||
const details = argsList[argsIndex];
|
||||
if ( details.n && details.n.includes(hn) ) { continue; }
|
||||
try { scriptlet(...details.a); } catch(ex) {}
|
||||
}
|
||||
@ -109,9 +110,7 @@ while ( hn ) {
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
argsMap.clear();
|
||||
argsList.length = 0;
|
||||
hostnamesMap.clear();
|
||||
|
||||
/******************************************************************************/
|
||||
|
@ -29,6 +29,7 @@
|
||||
/******************************************************************************/
|
||||
|
||||
/// name no-windowOpen-if
|
||||
/// alias no-windowopen-if
|
||||
/// alias nowoif
|
||||
|
||||
/******************************************************************************/
|
||||
@ -41,7 +42,7 @@
|
||||
|
||||
// $rulesetId$
|
||||
|
||||
const argsMap = new Map(self.$argsMap$);
|
||||
const argsList = self.$argsList$;
|
||||
|
||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||
|
||||
@ -129,10 +130,10 @@ let hn;
|
||||
try { hn = document.location.hostname; } catch(ex) { }
|
||||
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);
|
||||
let argsIndices = hostnamesMap.get(hn);
|
||||
if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
|
||||
for ( const argsIndex of argsIndices ) {
|
||||
const details = argsList[argsIndex];
|
||||
if ( details.n && details.n.includes(hn) ) { continue; }
|
||||
try { scriptlet(...details.a); } catch(ex) {}
|
||||
}
|
||||
@ -146,9 +147,7 @@ while ( hn ) {
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
argsMap.clear();
|
||||
argsList.length = 0;
|
||||
hostnamesMap.clear();
|
||||
|
||||
/******************************************************************************/
|
||||
|
@ -41,7 +41,7 @@
|
||||
|
||||
// $rulesetId$
|
||||
|
||||
const argsMap = new Map(self.$argsMap$);
|
||||
const argsList = self.$argsList$;
|
||||
|
||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||
|
||||
@ -175,10 +175,10 @@ let hn;
|
||||
try { hn = document.location.hostname; } catch(ex) { }
|
||||
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);
|
||||
let argsIndices = hostnamesMap.get(hn);
|
||||
if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
|
||||
for ( const argsIndex of argsIndices ) {
|
||||
const details = argsList[argsIndex];
|
||||
if ( details.n && details.n.includes(hn) ) { continue; }
|
||||
try { scriptlet(...details.a); } catch(ex) {}
|
||||
}
|
||||
@ -192,9 +192,7 @@ while ( hn ) {
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
argsMap.clear();
|
||||
argsList.length = 0;
|
||||
hostnamesMap.clear();
|
||||
|
||||
/******************************************************************************/
|
||||
|
@ -36,7 +36,7 @@ cp LICENSE.txt $DES/
|
||||
echo "*** uBOLite.mv3: Copying mv3-specific files"
|
||||
cp platform/mv3/extension/*.html $DES/
|
||||
cp platform/mv3/extension/css/* $DES/css/
|
||||
cp platform/mv3/extension/js/* $DES/js/
|
||||
cp -R platform/mv3/extension/js/* $DES/js/
|
||||
cp platform/mv3/extension/img/* $DES/img/
|
||||
cp -R platform/mv3/extension/_locales $DES/
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user