mirror of
https://github.com/gorhill/uBlock.git
synced 2024-11-10 09:07:54 +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
22 changed files with 1832 additions and 1231 deletions
|
@ -42,7 +42,6 @@ import {
|
||||||
} from './ruleset-manager.js';
|
} from './ruleset-manager.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getInjectableCount,
|
|
||||||
registerInjectables,
|
registerInjectables,
|
||||||
} from './scripting-manager.js';
|
} from './scripting-manager.js';
|
||||||
|
|
||||||
|
@ -63,6 +62,8 @@ const rulesetConfig = {
|
||||||
firstRun: false,
|
firstRun: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const UBOL_ORIGIN = runtime.getURL('').replace(/\/$/, '');
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
function getCurrentVersion() {
|
function getCurrentVersion() {
|
||||||
|
@ -165,6 +166,9 @@ function onPermissionsRemoved(permissions) {
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
function onMessage(request, sender, callback) {
|
function onMessage(request, sender, callback) {
|
||||||
|
|
||||||
|
if ( sender.origin !== UBOL_ORIGIN ) { return; }
|
||||||
|
|
||||||
switch ( request.what ) {
|
switch ( request.what ) {
|
||||||
|
|
||||||
case 'applyRulesets': {
|
case 'applyRulesets': {
|
||||||
|
@ -214,7 +218,6 @@ function onMessage(request, sender, callback) {
|
||||||
hasOmnipotence(),
|
hasOmnipotence(),
|
||||||
hasGreatPowers(request.origin),
|
hasGreatPowers(request.origin),
|
||||||
getEnabledRulesetsDetails(),
|
getEnabledRulesetsDetails(),
|
||||||
getInjectableCount(request.origin),
|
|
||||||
]).then(results => {
|
]).then(results => {
|
||||||
callback({
|
callback({
|
||||||
level: results[0],
|
level: results[0],
|
||||||
|
@ -222,7 +225,6 @@ function onMessage(request, sender, callback) {
|
||||||
hasOmnipotence: results[1],
|
hasOmnipotence: results[1],
|
||||||
hasGreatPowers: results[2],
|
hasGreatPowers: results[2],
|
||||||
rulesetDetails: results[3],
|
rulesetDetails: results[3],
|
||||||
injectableCount: results[4],
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -101,7 +101,7 @@ async function updateRegexRules() {
|
||||||
const toFetch = [];
|
const toFetch = [];
|
||||||
for ( const details of rulesetDetails ) {
|
for ( const details of rulesetDetails ) {
|
||||||
if ( details.rules.regexes === 0 ) { continue; }
|
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);
|
const regexRulesets = await Promise.all(toFetch);
|
||||||
|
|
||||||
|
@ -196,7 +196,7 @@ async function updateRemoveparamRules() {
|
||||||
const toFetch = [];
|
const toFetch = [];
|
||||||
for ( const details of rulesetDetails ) {
|
for ( const details of rulesetDetails ) {
|
||||||
if ( details.rules.removeparams === 0 ) { continue; }
|
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);
|
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 { fetchJSON } from './fetch.js';
|
||||||
import { getFilteringModeDetails } from './mode-manager.js';
|
import { getFilteringModeDetails } from './mode-manager.js';
|
||||||
import { getEnabledRulesetsDetails } from './ruleset-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() {
|
function getSpecificDetails() {
|
||||||
if ( scriptingDetailsPromise !== undefined ) {
|
let promise = resourceDetailPromises.get('specific');
|
||||||
return scriptingDetailsPromise;
|
if ( promise !== undefined ) { return promise; }
|
||||||
}
|
promise = fetchJSON('/rulesets/specific-details').then(entries => {
|
||||||
scriptingDetailsPromise = fetchJSON('/rulesets/scripting-details').then(entries => {
|
|
||||||
const out = new Map();
|
const out = new Map();
|
||||||
for ( const entry of entries ) {
|
for ( const entry of entries ) {
|
||||||
out.set(entry[0], new Map(entry[1]));
|
out.set(entry[0], new Map(entry[1]));
|
||||||
}
|
}
|
||||||
return out;
|
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
|
// Important: We need to sort the arrays for fast comparison
|
||||||
const arrayEq = (a = [], b = []) => {
|
const arrayEq = (a = [], b = [], sort = true) => {
|
||||||
const alen = a.length;
|
const alen = a.length;
|
||||||
if ( alen !== b.length ) { return false; }
|
if ( alen !== b.length ) { return false; }
|
||||||
a.sort(); b.sort();
|
if ( sort ) { a.sort(); b.sort(); }
|
||||||
for ( let i = 0; i < alen; i++ ) {
|
for ( let i = 0; i < alen; i++ ) {
|
||||||
if ( a[i] !== b[i] ) { return false; }
|
if ( a[i] !== b[i] ) { return false; }
|
||||||
}
|
}
|
||||||
|
@ -65,81 +105,41 @@ const arrayEq = (a = [], b = []) => {
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
const toRegisterableScript = (context, fname, hostnames) => {
|
// The extensions API does not always return exactly what we fed it, so we
|
||||||
if ( context.before.has(fname) ) {
|
// need to normalize some entries to be sure we properly detect changes when
|
||||||
return toUpdatableScript(context, fname, hostnames);
|
// comparing registered entries vs. entries to register.
|
||||||
}
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
const toUpdatableScript = (context, fname, hostnames) => {
|
const normalizeRegisteredContentScripts = registered => {
|
||||||
const registered = context.before.get(fname);
|
for ( const entry of registered ) {
|
||||||
context.before.delete(fname); // Important!
|
const { js } = entry;
|
||||||
const directive = { id: fname };
|
for ( let i = 0; i < js.length; i++ ) {
|
||||||
const matches = hostnames
|
const path = js[i];
|
||||||
? ut.matchesFromHostnames(hostnames)
|
if ( path.startsWith('/') ) { continue; }
|
||||||
: [ '<all_urls>' ];
|
js[i] = `/${path}`;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
return registered;
|
||||||
};
|
};
|
||||||
|
|
||||||
const RUN_AT_END_BIT = 0b10;
|
|
||||||
const MAIN_WORLD_BIT = 0b01;
|
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
async function registerGeneric(context, args) {
|
function registerGeneric(context, genericDetails) {
|
||||||
const { before } = context;
|
const { before, filteringModeDetails, rulesetsDetails } = context;
|
||||||
const registered = before.get('css-generic');
|
|
||||||
before.delete('css-generic'); // Important!
|
|
||||||
|
|
||||||
const {
|
|
||||||
filteringModeDetails,
|
|
||||||
rulesetsDetails,
|
|
||||||
} = args;
|
|
||||||
|
|
||||||
|
const excludeHostnames = [];
|
||||||
const js = [];
|
const js = [];
|
||||||
for ( const details of rulesetsDetails ) {
|
for ( const details of rulesetsDetails ) {
|
||||||
|
const hostnames = genericDetails.get(details.id);
|
||||||
|
if ( hostnames !== undefined ) {
|
||||||
|
excludeHostnames.push(...hostnames);
|
||||||
|
}
|
||||||
if ( details.css.generic.count === 0 ) { continue; }
|
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 ( js.length === 0 ) { return; }
|
||||||
if ( registered !== undefined ) {
|
|
||||||
context.toRemove.push('css-generic');
|
js.push('/js/scripting/css-generic.js');
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const matches = [];
|
const matches = [];
|
||||||
const excludeMatches = [];
|
const excludeMatches = [];
|
||||||
|
@ -147,17 +147,23 @@ async function registerGeneric(context, args) {
|
||||||
excludeMatches.push(...ut.matchesFromHostnames(filteringModeDetails.none));
|
excludeMatches.push(...ut.matchesFromHostnames(filteringModeDetails.none));
|
||||||
excludeMatches.push(...ut.matchesFromHostnames(filteringModeDetails.network));
|
excludeMatches.push(...ut.matchesFromHostnames(filteringModeDetails.network));
|
||||||
excludeMatches.push(...ut.matchesFromHostnames(filteringModeDetails.extendedSpecific));
|
excludeMatches.push(...ut.matchesFromHostnames(filteringModeDetails.extendedSpecific));
|
||||||
|
excludeMatches.push(...ut.matchesFromHostnames(excludeHostnames));
|
||||||
matches.push('<all_urls>');
|
matches.push('<all_urls>');
|
||||||
} else {
|
} else {
|
||||||
matches.push(...ut.matchesFromHostnames(filteringModeDetails.extendedGeneric));
|
matches.push(
|
||||||
|
...ut.matchesFromHostnames(
|
||||||
|
ut.subtractHostnameIters(
|
||||||
|
Array.from(filteringModeDetails.extendedGeneric),
|
||||||
|
excludeHostnames
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( matches.length === 0 ) {
|
if ( matches.length === 0 ) { return; }
|
||||||
if ( registered !== undefined ) {
|
|
||||||
context.toRemove.push('css-generic');
|
const registered = before.get('css-generic');
|
||||||
}
|
before.delete('css-generic'); // Important!
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// register
|
// register
|
||||||
if ( registered === undefined ) {
|
if ( registered === undefined ) {
|
||||||
|
@ -173,7 +179,7 @@ async function registerGeneric(context, args) {
|
||||||
|
|
||||||
// update
|
// update
|
||||||
const directive = { id: 'css-generic' };
|
const directive = { id: 'css-generic' };
|
||||||
if ( arrayEq(registered.js, js) === false ) {
|
if ( arrayEq(registered.js, js, false) === false ) {
|
||||||
directive.js = js;
|
directive.js = js;
|
||||||
}
|
}
|
||||||
if ( arrayEq(registered.matches, matches) === false ) {
|
if ( arrayEq(registered.matches, matches) === false ) {
|
||||||
|
@ -189,62 +195,258 @@ async function registerGeneric(context, args) {
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
async function getInjectableCount(origin) {
|
function registerProcedural(context, proceduralDetails) {
|
||||||
const url = ut.parsedURLromOrigin(origin);
|
const { before, filteringModeDetails, rulesetsDetails } = context;
|
||||||
if ( url === undefined ) { return 0; }
|
|
||||||
|
|
||||||
const [
|
const js = [];
|
||||||
rulesetIds,
|
const hostnameMatches = [];
|
||||||
scriptingDetails,
|
for ( const details of rulesetsDetails ) {
|
||||||
] = await Promise.all([
|
if ( details.css.procedural === 0 ) { continue; }
|
||||||
dnr.getEnabledRulesets(),
|
js.push(`/rulesets/scripting/procedural/${details.id}.procedural.js`);
|
||||||
getScriptingDetails(),
|
if ( proceduralDetails.has(details.id) ) {
|
||||||
]);
|
hostnameMatches.push(...proceduralDetails.get(details.id));
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
function registerDeclarative(context, declarativeDetails) {
|
||||||
const {
|
const { before, filteringModeDetails, rulesetsDetails } = context;
|
||||||
filteringModeDetails,
|
|
||||||
rulesetsDetails,
|
|
||||||
scriptingDetails,
|
|
||||||
} = args;
|
|
||||||
|
|
||||||
// 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 (
|
if (
|
||||||
filteringModeDetails.extendedSpecific.has('all-urls') ||
|
filteringModeDetails.extendedSpecific.has('all-urls') ||
|
||||||
filteringModeDetails.extendedGeneric.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 = [
|
const targetHostnames = [
|
||||||
...filteringModeDetails.extendedSpecific,
|
...filteringModeDetails.extendedSpecific,
|
||||||
...filteringModeDetails.extendedGeneric,
|
...filteringModeDetails.extendedGeneric,
|
||||||
];
|
];
|
||||||
|
|
||||||
const toRegisterMap = new Map();
|
|
||||||
|
|
||||||
const checkMatches = (hostnamesToFidsMap, hn) => {
|
const checkMatches = (hostnamesToFidsMap, hn) => {
|
||||||
let fids = hostnamesToFidsMap.get(hn);
|
let fids = hostnamesToFidsMap.get(hn);
|
||||||
if ( fids === undefined ) { return; }
|
if ( fids === undefined ) { return; }
|
||||||
|
@ -266,7 +468,7 @@ function registerSpecific(args) {
|
||||||
};
|
};
|
||||||
|
|
||||||
for ( const rulesetDetails of rulesetsDetails ) {
|
for ( const rulesetDetails of rulesetsDetails ) {
|
||||||
const hostnamesToFidsMap = scriptingDetails.get(rulesetDetails.id);
|
const hostnamesToFidsMap = specificDetails.get(rulesetDetails.id);
|
||||||
if ( hostnamesToFidsMap === undefined ) { continue; }
|
if ( hostnamesToFidsMap === undefined ) { continue; }
|
||||||
for ( let hn of targetHostnames ) {
|
for ( let hn of targetHostnames ) {
|
||||||
while ( hn ) {
|
while ( hn ) {
|
||||||
|
@ -279,21 +481,17 @@ function registerSpecific(args) {
|
||||||
return toRegisterMap;
|
return toRegisterMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerAllSpecific(args) {
|
function registerSpecificAll(context, specificDetails) {
|
||||||
const {
|
const { filteringModeDetails, rulesetsDetails } = context;
|
||||||
filteringModeDetails,
|
|
||||||
rulesetsDetails,
|
|
||||||
scriptingDetails,
|
|
||||||
} = args;
|
|
||||||
|
|
||||||
const toRegisterMap = new Map();
|
const toRegisterMap = new Map();
|
||||||
|
|
||||||
const excludeSet = new Set([
|
const excludeSet = new Set([
|
||||||
...filteringModeDetails.network,
|
...filteringModeDetails.network,
|
||||||
...filteringModeDetails.none,
|
...filteringModeDetails.none,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
for ( const rulesetDetails of rulesetsDetails ) {
|
for ( const rulesetDetails of rulesetsDetails ) {
|
||||||
const hostnamesToFidsMap = scriptingDetails.get(rulesetDetails.id);
|
const hostnamesToFidsMap = specificDetails.get(rulesetDetails.id);
|
||||||
if ( hostnamesToFidsMap === undefined ) { continue; }
|
if ( hostnamesToFidsMap === undefined ) { continue; }
|
||||||
for ( let [ hn, fids ] of hostnamesToFidsMap ) {
|
for ( let [ hn, fids ] of hostnamesToFidsMap ) {
|
||||||
if ( excludeSet.has(hn) ) { continue; }
|
if ( excludeSet.has(hn) ) { continue; }
|
||||||
|
@ -319,6 +517,48 @@ function registerAllSpecific(args) {
|
||||||
return toRegisterMap;
|
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) {
|
async function registerInjectables(origins) {
|
||||||
|
@ -329,37 +569,44 @@ async function registerInjectables(origins) {
|
||||||
const [
|
const [
|
||||||
filteringModeDetails,
|
filteringModeDetails,
|
||||||
rulesetsDetails,
|
rulesetsDetails,
|
||||||
scriptingDetails,
|
declarativeDetails,
|
||||||
|
proceduralDetails,
|
||||||
|
scriptletDetails,
|
||||||
|
specificDetails,
|
||||||
|
genericDetails,
|
||||||
registered,
|
registered,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
getFilteringModeDetails(),
|
getFilteringModeDetails(),
|
||||||
getEnabledRulesetsDetails(),
|
getEnabledRulesetsDetails(),
|
||||||
getScriptingDetails(),
|
getDeclarativeDetails(),
|
||||||
|
getProceduralDetails(),
|
||||||
|
getScriptletDetails(),
|
||||||
|
getSpecificDetails(),
|
||||||
|
getGenericDetails(),
|
||||||
browser.scripting.getRegisteredContentScripts(),
|
browser.scripting.getRegisteredContentScripts(),
|
||||||
]);
|
]);
|
||||||
|
const before = new Map(
|
||||||
const before = new Map(registered.map(entry => [ entry.id, entry ]));
|
normalizeRegisteredContentScripts(registered).map(
|
||||||
|
entry => [ entry.id, entry ]
|
||||||
|
)
|
||||||
|
);
|
||||||
const toAdd = [], toUpdate = [], toRemove = [];
|
const toAdd = [], toUpdate = [], toRemove = [];
|
||||||
const promises = [];
|
const promises = [];
|
||||||
const context = {
|
const context = {
|
||||||
filteringModeDetails,
|
filteringModeDetails,
|
||||||
|
rulesetsDetails,
|
||||||
before,
|
before,
|
||||||
toAdd,
|
toAdd,
|
||||||
toUpdate,
|
toUpdate,
|
||||||
toRemove,
|
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()));
|
toRemove.push(...Array.from(before.keys()));
|
||||||
|
|
||||||
if ( toRemove.length !== 0 ) {
|
if ( toRemove.length !== 0 ) {
|
||||||
|
@ -391,6 +638,5 @@ async function registerInjectables(origins) {
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getInjectableCount,
|
|
||||||
registerInjectables
|
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) => {
|
const isDescendantHostname = (hna, hnb) => {
|
||||||
if ( b === 'all-urls' ) { return true; }
|
if ( hnb === 'all-urls' ) { return true; }
|
||||||
if ( a.endsWith(b) === false ) { return false; }
|
if ( hna.endsWith(hnb) === false ) { return false; }
|
||||||
if ( a === b ) { return false; }
|
if ( hna === hnb ) { return false; }
|
||||||
return a.charCodeAt(a.length - b.length - 1) === 0x2E /* '.' */;
|
return hna.charCodeAt(hna.length - hnb.length - 1) === 0x2E /* '.' */;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isDescendantHostnameOfIter = (a, iter) => {
|
const isDescendantHostnameOfIter = (hna, iterb) => {
|
||||||
for ( const b of iter ) {
|
const setb = iterb instanceof Set ? iterb : new Set(iterb);
|
||||||
if ( isDescendantHostname(a, b) ) { return true; }
|
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;
|
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 => {
|
const matchesFromHostnames = hostnames => {
|
||||||
|
@ -102,6 +132,8 @@ export {
|
||||||
toBroaderHostname,
|
toBroaderHostname,
|
||||||
isDescendantHostname,
|
isDescendantHostname,
|
||||||
isDescendantHostnameOfIter,
|
isDescendantHostnameOfIter,
|
||||||
|
intersectHostnameIters,
|
||||||
|
subtractHostnameIters,
|
||||||
matchesFromHostnames,
|
matchesFromHostnames,
|
||||||
hostnamesFromMatches,
|
hostnamesFromMatches,
|
||||||
fnameFromFileId,
|
fnameFromFileId,
|
||||||
|
|
|
@ -54,7 +54,7 @@ const commandLineArgs = (( ) => {
|
||||||
const outputDir = commandLineArgs.get('output') || '.';
|
const outputDir = commandLineArgs.get('output') || '.';
|
||||||
const cacheDir = `${outputDir}/../mv3-data`;
|
const cacheDir = `${outputDir}/../mv3-data`;
|
||||||
const rulesetDir = `${outputDir}/rulesets`;
|
const rulesetDir = `${outputDir}/rulesets`;
|
||||||
const scriptletDir = `${rulesetDir}/js`;
|
const scriptletDir = `${rulesetDir}/scripting`;
|
||||||
const env = [
|
const env = [
|
||||||
'chromium',
|
'chromium',
|
||||||
'mv3',
|
'mv3',
|
||||||
|
@ -148,7 +148,11 @@ const writeOps = [];
|
||||||
|
|
||||||
const ruleResources = [];
|
const ruleResources = [];
|
||||||
const rulesetDetails = [];
|
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);
|
log(bad.map(rule => rule._error.map(v => `\t\t${v}`)).join('\n'), true);
|
||||||
|
|
||||||
writeFile(
|
writeFile(
|
||||||
`${rulesetDir}/${assetDetails.id}.json`,
|
`${rulesetDir}/main/${assetDetails.id}.json`,
|
||||||
`${JSON.stringify(plainGood, replacer)}\n`
|
`${JSON.stringify(plainGood, replacer)}\n`
|
||||||
);
|
);
|
||||||
|
|
||||||
if ( regexes.length !== 0 ) {
|
if ( regexes.length !== 0 ) {
|
||||||
writeFile(
|
writeFile(
|
||||||
`${rulesetDir}/${assetDetails.id}.regexes.json`,
|
`${rulesetDir}/regex/${assetDetails.id}.regexes.json`,
|
||||||
`${JSON.stringify(regexes, replacer)}\n`
|
`${JSON.stringify(regexes, replacer)}\n`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( removeparamsGood.length !== 0 ) {
|
if ( removeparamsGood.length !== 0 ) {
|
||||||
writeFile(
|
writeFile(
|
||||||
`${rulesetDir}/${assetDetails.id}.removeparams.json`,
|
`${rulesetDir}/removeparam/${assetDetails.id}.removeparams.json`,
|
||||||
`${JSON.stringify(removeparamsGood, replacer)}\n`
|
`${JSON.stringify(removeparamsGood, replacer)}\n`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -366,10 +370,10 @@ const globalPatchedScriptletsSet = new Set();
|
||||||
function addScriptingAPIResources(id, hostnames, fid) {
|
function addScriptingAPIResources(id, hostnames, fid) {
|
||||||
if ( hostnames === undefined ) { return; }
|
if ( hostnames === undefined ) { return; }
|
||||||
for ( const hn of hostnames ) {
|
for ( const hn of hostnames ) {
|
||||||
let hostnamesToFidMap = scriptingDetails.get(id);
|
let hostnamesToFidMap = specificDetails.get(id);
|
||||||
if ( hostnamesToFidMap === undefined ) {
|
if ( hostnamesToFidMap === undefined ) {
|
||||||
hostnamesToFidMap = new Map();
|
hostnamesToFidMap = new Map();
|
||||||
scriptingDetails.set(id, hostnamesToFidMap);
|
specificDetails.set(id, hostnamesToFidMap);
|
||||||
}
|
}
|
||||||
let fids = hostnamesToFidMap.get(hn);
|
let fids = hostnamesToFidMap.get(hn);
|
||||||
if ( fids === undefined ) {
|
if ( fids === undefined ) {
|
||||||
|
@ -383,11 +387,9 @@ function addScriptingAPIResources(id, hostnames, fid) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const toIsolatedStartFileId = s => (uidint32(s) & ~0b11) | 0b00;
|
const toCSSSpecific = s => (uidint32(s) & ~0b11) | 0b00;
|
||||||
const toMainStartFileId = s => (uidint32(s) & ~0b11) | 0b01;
|
|
||||||
const toIsolatedEndFileId = s => (uidint32(s) & ~0b11) | 0b10;
|
|
||||||
|
|
||||||
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$',
|
'$rulesetId$',
|
||||||
assetDetails.id
|
assetDetails.id
|
||||||
).replace(
|
).replace(
|
||||||
/\bself\.\$excludeHostnameSet\$/m,
|
/\bself\.\$genericSelectorMap\$/m,
|
||||||
`${JSON.stringify(exclusions, scriptletJsonReplacer)}`
|
|
||||||
).replace(
|
|
||||||
/\bself\.\$genericSelectorLists\$/m,
|
|
||||||
`${JSON.stringify(selectorLists, scriptletJsonReplacer)}`
|
`${JSON.stringify(selectorLists, scriptletJsonReplacer)}`
|
||||||
);
|
);
|
||||||
|
|
||||||
writeFile(
|
writeFile(
|
||||||
`${scriptletDir}/${assetDetails.id}.generic.js`,
|
`${scriptletDir}/generic/${assetDetails.id}.generic.js`,
|
||||||
patchedScriptlet
|
patchedScriptlet
|
||||||
);
|
);
|
||||||
|
|
||||||
|
genericDetails.set(assetDetails.id, exclusions.sort());
|
||||||
|
|
||||||
log(`CSS-generic: ${count} plain CSS selectors`);
|
log(`CSS-generic: ${count} plain CSS selectors`);
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
|
@ -434,10 +435,11 @@ const MAX_COSMETIC_FILTERS_PER_FILE = 256;
|
||||||
|
|
||||||
// This merges selectors which are used by the same hostnames
|
// This merges selectors which are used by the same hostnames
|
||||||
|
|
||||||
function groupCosmeticByHostnames(mapin) {
|
function groupSelectorsByHostnames(mapin) {
|
||||||
if ( mapin === undefined ) { return []; }
|
if ( mapin === undefined ) { return []; }
|
||||||
const merged = new Map();
|
const merged = new Map();
|
||||||
for ( const [ selector, details ] of mapin ) {
|
for ( const [ selector, details ] of mapin ) {
|
||||||
|
if ( details.rejected ) { continue; }
|
||||||
const json = JSON.stringify(details);
|
const json = JSON.stringify(details);
|
||||||
let entries = merged.get(json);
|
let entries = merged.get(json);
|
||||||
if ( entries === undefined ) {
|
if ( entries === undefined ) {
|
||||||
|
@ -460,7 +462,7 @@ function groupCosmeticByHostnames(mapin) {
|
||||||
// Also, we sort the hostnames to increase likelihood that selector with
|
// Also, we sort the hostnames to increase likelihood that selector with
|
||||||
// same hostnames will end up in same generated scriptlet.
|
// same hostnames will end up in same generated scriptlet.
|
||||||
|
|
||||||
function groupCosmeticBySelectors(arrayin) {
|
function groupHostnamesBySelectors(arrayin) {
|
||||||
const contentMap = new Map();
|
const contentMap = new Map();
|
||||||
for ( const entry of arrayin ) {
|
for ( const entry of arrayin ) {
|
||||||
const id = uidint32(JSON.stringify(entry.selectors));
|
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) {
|
async function processCosmeticFilters(assetDetails, mapin) {
|
||||||
if ( mapin === undefined ) { return 0; }
|
if ( mapin === undefined ) { return 0; }
|
||||||
|
|
||||||
const contentArray = groupCosmeticBySelectors(
|
const contentArray = groupHostnamesBySelectors(
|
||||||
groupCosmeticByHostnames(mapin)
|
groupSelectorsByHostnames(mapin)
|
||||||
);
|
);
|
||||||
|
|
||||||
// We do not want more than n CSS files per subscription, so we will
|
// 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; }
|
if ( details.y === undefined ) { continue; }
|
||||||
scriptletHostnameToIdMap(details.y, id, hostnamesMap);
|
scriptletHostnameToIdMap(details.y, id, hostnamesMap);
|
||||||
}
|
}
|
||||||
|
const argsList = argsMap2List(argsMap, hostnamesMap);
|
||||||
const patchedScriptlet = originalScriptletMap.get('css-specific')
|
const patchedScriptlet = originalScriptletMap.get('css-specific')
|
||||||
.replace(
|
.replace(
|
||||||
'$rulesetId$',
|
'$rulesetId$',
|
||||||
assetDetails.id
|
assetDetails.id
|
||||||
).replace(
|
).replace(
|
||||||
/\bself\.\$argsMap\$/m,
|
/\bself\.\$argsList\$/m,
|
||||||
`${JSON.stringify(argsMap, scriptletJsonReplacer)}`
|
`${JSON.stringify(argsList, scriptletJsonReplacer)}`
|
||||||
).replace(
|
).replace(
|
||||||
/\bself\.\$hostnamesMap\$/m,
|
/\bself\.\$hostnamesMap\$/m,
|
||||||
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
|
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
|
||||||
);
|
);
|
||||||
const fid = toIsolatedStartFileId(patchedScriptlet);
|
const fid = toCSSSpecific(patchedScriptlet);
|
||||||
if ( globalPatchedScriptletsSet.has(fid) === false ) {
|
if ( globalPatchedScriptletsSet.has(fid) === false ) {
|
||||||
globalPatchedScriptletsSet.add(fid);
|
globalPatchedScriptletsSet.add(fid);
|
||||||
const fname = fnameFromFileId(fid);
|
const fname = fnameFromFileId(fid);
|
||||||
writeFile(pathFromFileName(fname), patchedScriptlet);
|
writeFile(`${scriptletDir}/specific/${pathFromFileName(fname)}`, patchedScriptlet);
|
||||||
generatedFiles.push(fname);
|
generatedFiles.push(fname);
|
||||||
}
|
}
|
||||||
for ( const entry of slice ) {
|
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 === undefined ) { return 0; }
|
||||||
|
if ( mapin.size === 0 ) { return 0; }
|
||||||
|
|
||||||
const contentArray = groupCosmeticBySelectors(
|
// Distinguish declarative-compiled-as-procedural from actual procedural.
|
||||||
groupCosmeticByHostnames(mapin)
|
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
|
const argsMap = contentArray.map(entry => [
|
||||||
// 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 => [
|
|
||||||
entry[0],
|
entry[0],
|
||||||
{
|
{
|
||||||
a: entry[1].a ? entry[1].a.map(v => JSON.parse(v)) : undefined,
|
a: entry[1].a,
|
||||||
n: entry[1].n
|
n: entry[1].n,
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
const hostnamesMap = new Map();
|
const hostnamesMap = new Map();
|
||||||
for ( const [ id, details ] of slice ) {
|
for ( const [ id, details ] of contentArray ) {
|
||||||
if ( details.y === undefined ) { continue; }
|
if ( details.y === undefined ) { continue; }
|
||||||
scriptletHostnameToIdMap(details.y, id, hostnamesMap);
|
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(
|
.replace(
|
||||||
'$rulesetId$',
|
'$rulesetId$',
|
||||||
assetDetails.id
|
assetDetails.id
|
||||||
).replace(
|
).replace(
|
||||||
/\bself\.\$argsMap\$/m,
|
/\bself\.\$argsList\$/m,
|
||||||
`${JSON.stringify(argsMap, scriptletJsonReplacer)}`
|
`${JSON.stringify(argsList, scriptletJsonReplacer)}`
|
||||||
).replace(
|
).replace(
|
||||||
/\bself\.\$hostnamesMap\$/m,
|
/\bself\.\$hostnamesMap\$/m,
|
||||||
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
|
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
|
||||||
);
|
);
|
||||||
const fid = toIsolatedEndFileId(patchedScriptlet);
|
writeFile(`${scriptletDir}/declarative/${assetDetails.id}.declarative.js`, patchedScriptlet);
|
||||||
if ( globalPatchedScriptletsSet.has(fid) === false ) {
|
|
||||||
globalPatchedScriptletsSet.add(fid);
|
{
|
||||||
const fname = fnameFromFileId(fid);
|
const hostnames = new Set();
|
||||||
writeFile(pathFromFileName(fname), patchedScriptlet);
|
for ( const entry of contentArray ) {
|
||||||
generatedFiles.push(fname);
|
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 distinct filters: ${contentArray.length} distinct combined selectors`);
|
||||||
log(`Procedural-related injectable files: ${generatedFiles.length}`);
|
|
||||||
log(`\t${generatedFiles.join(', ')}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return contentArray.length;
|
return contentArray.length;
|
||||||
|
@ -753,28 +849,34 @@ async function processScriptletFilters(assetDetails, mapin) {
|
||||||
for ( const [ argsHash, details ] of argsDetails ) {
|
for ( const [ argsHash, details ] of argsDetails ) {
|
||||||
scriptletHostnameToIdMap(details.y, uidint32(argsHash), hostnamesMap);
|
scriptletHostnameToIdMap(details.y, uidint32(argsHash), hostnamesMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const argsList = argsMap2List(argsMap, hostnamesMap);
|
||||||
const patchedScriptlet = originalScriptletMap.get(token)
|
const patchedScriptlet = originalScriptletMap.get(token)
|
||||||
.replace(
|
.replace(
|
||||||
'$rulesetId$',
|
'$rulesetId$',
|
||||||
assetDetails.id
|
assetDetails.id
|
||||||
).replace(
|
).replace(
|
||||||
/\bself\.\$argsMap\$/m,
|
/\bself\.\$argsList\$/m,
|
||||||
`${JSON.stringify(argsMap, scriptletJsonReplacer)}`
|
`${JSON.stringify(argsList, scriptletJsonReplacer)}`
|
||||||
).replace(
|
).replace(
|
||||||
/\bself\.\$hostnamesMap\$/m,
|
/\bself\.\$hostnamesMap\$/m,
|
||||||
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
|
`${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
|
||||||
);
|
);
|
||||||
// ends-with 1 = scriptlet resource
|
const fname = `${assetDetails.id}.${token}.js`;
|
||||||
const fid = toMainStartFileId(patchedScriptlet);
|
const fpath = `${scriptletDir}/scriptlet/${fname}`;
|
||||||
if ( globalPatchedScriptletsSet.has(fid) === false ) {
|
writeFile(fpath, patchedScriptlet);
|
||||||
globalPatchedScriptletsSet.add(fid);
|
|
||||||
const fname = fnameFromFileId(fid);
|
|
||||||
writeFile(pathFromFileName(fname), patchedScriptlet);
|
|
||||||
generatedFiles.push(fname);
|
generatedFiles.push(fname);
|
||||||
|
|
||||||
|
const hostnameMatches = new Set(hostnamesMap.keys());
|
||||||
|
if ( hostnameMatches.has('*') ) {
|
||||||
|
hostnameMatches.clear();
|
||||||
|
hostnameMatches.add('*');
|
||||||
}
|
}
|
||||||
for ( const details of argsDetails.values() ) {
|
let rulesetScriptlets = scriptletStats.get(assetDetails.id);
|
||||||
addScriptingAPIResources(assetDetails.id, details.y, fid);
|
if ( rulesetScriptlets === undefined ) {
|
||||||
|
scriptletStats.set(assetDetails.id, rulesetScriptlets = []);
|
||||||
}
|
}
|
||||||
|
rulesetScriptlets.push([ token, Array.from(hostnameMatches).sort() ]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( generatedFiles.length !== 0 ) {
|
if ( generatedFiles.length !== 0 ) {
|
||||||
|
@ -790,15 +892,18 @@ async function processScriptletFilters(assetDetails, mapin) {
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
const rulesetFromURLS = async function(assetDetails) {
|
async function rulesetFromURLs(assetDetails) {
|
||||||
log('============================');
|
log('============================');
|
||||||
log(`Listset for '${assetDetails.id}':`);
|
log(`Listset for '${assetDetails.id}':`);
|
||||||
|
|
||||||
|
if ( assetDetails.text === undefined ) {
|
||||||
const text = await fetchAsset(assetDetails);
|
const text = await fetchAsset(assetDetails);
|
||||||
if ( text === '' ) { return; }
|
if ( text === '' ) { return; }
|
||||||
|
assetDetails.text = text;
|
||||||
|
}
|
||||||
|
|
||||||
const results = await dnrRulesetFromRawLists(
|
const results = await dnrRulesetFromRawLists(
|
||||||
[ { name: assetDetails.id, text } ],
|
[ { name: assetDetails.id, text: assetDetails.text } ],
|
||||||
{ env }
|
{ env }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -826,6 +931,11 @@ const rulesetFromURLS = async function(assetDetails) {
|
||||||
proceduralCosmetic.set(JSON.stringify(parsed), details);
|
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(
|
const genericCosmeticStats = await processGenericCosmeticFilters(
|
||||||
assetDetails,
|
assetDetails,
|
||||||
results.genericCosmetic,
|
results.genericCosmetic,
|
||||||
|
@ -835,15 +945,14 @@ const rulesetFromURLS = async function(assetDetails) {
|
||||||
assetDetails,
|
assetDetails,
|
||||||
declarativeCosmetic
|
declarativeCosmetic
|
||||||
);
|
);
|
||||||
|
const declarativeStats = await processDeclarativeCosmeticFilters(
|
||||||
|
assetDetails,
|
||||||
|
proceduralCosmetic
|
||||||
|
);
|
||||||
const proceduralStats = await processProceduralCosmeticFilters(
|
const proceduralStats = await processProceduralCosmeticFilters(
|
||||||
assetDetails,
|
assetDetails,
|
||||||
proceduralCosmetic
|
proceduralCosmetic
|
||||||
);
|
);
|
||||||
if ( rejectedCosmetic.length !== 0 ) {
|
|
||||||
log(`Rejected cosmetic filters: ${rejectedCosmetic.length}`);
|
|
||||||
log(rejectedCosmetic.map(line => `\t${line}`).join('\n'));
|
|
||||||
}
|
|
||||||
|
|
||||||
const scriptletStats = await processScriptletFilters(
|
const scriptletStats = await processScriptletFilters(
|
||||||
assetDetails,
|
assetDetails,
|
||||||
results.scriptlet
|
results.scriptlet
|
||||||
|
@ -871,6 +980,7 @@ const rulesetFromURLS = async function(assetDetails) {
|
||||||
css: {
|
css: {
|
||||||
generic: genericCosmeticStats,
|
generic: genericCosmeticStats,
|
||||||
specific: specificCosmeticStats,
|
specific: specificCosmeticStats,
|
||||||
|
declarative: declarativeStats,
|
||||||
procedural: proceduralStats,
|
procedural: proceduralStats,
|
||||||
},
|
},
|
||||||
scriptlets: {
|
scriptlets: {
|
||||||
|
@ -881,9 +991,9 @@ const rulesetFromURLS = async function(assetDetails) {
|
||||||
ruleResources.push({
|
ruleResources.push({
|
||||||
id: assetDetails.id,
|
id: assetDetails.id,
|
||||||
enabled: assetDetails.enabled,
|
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/resource-abuse.txt',
|
||||||
'https://ublockorigin.pages.dev/filters/unbreak.txt',
|
'https://ublockorigin.pages.dev/filters/unbreak.txt',
|
||||||
'https://ublockorigin.pages.dev/filters/quick-fixes.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/easylist.txt',
|
||||||
'https://secure.fanboy.co.nz/easyprivacy.txt',
|
'https://secure.fanboy.co.nz/easyprivacy.txt',
|
||||||
'https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&showintro=1&mimetype=plaintext',
|
'https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&showintro=1&mimetype=plaintext',
|
||||||
];
|
];
|
||||||
await rulesetFromURLS({
|
await rulesetFromURLs({
|
||||||
id: 'default',
|
id: 'default',
|
||||||
name: 'Ads, trackers, miners, and more' ,
|
name: 'Ads, trackers, miners, and more' ,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
@ -967,7 +1077,7 @@ async function main() {
|
||||||
}
|
}
|
||||||
const id = ids[0];
|
const id = ids[0];
|
||||||
const asset = assets[id];
|
const asset = assets[id];
|
||||||
await rulesetFromURLS({
|
await rulesetFromURLs({
|
||||||
id: id.toLowerCase(),
|
id: id.toLowerCase(),
|
||||||
lang: asset.lang,
|
lang: asset.lang,
|
||||||
name: asset.title,
|
name: asset.title,
|
||||||
|
@ -986,7 +1096,7 @@ async function main() {
|
||||||
const contentURL = Array.isArray(asset.contentURL)
|
const contentURL = Array.isArray(asset.contentURL)
|
||||||
? asset.contentURL[0]
|
? asset.contentURL[0]
|
||||||
: asset.contentURL;
|
: asset.contentURL;
|
||||||
await rulesetFromURLS({
|
await rulesetFromURLs({
|
||||||
id: id.toLowerCase(),
|
id: id.toLowerCase(),
|
||||||
name: asset.title,
|
name: asset.title,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
@ -996,14 +1106,14 @@ async function main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handpicked rulesets from abroad
|
// Handpicked rulesets from abroad
|
||||||
await rulesetFromURLS({
|
await rulesetFromURLs({
|
||||||
id: 'cname-trackers',
|
id: 'cname-trackers',
|
||||||
name: 'AdGuard CNAME-cloaked trackers',
|
name: 'AdGuard CNAME-cloaked trackers',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
urls: [ 'https://raw.githubusercontent.com/AdguardTeam/cname-trackers/master/combined_disguised_trackers.txt' ],
|
urls: [ 'https://raw.githubusercontent.com/AdguardTeam/cname-trackers/master/combined_disguised_trackers.txt' ],
|
||||||
homeURL: 'https://github.com/AdguardTeam/cname-trackers#cname-cloaked-trackers',
|
homeURL: 'https://github.com/AdguardTeam/cname-trackers#cname-cloaked-trackers',
|
||||||
});
|
});
|
||||||
await rulesetFromURLS({
|
await rulesetFromURLs({
|
||||||
id: 'stevenblack-hosts',
|
id: 'stevenblack-hosts',
|
||||||
name: 'Steven Black\'s hosts file',
|
name: 'Steven Black\'s hosts file',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
@ -1018,15 +1128,35 @@ async function main() {
|
||||||
|
|
||||||
// We sort the hostnames for convenience/performance in the extension's
|
// We sort the hostnames for convenience/performance in the extension's
|
||||||
// script manager -- the scripting API does a sort() internally.
|
// script manager -- the scripting API does a sort() internally.
|
||||||
for ( const [ rulesetId, hostnamesToFidsMap ] of scriptingDetails ) {
|
for ( const [ rulesetId, hostnamesToFidsMap ] of specificDetails ) {
|
||||||
scriptingDetails.set(
|
specificDetails.set(
|
||||||
rulesetId,
|
rulesetId,
|
||||||
Array.from(hostnamesToFidsMap).sort()
|
Array.from(hostnamesToFidsMap).sort()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
writeFile(
|
writeFile(
|
||||||
`${rulesetDir}/scripting-details.json`,
|
`${rulesetDir}/specific-details.json`,
|
||||||
`${JSON.stringify(scriptingDetails, jsonSetMapReplacer)}\n`
|
`${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);
|
await Promise.all(writeOps);
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
|
|
||||||
// $rulesetId$
|
// $rulesetId$
|
||||||
|
|
||||||
const argsMap = new Map(self.$argsMap$);
|
const argsList = self.$argsList$;
|
||||||
|
|
||||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||||
|
|
||||||
|
@ -157,10 +157,10 @@ let hn;
|
||||||
try { hn = document.location.hostname; } catch(ex) { }
|
try { hn = document.location.hostname; } catch(ex) { }
|
||||||
while ( hn ) {
|
while ( hn ) {
|
||||||
if ( hostnamesMap.has(hn) ) {
|
if ( hostnamesMap.has(hn) ) {
|
||||||
let argsHashes = hostnamesMap.get(hn);
|
let argsIndices = hostnamesMap.get(hn);
|
||||||
if ( typeof argsHashes === 'number' ) { argsHashes = [ argsHashes ]; }
|
if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
|
||||||
for ( const argsHash of argsHashes ) {
|
for ( const argsIndex of argsIndices ) {
|
||||||
const details = argsMap.get(argsHash);
|
const details = argsList[argsIndex];
|
||||||
if ( details.n && details.n.includes(hn) ) { continue; }
|
if ( details.n && details.n.includes(hn) ) { continue; }
|
||||||
try { scriptlet(...details.a); } catch(ex) {}
|
try { scriptlet(...details.a); } catch(ex) {}
|
||||||
}
|
}
|
||||||
|
@ -174,9 +174,7 @@ while ( hn ) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/******************************************************************************/
|
argsList.length = 0;
|
||||||
|
|
||||||
argsMap.clear();
|
|
||||||
hostnamesMap.clear();
|
hostnamesMap.clear();
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
|
|
||||||
// $rulesetId$
|
// $rulesetId$
|
||||||
|
|
||||||
const argsMap = new Map(self.$argsMap$);
|
const argsList = self.$argsList$;
|
||||||
|
|
||||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||||
|
|
||||||
|
@ -115,10 +115,10 @@ let hn;
|
||||||
try { hn = document.location.hostname; } catch(ex) { }
|
try { hn = document.location.hostname; } catch(ex) { }
|
||||||
while ( hn ) {
|
while ( hn ) {
|
||||||
if ( hostnamesMap.has(hn) ) {
|
if ( hostnamesMap.has(hn) ) {
|
||||||
let argsHashes = hostnamesMap.get(hn);
|
let argsIndices = hostnamesMap.get(hn);
|
||||||
if ( typeof argsHashes === 'number' ) { argsHashes = [ argsHashes ]; }
|
if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
|
||||||
for ( const argsHash of argsHashes ) {
|
for ( const argsIndex of argsIndices ) {
|
||||||
const details = argsMap.get(argsHash);
|
const details = argsList[argsIndex];
|
||||||
if ( details.n && details.n.includes(hn) ) { continue; }
|
if ( details.n && details.n.includes(hn) ) { continue; }
|
||||||
try { scriptlet(...details.a); } catch(ex) {}
|
try { scriptlet(...details.a); } catch(ex) {}
|
||||||
}
|
}
|
||||||
|
@ -132,9 +132,7 @@ while ( hn ) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/******************************************************************************/
|
argsList.length = 0;
|
||||||
|
|
||||||
argsMap.clear();
|
|
||||||
hostnamesMap.clear();
|
hostnamesMap.clear();
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
|
|
||||||
// $rulesetId$
|
// $rulesetId$
|
||||||
|
|
||||||
const argsMap = new Map(self.$argsMap$);
|
const argsList = self.$argsList$;
|
||||||
|
|
||||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||||
|
|
||||||
|
@ -89,10 +89,10 @@ let hn;
|
||||||
try { hn = document.location.hostname; } catch(ex) { }
|
try { hn = document.location.hostname; } catch(ex) { }
|
||||||
while ( hn ) {
|
while ( hn ) {
|
||||||
if ( hostnamesMap.has(hn) ) {
|
if ( hostnamesMap.has(hn) ) {
|
||||||
let argsHashes = hostnamesMap.get(hn);
|
let argsIndices = hostnamesMap.get(hn);
|
||||||
if ( typeof argsHashes === 'number' ) { argsHashes = [ argsHashes ]; }
|
if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
|
||||||
for ( const argsHash of argsHashes ) {
|
for ( const argsIndex of argsIndices ) {
|
||||||
const details = argsMap.get(argsHash);
|
const details = argsList[argsIndex];
|
||||||
if ( details.n && details.n.includes(hn) ) { continue; }
|
if ( details.n && details.n.includes(hn) ) { continue; }
|
||||||
try { scriptlet(...details.a); } catch(ex) {}
|
try { scriptlet(...details.a); } catch(ex) {}
|
||||||
}
|
}
|
||||||
|
@ -106,9 +106,7 @@ while ( hn ) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/******************************************************************************/
|
argsList.length = 0;
|
||||||
|
|
||||||
argsMap.clear();
|
|
||||||
hostnamesMap.clear();
|
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!
|
// Important!
|
||||||
// Isolate from global scope
|
// Isolate from global scope
|
||||||
(function uBOL_cssGeneric() {
|
(function uBOL_cssGenericImport() {
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
// $rulesetId$
|
// $rulesetId$
|
||||||
|
|
||||||
{
|
const toImport = self.$genericSelectorMap$;
|
||||||
const excludeHostnameSet = new Set(self.$excludeHostnameSet$);
|
|
||||||
|
|
||||||
let hn;
|
const genericSelectorMap = self.genericSelectorMap || new Map();
|
||||||
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 genericSelectorLists = new Map(self.$genericSelectorLists$);
|
if ( genericSelectorMap.size === 0 ) {
|
||||||
|
self.genericSelectorMap = new Map(toImport);
|
||||||
/******************************************************************************/
|
|
||||||
|
|
||||||
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;
|
return;
|
||||||
}
|
}
|
||||||
if ( styleSheetTimer !== undefined ) { return; }
|
|
||||||
styleSheetTimer = self.requestAnimationFrame(( ) => {
|
|
||||||
styleSheetTimer = undefined;
|
|
||||||
uBOL_injectStyleSheet();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/******************************************************************************/
|
for ( const toImportEntry of toImport ) {
|
||||||
|
const existing = genericSelectorMap.get(toImportEntry[0]);
|
||||||
const uBOL_processChanges = mutations => {
|
genericSelectorMap.set(
|
||||||
for ( let i = 0; i < mutations.length; i++ ) {
|
toImportEntry[0],
|
||||||
const mutation = mutations[i];
|
existing === undefined
|
||||||
for ( const added of mutation.addedNodes ) {
|
? toImportEntry[1]
|
||||||
if ( added.nodeType !== 1 ) { continue; }
|
: `${existing},${toImportEntry[1]}`
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
/******************************************************************************/
|
self.genericSelectorMap = genericSelectorMap;
|
||||||
|
|
||||||
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}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
|
|
|
@ -25,659 +25,24 @@
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
/// name css-specific-procedural
|
/// name css-procedural
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
// Important!
|
// Important!
|
||||||
// Isolate from global scope
|
// Isolate from global scope
|
||||||
(function uBOL_cssSpecificProcedural() {
|
(function uBOL_cssProceduralImport() {
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
// $rulesetId$
|
// $rulesetId$
|
||||||
|
|
||||||
const argsMap = new Map(self.$argsMap$);
|
const argsList = self.$argsList$;
|
||||||
|
|
||||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||||
|
|
||||||
/******************************************************************************/
|
self.proceduralImports = self.proceduralImports || [];
|
||||||
|
self.proceduralImports.push({ argsList, 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();
|
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
|
|
||||||
// $rulesetId$
|
// $rulesetId$
|
||||||
|
|
||||||
const argsMap = new Map(self.$argsMap$);
|
const argsList = self.$argsList$;
|
||||||
|
|
||||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||||
|
|
||||||
|
@ -48,10 +48,10 @@ try { hn = document.location.hostname; } catch(ex) { }
|
||||||
const styles = [];
|
const styles = [];
|
||||||
while ( hn ) {
|
while ( hn ) {
|
||||||
if ( hostnamesMap.has(hn) ) {
|
if ( hostnamesMap.has(hn) ) {
|
||||||
let argsHashes = hostnamesMap.get(hn);
|
let argsIndices = hostnamesMap.get(hn);
|
||||||
if ( typeof argsHashes === 'number' ) { argsHashes = [ argsHashes ]; }
|
if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
|
||||||
for ( const argsHash of argsHashes ) {
|
for ( const argsIndex of argsIndices ) {
|
||||||
const details = argsMap.get(argsHash);
|
const details = argsList[argsIndex];
|
||||||
if ( details.n && details.n.includes(hn) ) { continue; }
|
if ( details.n && details.n.includes(hn) ) { continue; }
|
||||||
styles.push(details.a);
|
styles.push(details.a);
|
||||||
}
|
}
|
||||||
|
@ -65,6 +65,9 @@ while ( hn ) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
argsList.length = 0;
|
||||||
|
hostnamesMap.clear();
|
||||||
|
|
||||||
if ( styles.length === 0 ) { return; }
|
if ( styles.length === 0 ) { return; }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -79,11 +82,6 @@ try {
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
argsMap.clear();
|
|
||||||
hostnamesMap.clear();
|
|
||||||
|
|
||||||
/******************************************************************************/
|
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
|
|
||||||
// $rulesetId$
|
// $rulesetId$
|
||||||
|
|
||||||
const argsMap = new Map(self.$argsMap$);
|
const argsList = self.$argsList$;
|
||||||
|
|
||||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||||
|
|
||||||
|
@ -133,10 +133,10 @@ let hn;
|
||||||
try { hn = document.location.hostname; } catch(ex) { }
|
try { hn = document.location.hostname; } catch(ex) { }
|
||||||
while ( hn ) {
|
while ( hn ) {
|
||||||
if ( hostnamesMap.has(hn) ) {
|
if ( hostnamesMap.has(hn) ) {
|
||||||
let argsHashes = hostnamesMap.get(hn);
|
let argsIndices = hostnamesMap.get(hn);
|
||||||
if ( typeof argsHashes === 'number' ) { argsHashes = [ argsHashes ]; }
|
if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
|
||||||
for ( const argsHash of argsHashes ) {
|
for ( const argsIndex of argsIndices ) {
|
||||||
const details = argsMap.get(argsHash);
|
const details = argsList[argsIndex];
|
||||||
if ( details.n && details.n.includes(hn) ) { continue; }
|
if ( details.n && details.n.includes(hn) ) { continue; }
|
||||||
try { scriptlet(...details.a); } catch(ex) {}
|
try { scriptlet(...details.a); } catch(ex) {}
|
||||||
}
|
}
|
||||||
|
@ -150,9 +150,7 @@ while ( hn ) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/******************************************************************************/
|
argsList.length = 0;
|
||||||
|
|
||||||
argsMap.clear();
|
|
||||||
hostnamesMap.clear();
|
hostnamesMap.clear();
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
/// name no-addEventListener-if
|
/// name no-addeventlistener-if
|
||||||
/// alias noaelif
|
/// alias noaelif
|
||||||
/// alias aeld
|
/// alias aeld
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
|
|
||||||
// $rulesetId$
|
// $rulesetId$
|
||||||
|
|
||||||
const argsMap = new Map(self.$argsMap$);
|
const argsList = self.$argsList$;
|
||||||
|
|
||||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||||
|
|
||||||
|
@ -89,10 +89,10 @@ let hn;
|
||||||
try { hn = document.location.hostname; } catch(ex) { }
|
try { hn = document.location.hostname; } catch(ex) { }
|
||||||
while ( hn ) {
|
while ( hn ) {
|
||||||
if ( hostnamesMap.has(hn) ) {
|
if ( hostnamesMap.has(hn) ) {
|
||||||
let argsHashes = hostnamesMap.get(hn);
|
let argsIndices = hostnamesMap.get(hn);
|
||||||
if ( typeof argsHashes === 'number' ) { argsHashes = [ argsHashes ]; }
|
if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
|
||||||
for ( const argsHash of argsHashes ) {
|
for ( const argsIndex of argsIndices ) {
|
||||||
const details = argsMap.get(argsHash);
|
const details = argsList[argsIndex];
|
||||||
if ( details.n && details.n.includes(hn) ) { continue; }
|
if ( details.n && details.n.includes(hn) ) { continue; }
|
||||||
try { scriptlet(...details.a); } catch(ex) {}
|
try { scriptlet(...details.a); } catch(ex) {}
|
||||||
}
|
}
|
||||||
|
@ -106,9 +106,7 @@ while ( hn ) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/******************************************************************************/
|
argsList.length = 0;
|
||||||
|
|
||||||
argsMap.clear();
|
|
||||||
hostnamesMap.clear();
|
hostnamesMap.clear();
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
|
@ -28,7 +28,8 @@
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
/// name no-setInterval-if
|
/// name no-setinterval-if
|
||||||
|
/// alias no-setInterval-if
|
||||||
/// alias nosiif
|
/// alias nosiif
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
@ -41,7 +42,7 @@
|
||||||
|
|
||||||
// $rulesetId$
|
// $rulesetId$
|
||||||
|
|
||||||
const argsMap = new Map(self.$argsMap$);
|
const argsList = self.$argsList$;
|
||||||
|
|
||||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||||
|
|
||||||
|
@ -92,10 +93,10 @@ let hn;
|
||||||
try { hn = document.location.hostname; } catch(ex) { }
|
try { hn = document.location.hostname; } catch(ex) { }
|
||||||
while ( hn ) {
|
while ( hn ) {
|
||||||
if ( hostnamesMap.has(hn) ) {
|
if ( hostnamesMap.has(hn) ) {
|
||||||
let argsHashes = hostnamesMap.get(hn);
|
let argsIndices = hostnamesMap.get(hn);
|
||||||
if ( typeof argsHashes === 'number' ) { argsHashes = [ argsHashes ]; }
|
if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
|
||||||
for ( const argsHash of argsHashes ) {
|
for ( const argsIndex of argsIndices ) {
|
||||||
const details = argsMap.get(argsHash);
|
const details = argsList[argsIndex];
|
||||||
if ( details.n && details.n.includes(hn) ) { continue; }
|
if ( details.n && details.n.includes(hn) ) { continue; }
|
||||||
try { scriptlet(...details.a); } catch(ex) {}
|
try { scriptlet(...details.a); } catch(ex) {}
|
||||||
}
|
}
|
||||||
|
@ -109,9 +110,7 @@ while ( hn ) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/******************************************************************************/
|
argsList.length = 0;
|
||||||
|
|
||||||
argsMap.clear();
|
|
||||||
hostnamesMap.clear();
|
hostnamesMap.clear();
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
|
@ -28,7 +28,8 @@
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
/// name no-setTimeout-if
|
/// name no-settimeout-if
|
||||||
|
/// alias no-setTimeout-if
|
||||||
/// alias nostif
|
/// alias nostif
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
@ -41,7 +42,7 @@
|
||||||
|
|
||||||
// $rulesetId$
|
// $rulesetId$
|
||||||
|
|
||||||
const argsMap = new Map(self.$argsMap$);
|
const argsList = self.$argsList$;
|
||||||
|
|
||||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||||
|
|
||||||
|
@ -92,10 +93,10 @@ let hn;
|
||||||
try { hn = document.location.hostname; } catch(ex) { }
|
try { hn = document.location.hostname; } catch(ex) { }
|
||||||
while ( hn ) {
|
while ( hn ) {
|
||||||
if ( hostnamesMap.has(hn) ) {
|
if ( hostnamesMap.has(hn) ) {
|
||||||
let argsHashes = hostnamesMap.get(hn);
|
let argsIndices = hostnamesMap.get(hn);
|
||||||
if ( typeof argsHashes === 'number' ) { argsHashes = [ argsHashes ]; }
|
if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
|
||||||
for ( const argsHash of argsHashes ) {
|
for ( const argsIndex of argsIndices ) {
|
||||||
const details = argsMap.get(argsHash);
|
const details = argsList[argsIndex];
|
||||||
if ( details.n && details.n.includes(hn) ) { continue; }
|
if ( details.n && details.n.includes(hn) ) { continue; }
|
||||||
try { scriptlet(...details.a); } catch(ex) {}
|
try { scriptlet(...details.a); } catch(ex) {}
|
||||||
}
|
}
|
||||||
|
@ -109,9 +110,7 @@ while ( hn ) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/******************************************************************************/
|
argsList.length = 0;
|
||||||
|
|
||||||
argsMap.clear();
|
|
||||||
hostnamesMap.clear();
|
hostnamesMap.clear();
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
/// name no-windowOpen-if
|
/// name no-windowOpen-if
|
||||||
|
/// alias no-windowopen-if
|
||||||
/// alias nowoif
|
/// alias nowoif
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
@ -41,7 +42,7 @@
|
||||||
|
|
||||||
// $rulesetId$
|
// $rulesetId$
|
||||||
|
|
||||||
const argsMap = new Map(self.$argsMap$);
|
const argsList = self.$argsList$;
|
||||||
|
|
||||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||||
|
|
||||||
|
@ -129,10 +130,10 @@ let hn;
|
||||||
try { hn = document.location.hostname; } catch(ex) { }
|
try { hn = document.location.hostname; } catch(ex) { }
|
||||||
while ( hn ) {
|
while ( hn ) {
|
||||||
if ( hostnamesMap.has(hn) ) {
|
if ( hostnamesMap.has(hn) ) {
|
||||||
let argsHashes = hostnamesMap.get(hn);
|
let argsIndices = hostnamesMap.get(hn);
|
||||||
if ( typeof argsHashes === 'number' ) { argsHashes = [ argsHashes ]; }
|
if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
|
||||||
for ( const argsHash of argsHashes ) {
|
for ( const argsIndex of argsIndices ) {
|
||||||
const details = argsMap.get(argsHash);
|
const details = argsList[argsIndex];
|
||||||
if ( details.n && details.n.includes(hn) ) { continue; }
|
if ( details.n && details.n.includes(hn) ) { continue; }
|
||||||
try { scriptlet(...details.a); } catch(ex) {}
|
try { scriptlet(...details.a); } catch(ex) {}
|
||||||
}
|
}
|
||||||
|
@ -146,9 +147,7 @@ while ( hn ) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/******************************************************************************/
|
argsList.length = 0;
|
||||||
|
|
||||||
argsMap.clear();
|
|
||||||
hostnamesMap.clear();
|
hostnamesMap.clear();
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
|
|
||||||
// $rulesetId$
|
// $rulesetId$
|
||||||
|
|
||||||
const argsMap = new Map(self.$argsMap$);
|
const argsList = self.$argsList$;
|
||||||
|
|
||||||
const hostnamesMap = new Map(self.$hostnamesMap$);
|
const hostnamesMap = new Map(self.$hostnamesMap$);
|
||||||
|
|
||||||
|
@ -175,10 +175,10 @@ let hn;
|
||||||
try { hn = document.location.hostname; } catch(ex) { }
|
try { hn = document.location.hostname; } catch(ex) { }
|
||||||
while ( hn ) {
|
while ( hn ) {
|
||||||
if ( hostnamesMap.has(hn) ) {
|
if ( hostnamesMap.has(hn) ) {
|
||||||
let argsHashes = hostnamesMap.get(hn);
|
let argsIndices = hostnamesMap.get(hn);
|
||||||
if ( typeof argsHashes === 'number' ) { argsHashes = [ argsHashes ]; }
|
if ( typeof argsIndices === 'number' ) { argsIndices = [ argsIndices ]; }
|
||||||
for ( const argsHash of argsHashes ) {
|
for ( const argsIndex of argsIndices ) {
|
||||||
const details = argsMap.get(argsHash);
|
const details = argsList[argsIndex];
|
||||||
if ( details.n && details.n.includes(hn) ) { continue; }
|
if ( details.n && details.n.includes(hn) ) { continue; }
|
||||||
try { scriptlet(...details.a); } catch(ex) {}
|
try { scriptlet(...details.a); } catch(ex) {}
|
||||||
}
|
}
|
||||||
|
@ -192,9 +192,7 @@ while ( hn ) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/******************************************************************************/
|
argsList.length = 0;
|
||||||
|
|
||||||
argsMap.clear();
|
|
||||||
hostnamesMap.clear();
|
hostnamesMap.clear();
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
|
@ -36,7 +36,7 @@ cp LICENSE.txt $DES/
|
||||||
echo "*** uBOLite.mv3: Copying mv3-specific files"
|
echo "*** uBOLite.mv3: Copying mv3-specific files"
|
||||||
cp platform/mv3/extension/*.html $DES/
|
cp platform/mv3/extension/*.html $DES/
|
||||||
cp platform/mv3/extension/css/* $DES/css/
|
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 platform/mv3/extension/img/* $DES/img/
|
||||||
cp -R platform/mv3/extension/_locales $DES/
|
cp -R platform/mv3/extension/_locales $DES/
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue