mirror of
https://github.com/gorhill/uBlock.git
synced 2026-03-11 09:04:36 +00:00
303 lines
10 KiB
JavaScript
303 lines
10 KiB
JavaScript
/*******************************************************************************
|
|
|
|
uBlock Origin Lite - a comprehensive, MV3-compliant content blocker
|
|
Copyright (C) 2025-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
|
|
*/
|
|
|
|
(async ( ) => {
|
|
|
|
/******************************************************************************/
|
|
|
|
const ubolOverlay = self.ubolOverlay;
|
|
if ( ubolOverlay === undefined ) { return; }
|
|
if ( ubolOverlay.file === '/picker-ui.html' ) { return; }
|
|
|
|
/******************************************************************************/
|
|
|
|
function attributeNameFromPart(part) {
|
|
const pos = part.search(/\^?=/);
|
|
return part.slice(1, pos);
|
|
}
|
|
|
|
function selectorFromAddresses(partsDB, addresses) {
|
|
const selector = [];
|
|
let majorLast = -1;
|
|
for ( const address of addresses ) {
|
|
const major = address >>> 12;
|
|
if ( majorLast !== -1 ) {
|
|
const delta = majorLast - major;
|
|
if ( delta > 1 ) {
|
|
selector.push(' ');
|
|
} else if ( delta === 1 ) {
|
|
selector.push(' > ');
|
|
}
|
|
}
|
|
majorLast = major;
|
|
const part = partsDB.get(address);
|
|
selector.push(
|
|
(address & 0xF) === 3
|
|
? `[${attributeNameFromPart(part)}]`
|
|
: part
|
|
);
|
|
}
|
|
return selector.join('');
|
|
}
|
|
|
|
/*******************************************************************************
|
|
*
|
|
* Selector part address:
|
|
* 0b00000000_00000000_0000
|
|
* | | |
|
|
* | | +-- 4-bit: Descriptor
|
|
* | +------- 8-bit: Part index
|
|
* +---------------- 8-bit: List index
|
|
* Descriptor:
|
|
* - 0: tag name
|
|
* - 1: id
|
|
* - 2: class
|
|
* - 3: attribute
|
|
* - 4: :nth-of-type
|
|
* List index: 0 is deepest
|
|
*
|
|
* Selector part addresses are used to reference parts in associated database.
|
|
*
|
|
* */
|
|
|
|
function candidatesAtPoint(x, y) {
|
|
// We need at least one element.
|
|
let elem = null;
|
|
if ( typeof x === 'number' ) {
|
|
elem = ubolOverlay.elementFromPoint(x, y);
|
|
} else if ( x instanceof HTMLElement ) {
|
|
elem = x;
|
|
x = undefined;
|
|
}
|
|
|
|
const partsDB = new Map();
|
|
const listParts = [];
|
|
while ( elem && elem !== document.body ) {
|
|
const tagName = elem.localName;
|
|
const addressMajor = listParts.length << 12;
|
|
partsDB.set(addressMajor, CSS.escape(tagName));
|
|
const parts = [ addressMajor ];
|
|
// Id
|
|
if ( typeof elem.id === 'string' && elem.id !== '' ) {
|
|
const address = addressMajor | parts.length << 4 | 1;
|
|
partsDB.set(address, `#${CSS.escape(elem.id)}`);
|
|
parts.push(address);
|
|
}
|
|
// Classes
|
|
for ( const name of elem.classList.values() ) {
|
|
const address = addressMajor | parts.length << 4 | 2;
|
|
partsDB.set(address, `.${CSS.escape(name)}`);
|
|
parts.push(address);
|
|
}
|
|
// Attributes
|
|
for ( const name of elem.getAttributeNames() ) {
|
|
if ( name === 'id' || name === 'class' ) { continue; }
|
|
if ( excludedAttributeExpansion.includes(name) ) {
|
|
const address = addressMajor | parts.length << 4 | 3;
|
|
partsDB.set(address, `[${CSS.escape(name)}]`);
|
|
parts.push(address);
|
|
continue;
|
|
}
|
|
let value = elem.getAttribute(name);
|
|
const pos = value.search(/[\n\r]/);
|
|
if ( pos !== -1 ) {
|
|
value = value.slice(0, pos);
|
|
}
|
|
const address = addressMajor | parts.length << 4 | 3;
|
|
partsDB.set(address, `[${CSS.escape(name)}="${value}"]`);
|
|
parts.push(address);
|
|
}
|
|
// https://github.com/chrisaljoudi/uBlock/issues/637
|
|
// If the selector is still ambiguous at this point, further narrow
|
|
// using `:nth-of-type`.
|
|
const parentNode = elem.parentNode;
|
|
if ( ubolOverlay.qsa(parentNode, `:scope > ${selectorFromAddresses(partsDB, parts)}`).length > 1 ) {
|
|
let i = 1;
|
|
while ( elem.previousSibling !== null ) {
|
|
elem = elem.previousSibling;
|
|
if ( typeof elem.localName !== 'string' ) { continue; }
|
|
if ( elem.localName !== tagName ) { continue; }
|
|
i++;
|
|
}
|
|
const address = addressMajor | parts.length << 4 | 4;
|
|
partsDB.set(address, `:nth-of-type(${i})`);
|
|
parts.push(address);
|
|
}
|
|
listParts.push(parts);
|
|
elem = elem.parentElement;
|
|
}
|
|
if ( listParts.length === 0 ) { return; }
|
|
|
|
const sliderCandidates = [];
|
|
for ( let i = 0, n = listParts.length; i < n; i++ ) {
|
|
sliderCandidates.push(listParts[i]);
|
|
for ( let j = i + 1; j < n; j++ ) {
|
|
sliderCandidates.push([
|
|
...listParts[j],
|
|
...sliderCandidates.at(-1),
|
|
]);
|
|
}
|
|
}
|
|
const sliderMap = new Map();
|
|
for ( const candidates of sliderCandidates ) {
|
|
if ( candidates.some(a => (a & 0xF) === 1) ) {
|
|
const selectorPath = candidates.filter(a => (a & 0xF) === 1);
|
|
sliderMap.set(JSON.stringify(selectorPath), 0);
|
|
} else if ( candidates.some(a => (a & 0xF) === 4) ) {
|
|
const selectorPath = candidates.filter(a => {
|
|
return a &= 0xF, a === 0 || a === 4;
|
|
});
|
|
sliderMap.set(JSON.stringify(selectorPath), 0);
|
|
}
|
|
if ( candidates.some(a => (a & 0xF) === 2) ) {
|
|
const selectorPath = candidates.filter(a => {
|
|
return a &= 0xF, a === 0 || a === 2;
|
|
});
|
|
sliderMap.set(JSON.stringify(selectorPath), 0);
|
|
}
|
|
const selectorPath = candidates.filter(a => {
|
|
return a &= 0xF, a === 0 || a === 3;
|
|
});
|
|
sliderMap.set(JSON.stringify(selectorPath), 0);
|
|
}
|
|
sliderMap.delete('[]');
|
|
const elemToIdMap = new Map();
|
|
const resultSetMap = new Map();
|
|
let elemId = 1;
|
|
for ( const json of sliderMap.keys() ) {
|
|
const addresses = JSON.parse(json);
|
|
const selector = selectorFromAddresses(partsDB, addresses);
|
|
if ( excludedSelectors.includes(selector) ) { continue; }
|
|
const elems = ubolOverlay.qsa(document, selector);
|
|
if ( elems.length === 0 ) { continue; }
|
|
const resultSet = [];
|
|
for ( const elem of elems ) {
|
|
if ( elemToIdMap.has(elem) === false ) {
|
|
elemToIdMap.set(elem, elemId++);
|
|
}
|
|
resultSet.push(elemToIdMap.get(elem));
|
|
}
|
|
const resultSetKey = JSON.stringify(resultSet.sort());
|
|
const current = resultSetMap.get(resultSetKey);
|
|
if ( current ) {
|
|
if ( current.length < addresses.length ) { continue; }
|
|
if ( current.length === addresses.length ) {
|
|
if ( addresses.some(a => (a & 0xF) === 2) === false ) {
|
|
if ( current.some(a => (a & 0xF) === 2) ) { continue; }
|
|
}
|
|
}
|
|
}
|
|
resultSetMap.set(resultSetKey, addresses);
|
|
}
|
|
const sliderParts = Array.from(resultSetMap).toSorted((a, b) => {
|
|
let amajor = a[1].at(-1) >>> 12;
|
|
let bmajor = b[1].at(-1) >>> 12;
|
|
if ( amajor !== bmajor ) { return bmajor - amajor; }
|
|
amajor = a[1].at(0) >>> 12;
|
|
bmajor = b[1].at(0) >>> 12;
|
|
if ( amajor !== bmajor ) { return bmajor - amajor; }
|
|
if ( a[0].length !== b[0].length ) {
|
|
return b[0].length - a[0].length;
|
|
}
|
|
return b[1].length - a[1].length;
|
|
}).map(a => a[1]);
|
|
return {
|
|
partsDB: Array.from(partsDB),
|
|
listParts,
|
|
sliderParts,
|
|
};
|
|
}
|
|
|
|
const excludedAttributeExpansion = [
|
|
'sizes',
|
|
'srcset',
|
|
];
|
|
const excludedSelectors = [
|
|
'div',
|
|
'span',
|
|
];
|
|
|
|
/******************************************************************************/
|
|
|
|
async function previewSelector(selector) {
|
|
if ( selector === previewedSelector ) { return; }
|
|
if ( previewedSelector !== '' ) {
|
|
if ( previewedSelector.startsWith('{') ) {
|
|
if ( self.pickerProceduralFilteringAPI ) {
|
|
await self.pickerProceduralFilteringAPI.reset();
|
|
}
|
|
}
|
|
if ( previewedCSS !== '' ) {
|
|
await ubolOverlay.sendMessage({ what: 'removeCSS', css: previewedCSS });
|
|
previewedCSS = '';
|
|
}
|
|
}
|
|
previewedSelector = selector || '';
|
|
if ( selector === '' ) { return; }
|
|
if ( selector.startsWith('{') ) {
|
|
if ( self.ProceduralFiltererAPI === undefined ) { return; }
|
|
if ( self.pickerProceduralFilteringAPI === undefined ) {
|
|
self.pickerProceduralFilteringAPI = new self.ProceduralFiltererAPI();
|
|
}
|
|
self.pickerProceduralFilteringAPI.addSelectors([ JSON.parse(selector) ]);
|
|
return;
|
|
}
|
|
previewedCSS = `${selector}{display:none!important;}`;
|
|
await ubolOverlay.sendMessage({ what: 'insertCSS', css: previewedCSS });
|
|
}
|
|
|
|
let previewedSelector = '';
|
|
let previewedCSS = '';
|
|
|
|
/******************************************************************************/
|
|
|
|
const previewProceduralFiltererAPI = new self.ProceduralFiltererAPI();
|
|
|
|
/******************************************************************************/
|
|
|
|
function onMessage(msg) {
|
|
switch ( msg.what ) {
|
|
case 'quitTool':
|
|
previewProceduralFiltererAPI.reset();
|
|
break;
|
|
case 'startCustomFilters':
|
|
return ubolOverlay.sendMessage({ what: 'startCustomFilters' });
|
|
case 'terminateCustomFilters':
|
|
return ubolOverlay.sendMessage({ what: 'terminateCustomFilters' });
|
|
case 'candidatesAtPoint':
|
|
return candidatesAtPoint(msg.mx, msg.my, msg.broad);
|
|
case 'previewSelector':
|
|
return previewSelector(msg.selector);
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
await ubolOverlay.install('/picker-ui.html', onMessage);
|
|
|
|
/******************************************************************************/
|
|
|
|
})();
|
|
|
|
|
|
void 0;
|