[Code Health] Format components/cosmetic_filters/**/*.{ts,js} (#32555)
Format components/cosmetic_filters/**/*.{ts,js}
This commit is contained in:
@@ -62,7 +62,6 @@ components/brave_wallet/resources/solana_web3_script.js
|
||||
/components/playlist/
|
||||
/components/speedreader/
|
||||
/components/common/
|
||||
/components/cosmetic_filters/
|
||||
/components/definitions/
|
||||
/components/new_tab_takeover/
|
||||
/components/skus/
|
||||
|
||||
@@ -9,7 +9,10 @@
|
||||
// - for cosmetic filters work with CSS and stylesheet. That work itself
|
||||
// could call the script several times.
|
||||
|
||||
import { applyCompiledSelector, compileProceduralSelector } from './procedural_filters'
|
||||
import {
|
||||
applyCompiledSelector,
|
||||
compileProceduralSelector,
|
||||
} from './procedural_filters'
|
||||
|
||||
// Start looking for things to unhide before at most this long after
|
||||
// the backend script is up and connected (eg backgroundReady = true),
|
||||
@@ -52,9 +55,10 @@ const styleAttrMap = new Map<string, string>()
|
||||
const queriedIds = new Set<string>()
|
||||
const queriedClasses = new Set<string>()
|
||||
|
||||
const notYetQueriedElements: Array<(Element[] | NodeListOf<Element>)> = []
|
||||
const notYetQueriedElements: Array<Element[] | NodeListOf<Element>> = []
|
||||
|
||||
const classIdWithoutHtmlOrBody = '[id]:not(html):not(body),[class]:not(html):not(body)'
|
||||
const classIdWithoutHtmlOrBody =
|
||||
'[id]:not(html):not(body),[class]:not(html):not(body)'
|
||||
|
||||
// Each of these get setup once the mutation observer starts running.
|
||||
let notYetQueriedClasses: string[] = []
|
||||
@@ -74,7 +78,10 @@ CC.secondRunQueue = CC.secondRunQueue || new Set<string>()
|
||||
// more time.
|
||||
CC.finalRunQueue = CC.finalRunQueue || new Set<string>()
|
||||
CC.allQueues = CC.allQueues || [
|
||||
CC.firstRunQueue, CC.secondRunQueue, CC.finalRunQueue]
|
||||
CC.firstRunQueue,
|
||||
CC.secondRunQueue,
|
||||
CC.finalRunQueue,
|
||||
]
|
||||
CC.numQueues = CC.numQueues || CC.allQueues.length
|
||||
CC.alreadyUnhiddenSelectors = CC.alreadyUnhiddenSelectors || new Set<string>()
|
||||
CC.alreadyKnownFirstPartySubtrees =
|
||||
@@ -96,35 +103,38 @@ CC.fetchNewClassIdRulesThrottlingMs =
|
||||
*/
|
||||
const idleize = (onIdle: Function, timeout: number) => {
|
||||
let idleId: number | undefined
|
||||
return function WillRunOnIdle () {
|
||||
return function WillRunOnIdle() {
|
||||
if (idleId !== undefined) {
|
||||
return
|
||||
}
|
||||
idleId = window.requestIdleCallback(() => {
|
||||
idleId = undefined
|
||||
onIdle()
|
||||
}, { timeout })
|
||||
idleId = window.requestIdleCallback(
|
||||
() => {
|
||||
idleId = undefined
|
||||
onIdle()
|
||||
},
|
||||
{ timeout },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const isRelativeUrl = (url: string): boolean => {
|
||||
return (
|
||||
!url.startsWith('//') &&
|
||||
!url.startsWith('http://') &&
|
||||
!url.startsWith('https://')
|
||||
!url.startsWith('//')
|
||||
&& !url.startsWith('http://')
|
||||
&& !url.startsWith('https://')
|
||||
)
|
||||
}
|
||||
|
||||
const isElement = (node: Node): boolean => {
|
||||
return (node.nodeType === 1)
|
||||
return node.nodeType === 1
|
||||
}
|
||||
|
||||
const asElement = (node: Node): Element | null => {
|
||||
return isElement(node) ? node as Element : null
|
||||
return isElement(node) ? (node as Element) : null
|
||||
}
|
||||
|
||||
const isHTMLElement = (node: Node): boolean => {
|
||||
return ('innerText' in node)
|
||||
return 'innerText' in node
|
||||
}
|
||||
|
||||
// The fetchNewClassIdRules() can be called of each MutationObserver event.
|
||||
@@ -144,18 +154,14 @@ const ShouldThrottleFetchNewClassIdsRules = (): boolean => {
|
||||
const msToWait = nextFetchNewClassIdRulesCall - now
|
||||
if (msToWait > 0) {
|
||||
// Schedule the call in |msToWait| ms and return.
|
||||
fetchNewClassIdRulesTimeoutId =
|
||||
window.setTimeout(
|
||||
() => {
|
||||
fetchNewClassIdRulesTimeoutId = undefined
|
||||
fetchNewClassIdRules()
|
||||
}
|
||||
, msToWait)
|
||||
fetchNewClassIdRulesTimeoutId = window.setTimeout(() => {
|
||||
fetchNewClassIdRulesTimeoutId = undefined
|
||||
fetchNewClassIdRules()
|
||||
}, msToWait)
|
||||
return true
|
||||
}
|
||||
|
||||
nextFetchNewClassIdRulesCall =
|
||||
now + CC.fetchNewClassIdRulesThrottlingMs
|
||||
nextFetchNewClassIdRulesCall = now + CC.fetchNewClassIdRulesThrottlingMs
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -180,16 +186,20 @@ const fetchNewClassIdRules = () => {
|
||||
}
|
||||
}
|
||||
notYetQueriedElements.length = 0
|
||||
if ((!notYetQueriedClasses || notYetQueriedClasses.length === 0) &&
|
||||
(!notYetQueriedIds || notYetQueriedIds.length === 0)) {
|
||||
if (
|
||||
(!notYetQueriedClasses || notYetQueriedClasses.length === 0)
|
||||
&& (!notYetQueriedIds || notYetQueriedIds.length === 0)
|
||||
) {
|
||||
return
|
||||
}
|
||||
// Callback to c++ renderer process
|
||||
// @ts-expect-error
|
||||
cf_worker.hiddenClassIdSelectors(
|
||||
JSON.stringify({
|
||||
classes: notYetQueriedClasses, ids: notYetQueriedIds
|
||||
}))
|
||||
JSON.stringify({
|
||||
classes: notYetQueriedClasses,
|
||||
ids: notYetQueriedIds,
|
||||
}),
|
||||
)
|
||||
notYetQueriedClasses = []
|
||||
notYetQueriedIds = []
|
||||
}
|
||||
@@ -204,7 +214,7 @@ const useMutationObserver = () => {
|
||||
const observerConfig = {
|
||||
subtree: true,
|
||||
childList: true,
|
||||
attributeFilter: ['id', 'class']
|
||||
attributeFilter: ['id', 'class'],
|
||||
}
|
||||
observer.observe(document.documentElement, observerConfig)
|
||||
}
|
||||
@@ -216,11 +226,15 @@ const usePolling = (observer?: MutationObserver) => {
|
||||
}
|
||||
|
||||
const futureTimeMs = window.Date.now() + returnToMutationObserverIntervalMs
|
||||
const queryAttrsFromDocumentBound = queryAttrsFromDocument.bind(undefined,
|
||||
futureTimeMs)
|
||||
const queryAttrsFromDocumentBound = queryAttrsFromDocument.bind(
|
||||
undefined,
|
||||
futureTimeMs,
|
||||
)
|
||||
|
||||
selectorsPollingIntervalId = window.setInterval(queryAttrsFromDocumentBound,
|
||||
selectorsPollingIntervalMs)
|
||||
selectorsPollingIntervalId = window.setInterval(
|
||||
queryAttrsFromDocumentBound,
|
||||
selectorsPollingIntervalMs,
|
||||
)
|
||||
}
|
||||
|
||||
const queueAttrsFromMutations = (mutations: MutationRecord[]): number => {
|
||||
@@ -269,7 +283,10 @@ const queueAttrsFromMutations = (mutations: MutationRecord[]): number => {
|
||||
return mutationScore
|
||||
}
|
||||
|
||||
const onMutations = (mutations: MutationRecord[], observer: MutationObserver) => {
|
||||
const onMutations = (
|
||||
mutations: MutationRecord[],
|
||||
observer: MutationObserver,
|
||||
) => {
|
||||
// Callback to c++ renderer process
|
||||
// @ts-expect-error
|
||||
const eventId: number | undefined = cf_worker.onHandleMutationsBegin?.()
|
||||
@@ -297,20 +314,23 @@ const onMutations = (mutations: MutationRecord[], observer: MutationObserver) =>
|
||||
}
|
||||
|
||||
if (CC.hasProceduralActions) {
|
||||
const addedElements : Element[] = [];
|
||||
mutations.forEach(mutation =>
|
||||
mutation.addedNodes.length !== 0 && mutation.addedNodes.forEach(n => {
|
||||
if (n.nodeType === Node.ELEMENT_NODE) {
|
||||
addedElements.push(n as Element)
|
||||
const childNodes = (n as Element).querySelectorAll('*')
|
||||
childNodes.length !== 0 && childNodes.forEach(c => {
|
||||
c.nodeType === Node.ELEMENT_NODE && addedElements.push(c)
|
||||
})
|
||||
}
|
||||
})
|
||||
const addedElements: Element[] = []
|
||||
mutations.forEach(
|
||||
(mutation) =>
|
||||
mutation.addedNodes.length !== 0
|
||||
&& mutation.addedNodes.forEach((n) => {
|
||||
if (n.nodeType === Node.ELEMENT_NODE) {
|
||||
addedElements.push(n as Element)
|
||||
const childNodes = (n as Element).querySelectorAll('*')
|
||||
childNodes.length !== 0
|
||||
&& childNodes.forEach((c) => {
|
||||
c.nodeType === Node.ELEMENT_NODE && addedElements.push(c)
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
if (addedElements.length !== 0) {
|
||||
executeProceduralActions(addedElements);
|
||||
executeProceduralActions(addedElements)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,8 +351,14 @@ const isFirstPartyUrl = (url: string): boolean => {
|
||||
return cf_worker.isFirstPartyUrl(url)
|
||||
}
|
||||
|
||||
const stripChildTagsFromText = (elm: HTMLElement, tagName: string, text: string): string => {
|
||||
const childElms = Array.from(elm.getElementsByTagName(tagName)) as HTMLElement[]
|
||||
const stripChildTagsFromText = (
|
||||
elm: HTMLElement,
|
||||
tagName: string,
|
||||
text: string,
|
||||
): string => {
|
||||
const childElms = Array.from(
|
||||
elm.getElementsByTagName(tagName),
|
||||
) as HTMLElement[]
|
||||
let localText = text
|
||||
for (const anElm of childElms) {
|
||||
localText = localText.replaceAll(anElm.innerText, '')
|
||||
@@ -403,7 +429,10 @@ interface IsFirstPartyQueryResult {
|
||||
*
|
||||
* Finally, special case some ids we know are used only for third party ads.
|
||||
*/
|
||||
const isSubTreeFirstParty = (elm: Element, possibleQueryResult?: IsFirstPartyQueryResult): boolean => {
|
||||
const isSubTreeFirstParty = (
|
||||
elm: Element,
|
||||
possibleQueryResult?: IsFirstPartyQueryResult,
|
||||
): boolean => {
|
||||
let queryResult: IsFirstPartyQueryResult
|
||||
let isTopLevel: boolean
|
||||
|
||||
@@ -414,7 +443,7 @@ const isSubTreeFirstParty = (elm: Element, possibleQueryResult?: IsFirstPartyQue
|
||||
queryResult = {
|
||||
foundFirstPartyResource: false,
|
||||
foundThirdPartyResource: false,
|
||||
foundKnownThirdPartyAd: false
|
||||
foundKnownThirdPartyAd: false,
|
||||
}
|
||||
isTopLevel = true
|
||||
}
|
||||
@@ -422,9 +451,11 @@ const isSubTreeFirstParty = (elm: Element, possibleQueryResult?: IsFirstPartyQue
|
||||
if (elm.getAttribute) {
|
||||
if (elm.hasAttribute('id')) {
|
||||
const elmId = elm.getAttribute('id')!
|
||||
if (elmId.startsWith('google_ads_iframe_') ||
|
||||
elmId.startsWith('div-gpt-ad') ||
|
||||
elmId.startsWith('adfox_')) {
|
||||
if (
|
||||
elmId.startsWith('google_ads_iframe_')
|
||||
|| elmId.startsWith('div-gpt-ad')
|
||||
|| elmId.startsWith('adfox_')
|
||||
) {
|
||||
queryResult.foundKnownThirdPartyAd = true
|
||||
return false
|
||||
}
|
||||
@@ -442,8 +473,7 @@ const isSubTreeFirstParty = (elm: Element, possibleQueryResult?: IsFirstPartyQue
|
||||
|
||||
if (elm.hasAttribute('style')) {
|
||||
const elmStyle = elm.getAttribute('style')!
|
||||
if (elmStyle.includes('url(') ||
|
||||
elmStyle.includes('//')) {
|
||||
if (elmStyle.includes('url(') || elmStyle.includes('//')) {
|
||||
queryResult.foundThirdPartyResource = true
|
||||
}
|
||||
}
|
||||
@@ -477,7 +507,7 @@ const isSubTreeFirstParty = (elm: Element, possibleQueryResult?: IsFirstPartyQue
|
||||
}
|
||||
|
||||
if (!isTopLevel) {
|
||||
return (!queryResult.foundThirdPartyResource)
|
||||
return !queryResult.foundThirdPartyResource
|
||||
}
|
||||
|
||||
if (queryResult.foundThirdPartyResource) {
|
||||
@@ -492,8 +522,8 @@ const unhideSelectors = (selectors: Set<string>) => {
|
||||
}
|
||||
// Find selectors we have a rule index for
|
||||
const rulesToRemove = Array.from(selectors)
|
||||
.map(selector => CC.allSelectorsToRules.get(selector))
|
||||
.filter(i => i !== undefined)
|
||||
.map((selector) => CC.allSelectorsToRules.get(selector))
|
||||
.filter((i) => i !== undefined)
|
||||
.sort()
|
||||
.reverse()
|
||||
// Delete the rules
|
||||
@@ -520,7 +550,11 @@ const unhideSelectors = (selectors: Set<string>) => {
|
||||
}
|
||||
if (oldIdx !== i) {
|
||||
// Probably out of sync
|
||||
console.error('Cosmetic Filters: old index did not match lookup index', { selector, oldIdx, i })
|
||||
console.error('Cosmetic Filters: old index did not match lookup index', {
|
||||
selector,
|
||||
oldIdx,
|
||||
i,
|
||||
})
|
||||
}
|
||||
CC.allSelectorsToRules.set(selector, oldIdx - countAtLastHighest)
|
||||
}
|
||||
@@ -555,7 +589,10 @@ const pumpCosmeticFilterQueues = () => {
|
||||
continue
|
||||
}
|
||||
|
||||
const currentWorkLoad = Array.from(currentQueue.values()).slice(0, maxWorkSize)
|
||||
const currentWorkLoad = Array.from(currentQueue.values()).slice(
|
||||
0,
|
||||
maxWorkSize,
|
||||
)
|
||||
const comboSelector = currentWorkLoad.join(',')
|
||||
const matchingElms = document.querySelectorAll(comboSelector)
|
||||
// Will hold selectors identified by _this_ queue pumping, that were
|
||||
@@ -610,7 +647,8 @@ const pumpCosmeticFilterQueues = () => {
|
||||
for (const aUsedSelector of currentWorkLoad) {
|
||||
currentQueue.delete(aUsedSelector)
|
||||
// Don't requeue selectors we know identify first party content.
|
||||
const selectorMatchedFirstParty = newlyIdentifiedFirstPartySelectors.has(aUsedSelector)
|
||||
const selectorMatchedFirstParty =
|
||||
newlyIdentifiedFirstPartySelectors.has(aUsedSelector)
|
||||
if (nextQueue && !selectorMatchedFirstParty) {
|
||||
nextQueue.add(aUsedSelector)
|
||||
}
|
||||
@@ -635,7 +673,7 @@ const pumpCosmeticFilterQueues = () => {
|
||||
|
||||
const pumpCosmeticFilterQueuesOnIdle = idleize(
|
||||
pumpCosmeticFilterQueues,
|
||||
pumpIntervalMaxMs
|
||||
pumpIntervalMaxMs,
|
||||
)
|
||||
|
||||
const queryAttrsFromDocument = (switchToMutationObserverAtTime?: number) => {
|
||||
@@ -652,7 +690,7 @@ const queryAttrsFromDocument = (switchToMutationObserverAtTime?: number) => {
|
||||
fetchNewClassIdRules()
|
||||
}
|
||||
|
||||
if (CC.hasProceduralActions) executeProceduralActions();
|
||||
if (CC.hasProceduralActions) executeProceduralActions()
|
||||
|
||||
if (eventId) {
|
||||
// Callback to c++ renderer process
|
||||
@@ -660,8 +698,10 @@ const queryAttrsFromDocument = (switchToMutationObserverAtTime?: number) => {
|
||||
cf_worker.onQuerySelectorsEnd(eventId)
|
||||
}
|
||||
|
||||
if (switchToMutationObserverAtTime !== undefined &&
|
||||
window.Date.now() >= switchToMutationObserverAtTime) {
|
||||
if (
|
||||
switchToMutationObserverAtTime !== undefined
|
||||
&& window.Date.now() >= switchToMutationObserverAtTime
|
||||
) {
|
||||
useMutationObserver()
|
||||
}
|
||||
}
|
||||
@@ -690,19 +730,22 @@ const scheduleQueuePump = (hide1pContent: boolean, genericHide: boolean) => {
|
||||
}
|
||||
// Third / final possibility, this is this the first time this has been
|
||||
// called, in which case set up a timer and quit
|
||||
CC._startCheckingId = window.requestIdleCallback(_ => {
|
||||
CC._hasDelayOcurred = true
|
||||
if (!genericHide || CC.hasProceduralActions) {
|
||||
if (CC.firstSelectorsPollingDelayMs === undefined) {
|
||||
startObserving()
|
||||
} else {
|
||||
window.setTimeout(startObserving, CC.firstSelectorsPollingDelayMs)
|
||||
CC._startCheckingId = window.requestIdleCallback(
|
||||
(_) => {
|
||||
CC._hasDelayOcurred = true
|
||||
if (!genericHide || CC.hasProceduralActions) {
|
||||
if (CC.firstSelectorsPollingDelayMs === undefined) {
|
||||
startObserving()
|
||||
} else {
|
||||
window.setTimeout(startObserving, CC.firstSelectorsPollingDelayMs)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hide1pContent) {
|
||||
pumpCosmeticFilterQueuesOnIdle()
|
||||
}
|
||||
}, { timeout: maxTimeMSBeforeStart })
|
||||
if (!hide1pContent) {
|
||||
pumpCosmeticFilterQueuesOnIdle()
|
||||
}
|
||||
},
|
||||
{ timeout: maxTimeMSBeforeStart },
|
||||
)
|
||||
}
|
||||
|
||||
const tryScheduleQueuePump = () => {
|
||||
@@ -755,40 +798,48 @@ const executeProceduralActions = (added?: Element[]) => {
|
||||
// classList.remove(tokens...) always triggers another mutation
|
||||
// even if nothing was removed.
|
||||
if (element.classList.contains(action.arg)) {
|
||||
element.classList.remove(action.arg);
|
||||
element.classList.remove(action.arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const { selector, action } of CC.proceduralActionFilters) {
|
||||
try {
|
||||
let matchingElements: Element[] | NodeListOf<any>;
|
||||
let startOperator: number;
|
||||
let matchingElements: Element[] | NodeListOf<any>
|
||||
let startOperator: number
|
||||
|
||||
if (selector[0].type === 'css-selector' && added === undefined) {
|
||||
matchingElements = document.querySelectorAll(selector[0].arg);
|
||||
startOperator = 1;
|
||||
matchingElements = document.querySelectorAll(selector[0].arg)
|
||||
startOperator = 1
|
||||
} else if (added === undefined) {
|
||||
matchingElements = document.querySelectorAll('*');
|
||||
startOperator = 0;
|
||||
matchingElements = document.querySelectorAll('*')
|
||||
startOperator = 0
|
||||
} else {
|
||||
matchingElements = added;
|
||||
startOperator = 0;
|
||||
matchingElements = added
|
||||
startOperator = 0
|
||||
}
|
||||
|
||||
if (startOperator === selector.length) {
|
||||
// First `css-selector` was already handled, and no more elements remain
|
||||
matchingElements.forEach(elem => performAction(elem, action))
|
||||
matchingElements.forEach((elem) => performAction(elem, action))
|
||||
} else {
|
||||
const filter = compileProceduralSelector(selector.slice(startOperator));
|
||||
applyCompiledSelector(filter, matchingElements as HTMLElement[]).forEach(elem => performAction(elem, action))
|
||||
const filter = compileProceduralSelector(selector.slice(startOperator))
|
||||
applyCompiledSelector(
|
||||
filter,
|
||||
matchingElements as HTMLElement[],
|
||||
).forEach((elem) => performAction(elem, action))
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to apply filter ' + JSON.stringify(selector)
|
||||
+ ' ' + JSON.stringify(action) + ': ');
|
||||
console.error(e.message);
|
||||
console.error(e.stack);
|
||||
console.error(
|
||||
'Failed to apply filter '
|
||||
+ JSON.stringify(selector)
|
||||
+ ' '
|
||||
+ JSON.stringify(action)
|
||||
+ ': ',
|
||||
)
|
||||
console.error(e.message)
|
||||
console.error(e.stack)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (CC.hasProceduralActions) executeProceduralActions();
|
||||
if (CC.hasProceduralActions) executeProceduralActions()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,40 +19,50 @@ const api = {
|
||||
cosmeticFilterManage: () => {
|
||||
cf_worker.manageCustomFilters()
|
||||
},
|
||||
getElementPickerThemeInfo: (callback: (
|
||||
isDarkModeEnabled: boolean, bgcolor: number) => void) => {
|
||||
cf_worker.getElementPickerThemeInfo().then(
|
||||
(val: { isDarkModeEnabled: boolean; bgcolor: number }) => {
|
||||
getElementPickerThemeInfo: (
|
||||
callback: (isDarkModeEnabled: boolean, bgcolor: number) => void,
|
||||
) => {
|
||||
cf_worker
|
||||
.getElementPickerThemeInfo()
|
||||
.then((val: { isDarkModeEnabled: boolean; bgcolor: number }) => {
|
||||
callback(val.isDarkModeEnabled, val.bgcolor)
|
||||
})
|
||||
},
|
||||
getLocalizedTexts: (callback: (
|
||||
btnCreateDisabledText: string,
|
||||
btnCreateEnabledText: string,
|
||||
btnManageText: string,
|
||||
btnShowRulesBoxText: string,
|
||||
btnHideRulesBoxText: string,
|
||||
btnQuitText: string) => void) => {
|
||||
cf_worker.getLocalizedTexts().then(
|
||||
(val: {
|
||||
btnCreateDisabledText: string;
|
||||
btnCreateEnabledText: string;
|
||||
btnManageText: string;
|
||||
btnShowRulesBoxText: string;
|
||||
btnHideRulesBoxText: string;
|
||||
btnQuitText: string
|
||||
}) => {
|
||||
callback(val.btnCreateDisabledText,
|
||||
val.btnCreateEnabledText,
|
||||
val.btnManageText,
|
||||
val.btnShowRulesBoxText,
|
||||
val.btnHideRulesBoxText,
|
||||
val.btnQuitText)
|
||||
})
|
||||
getLocalizedTexts: (
|
||||
callback: (
|
||||
btnCreateDisabledText: string,
|
||||
btnCreateEnabledText: string,
|
||||
btnManageText: string,
|
||||
btnShowRulesBoxText: string,
|
||||
btnHideRulesBoxText: string,
|
||||
btnQuitText: string,
|
||||
) => void,
|
||||
) => {
|
||||
cf_worker
|
||||
.getLocalizedTexts()
|
||||
.then(
|
||||
(val: {
|
||||
btnCreateDisabledText: string
|
||||
btnCreateEnabledText: string
|
||||
btnManageText: string
|
||||
btnShowRulesBoxText: string
|
||||
btnHideRulesBoxText: string
|
||||
btnQuitText: string
|
||||
}) => {
|
||||
callback(
|
||||
val.btnCreateDisabledText,
|
||||
val.btnCreateEnabledText,
|
||||
val.btnManageText,
|
||||
val.btnShowRulesBoxText,
|
||||
val.btnHideRulesBoxText,
|
||||
val.btnQuitText,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
getPlatform: (): string => {
|
||||
return cf_worker.getPlatform()
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// When the picker is activated, it eats all pointer events and takes up the
|
||||
@@ -70,11 +80,11 @@ const elementFromFrameCoords = (x: number, y: number): Element | null => {
|
||||
}
|
||||
|
||||
enum SpecificityFlags {
|
||||
Id = (1 << 0),
|
||||
Hierarchy = (1 << 1),
|
||||
Attributes = (1 << 2),
|
||||
Class = (1 << 3),
|
||||
NthOfType = (1 << 4)
|
||||
Id = 1 << 0,
|
||||
Hierarchy = 1 << 1,
|
||||
Attributes = 1 << 2,
|
||||
Class = 1 << 3,
|
||||
NthOfType = 1 << 4,
|
||||
}
|
||||
|
||||
const mostSpecificMask = 0b11111
|
||||
@@ -83,7 +93,7 @@ enum Selector {
|
||||
Id,
|
||||
Class,
|
||||
Attributes,
|
||||
NthOfType
|
||||
NthOfType,
|
||||
}
|
||||
|
||||
interface Rule {
|
||||
@@ -112,7 +122,9 @@ class ElementSelectorBuilder {
|
||||
if (Array.isArray(rule.value) && rule.value.length === 0) {
|
||||
return
|
||||
}
|
||||
if (rule.type === Selector.Id) { this.hasId = true }
|
||||
if (rule.type === Selector.Id) {
|
||||
this.hasId = true
|
||||
}
|
||||
this.rules.push(rule)
|
||||
}
|
||||
|
||||
@@ -134,21 +146,21 @@ class ElementSelectorBuilder {
|
||||
continue
|
||||
}
|
||||
if (
|
||||
!(mask & SpecificityFlags.Attributes) &&
|
||||
rule.type === Selector.Attributes
|
||||
!(mask & SpecificityFlags.Attributes)
|
||||
&& rule.type === Selector.Attributes
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (
|
||||
!(mask & SpecificityFlags.NthOfType) &&
|
||||
rule.type === Selector.NthOfType
|
||||
!(mask & SpecificityFlags.NthOfType)
|
||||
&& rule.type === Selector.NthOfType
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (
|
||||
this.hasId &&
|
||||
mask & SpecificityFlags.Id &&
|
||||
rule.type === Selector.Class
|
||||
this.hasId
|
||||
&& mask & SpecificityFlags.Id
|
||||
&& rule.type === Selector.Class
|
||||
) {
|
||||
continue
|
||||
}
|
||||
@@ -180,7 +192,9 @@ class ElementSelectorBuilder {
|
||||
selector += `:nth-of-type(${rule.value})`
|
||||
break
|
||||
}
|
||||
default: { /* Unreachable */ }
|
||||
default: {
|
||||
/* Unreachable */
|
||||
}
|
||||
}
|
||||
}
|
||||
return selector
|
||||
@@ -193,11 +207,13 @@ const cssSelectorFromElement = (elem: Element): ElementSelectorBuilder => {
|
||||
const builder = new ElementSelectorBuilder(elem)
|
||||
|
||||
// ID
|
||||
if (elem.id.length > 0 &&
|
||||
document.querySelectorAll(`#${elem.id}`).length === 1) {
|
||||
if (
|
||||
elem.id.length > 0
|
||||
&& document.querySelectorAll(`#${elem.id}`).length === 1
|
||||
) {
|
||||
builder.addRule({
|
||||
type: Selector.Id,
|
||||
value: CSS.escape(elem.id)
|
||||
value: CSS.escape(elem.id),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -205,7 +221,7 @@ const cssSelectorFromElement = (elem: Element): ElementSelectorBuilder => {
|
||||
if (elem.classList.length > 0) {
|
||||
builder.addRule({
|
||||
type: Selector.Class,
|
||||
value: Array.from(elem.classList).map((c: string) => CSS.escape(c))
|
||||
value: Array.from(elem.classList).map((c: string) => CSS.escape(c)),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -221,7 +237,7 @@ const cssSelectorFromElement = (elem: Element): ElementSelectorBuilder => {
|
||||
if (url !== undefined && url.length > 0) {
|
||||
attributes.push({
|
||||
attr: 'href',
|
||||
value: url
|
||||
value: url,
|
||||
})
|
||||
}
|
||||
break
|
||||
@@ -231,7 +247,7 @@ const cssSelectorFromElement = (elem: Element): ElementSelectorBuilder => {
|
||||
if (url !== undefined && url.length > 0) {
|
||||
attributes.push({
|
||||
attr: 'src',
|
||||
value: url.slice(0, 256)
|
||||
value: url.slice(0, 256),
|
||||
})
|
||||
}
|
||||
break
|
||||
@@ -249,43 +265,47 @@ const cssSelectorFromElement = (elem: Element): ElementSelectorBuilder => {
|
||||
if (alttext !== undefined && alttext.length > 0) {
|
||||
attributes.push({
|
||||
attr: 'alt',
|
||||
value: alttext
|
||||
value: alttext,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
attributes.push({
|
||||
attr: 'src',
|
||||
value: data
|
||||
value: data,
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
default: { break }
|
||||
default: {
|
||||
break
|
||||
}
|
||||
}
|
||||
if (attributes.length > 0) {
|
||||
builder.addRule({
|
||||
type: Selector.Attributes,
|
||||
value: attributes
|
||||
value: attributes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const querySelectorNoExcept = (
|
||||
node: Element | null,
|
||||
selector: string
|
||||
selector: string,
|
||||
): Element[] => {
|
||||
if (node !== null) {
|
||||
try {
|
||||
const r = node.querySelectorAll(selector)
|
||||
return Array.from(r)
|
||||
} catch { /* Deliberately left empty */ }
|
||||
} catch {
|
||||
/* Deliberately left empty */
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
if (
|
||||
builder.size() === 0 ||
|
||||
querySelectorNoExcept(elem.parentElement, builder.toString()).length > 1
|
||||
builder.size() === 0
|
||||
|| querySelectorNoExcept(elem.parentElement, builder.toString()).length > 1
|
||||
) {
|
||||
builder.addTag(tag)
|
||||
if (
|
||||
@@ -294,12 +314,14 @@ const cssSelectorFromElement = (elem: Element): ElementSelectorBuilder => {
|
||||
let index = 1
|
||||
let sibling: Element | null = elem.previousElementSibling
|
||||
while (sibling !== null) {
|
||||
if (sibling.localName === tag) { index++ }
|
||||
if (sibling.localName === tag) {
|
||||
index++
|
||||
}
|
||||
sibling = sibling.previousElementSibling
|
||||
}
|
||||
builder.addRule({
|
||||
type: Selector.NthOfType,
|
||||
value: index
|
||||
value: index,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -406,14 +428,15 @@ class TargetsCollection {
|
||||
this.targets.length = 0
|
||||
elems.forEach((elem: Element) => {
|
||||
this.targets.push(new Target(elem))
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
forceRecalcCoords() {
|
||||
this.targets.forEach(t => t.forceRecalcCoords())
|
||||
this.targets.forEach((t) => t.forceRecalcCoords())
|
||||
// for case when element no longer in the DOM
|
||||
this.targets = this.targets.filter(item =>
|
||||
item.coord.height !== 0 && item.coord.width !== 0)
|
||||
this.targets = this.targets.filter(
|
||||
(item) => item.coord.height !== 0 && item.coord.width !== 0,
|
||||
)
|
||||
if (this.targets.length === 0 && this.togglePicker) {
|
||||
this.togglePicker(false)
|
||||
}
|
||||
@@ -430,12 +453,12 @@ const targetRectFromElement = (elem: Element): TargetRect => {
|
||||
x: rect.left,
|
||||
y: rect.top,
|
||||
width: rect.right - rect.left,
|
||||
height: rect.bottom - rect.top
|
||||
height: rect.bottom - rect.top,
|
||||
}
|
||||
}
|
||||
|
||||
let lastHoveredElem: HTMLElement | null = null
|
||||
const targetedElems = new TargetsCollection
|
||||
const targetedElems = new TargetsCollection()
|
||||
|
||||
const recalculateAndSendTargets = (elems: Element[] | null) => {
|
||||
if (elems) {
|
||||
@@ -459,17 +482,19 @@ const hideByCssSelector = (selector: string) => {
|
||||
}
|
||||
|
||||
interface SliderOptions {
|
||||
onChange?: (value: number) => void;
|
||||
onChange?: (value: number) => void
|
||||
}
|
||||
|
||||
interface SliderAPI {
|
||||
getValue: () => number;
|
||||
min: number;
|
||||
max: number;
|
||||
getValue: () => number
|
||||
min: number
|
||||
max: number
|
||||
}
|
||||
|
||||
const onTargetSelected = (selected: Element | null, index: number): string => {
|
||||
if (lastHoveredElem === null) { return '' }
|
||||
if (lastHoveredElem === null) {
|
||||
return ''
|
||||
}
|
||||
|
||||
let elem: Element | null = selected
|
||||
const selectorBuilders = []
|
||||
@@ -478,7 +503,7 @@ const onTargetSelected = (selected: Element | null, index: number): string => {
|
||||
0b11101, // No DOM hierarchy
|
||||
0b01011, // No nth-of-type, no attributes
|
||||
0b10011, // No attributes, no class names
|
||||
0b11111 // All selector rules (default)
|
||||
0b11111, // All selector rules (default)
|
||||
]
|
||||
const mask: number = specificityMasks[index]
|
||||
|
||||
@@ -496,8 +521,10 @@ const onTargetSelected = (selected: Element | null, index: number): string => {
|
||||
for (; i < selectorBuilders.length; i++) {
|
||||
const b = selectorBuilders[i]
|
||||
try {
|
||||
if ((mask & SpecificityFlags.Id) && b.hasId ||
|
||||
document.querySelectorAll(b.toString(mask)).length === 1) {
|
||||
if (
|
||||
(mask & SpecificityFlags.Id && b.hasId)
|
||||
|| document.querySelectorAll(b.toString(mask)).length === 1
|
||||
) {
|
||||
break
|
||||
}
|
||||
} catch {
|
||||
@@ -521,9 +548,9 @@ const elementPickerHoverCoordsChanged = (x: number, y: number) => {
|
||||
}
|
||||
|
||||
const getElementBySelector = (selector: string) => {
|
||||
let elements: Element[] | null;
|
||||
const nodeList = document.querySelectorAll(selector);
|
||||
elements = nodeList.length > 0 ? Array.from(nodeList) : null;
|
||||
let elements: Element[] | null
|
||||
const nodeList = document.querySelectorAll(selector)
|
||||
elements = nodeList.length > 0 ? Array.from(nodeList) : null
|
||||
return elements
|
||||
}
|
||||
|
||||
@@ -533,7 +560,7 @@ const elementPickerUserSelectedTarget = (specificity: number) => {
|
||||
if (selector !== '') {
|
||||
try {
|
||||
recalculateAndSendTargets(getElementBySelector(selector))
|
||||
} catch { }
|
||||
} catch {}
|
||||
}
|
||||
return {
|
||||
isValid: selector !== '',
|
||||
@@ -550,12 +577,14 @@ const elementPickerUserModifiedRule = (selector: string) => {
|
||||
if (selector.length > 0) {
|
||||
try {
|
||||
recalculateAndSendTargets(Array.from(document.querySelectorAll(selector)))
|
||||
} catch { }
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
const setShowRulesHiddenBtnState = (
|
||||
showRulesButton: HTMLElement | null, show: boolean) => {
|
||||
showRulesButton: HTMLElement | null,
|
||||
show: boolean,
|
||||
) => {
|
||||
if (!showRulesButton) return
|
||||
showRulesButton.textContent = show ? btnHideRulesBoxText : btnShowRulesBoxText
|
||||
}
|
||||
@@ -565,49 +594,50 @@ const setMinimizeState = (minimized: boolean) => {
|
||||
pickerDiv.classList.toggle('minimized', minimized)
|
||||
}
|
||||
|
||||
function initSlider(element: HTMLElement
|
||||
| null, options: SliderOptions = {}): SliderAPI | undefined {
|
||||
if (!element) return;
|
||||
function initSlider(
|
||||
element: HTMLElement | null,
|
||||
options: SliderOptions = {},
|
||||
): SliderAPI | undefined {
|
||||
if (!element) return
|
||||
|
||||
const inputElement = element as HTMLInputElement
|
||||
if (!inputElement) return;
|
||||
const inputElement = element as HTMLInputElement
|
||||
if (!inputElement) return
|
||||
|
||||
const min = parseInt(inputElement.min ?? '1', 10);
|
||||
const max = parseInt(inputElement.max ?? '4', 10);
|
||||
const min = parseInt(inputElement.min ?? '1', 10)
|
||||
const max = parseInt(inputElement.max ?? '4', 10)
|
||||
const initialValue = 4
|
||||
|
||||
inputElement.tabIndex = 0;
|
||||
inputElement.tabIndex = 0
|
||||
|
||||
let currentValue = initialValue;
|
||||
let currentValue = initialValue
|
||||
|
||||
const updateSlider = (fireEvent: boolean): number => {
|
||||
const value = parseFloat(inputElement.value)
|
||||
const currMin = parseFloat(inputElement.min)
|
||||
const currMax = parseFloat(inputElement.max)
|
||||
|
||||
const value = parseFloat(inputElement.value);
|
||||
const currMin = parseFloat(inputElement.min);
|
||||
const currMax = parseFloat(inputElement.max);
|
||||
const percentage = ((value - currMin) / (currMax - currMin)) * 100
|
||||
|
||||
const percentage = ((value - currMin) / (currMax - currMin)) * 100;
|
||||
inputElement.style.setProperty('--value', `${percentage}%`)
|
||||
|
||||
inputElement.style.setProperty('--value', `${percentage}%`);
|
||||
|
||||
currentValue = value;
|
||||
currentValue = value
|
||||
|
||||
if (fireEvent && options.onChange) {
|
||||
options.onChange(currentValue);
|
||||
options.onChange(currentValue)
|
||||
}
|
||||
return value;
|
||||
};
|
||||
return value
|
||||
}
|
||||
|
||||
inputElement.addEventListener('input', () => updateSlider(true));
|
||||
inputElement.addEventListener('input', () => updateSlider(true))
|
||||
|
||||
// Initial update
|
||||
updateSlider(false);
|
||||
updateSlider(false)
|
||||
// Return API for external control
|
||||
return {
|
||||
getValue: () => currentValue,
|
||||
min,
|
||||
max
|
||||
};
|
||||
max,
|
||||
}
|
||||
}
|
||||
|
||||
const launchElementPicker = (root: ShadowRoot) => {
|
||||
@@ -635,15 +665,15 @@ const launchElementPicker = (root: ShadowRoot) => {
|
||||
}
|
||||
const maximizeButton = root.getElementById('desktop-min-icon-container')!
|
||||
maximizeButton.addEventListener('click', () => {
|
||||
setMinimizeState(false);
|
||||
setMinimizeState(false)
|
||||
})
|
||||
|
||||
const sliderElement = root.getElementById('custom-slider');
|
||||
const sliderElement = root.getElementById('custom-slider')
|
||||
const slider = initSlider(sliderElement, {
|
||||
onChange: () => {
|
||||
dispatchSelect()
|
||||
}
|
||||
});
|
||||
},
|
||||
})
|
||||
|
||||
root.addEventListener(
|
||||
'keydown',
|
||||
@@ -658,8 +688,10 @@ const launchElementPicker = (root: ShadowRoot) => {
|
||||
)
|
||||
|
||||
const svg = root.getElementById('picker-ui')!
|
||||
if (window.matchMedia("(pointer: fine)").matches &&
|
||||
window.matchMedia("(hover: hover)").matches) {
|
||||
if (
|
||||
window.matchMedia('(pointer: fine)').matches
|
||||
&& window.matchMedia('(hover: hover)').matches
|
||||
) {
|
||||
svg.addEventListener(
|
||||
'mousemove',
|
||||
(event) => {
|
||||
@@ -692,14 +724,14 @@ const launchElementPicker = (root: ShadowRoot) => {
|
||||
|
||||
const section = root.getElementById('main-section')!
|
||||
const enableButtons = (isDisabled: boolean) => {
|
||||
const elements = root.querySelectorAll('.button');
|
||||
elements.forEach(element => {
|
||||
const elements = root.querySelectorAll('.button')
|
||||
elements.forEach((element) => {
|
||||
if (isDisabled) {
|
||||
element.classList.add('disabled')
|
||||
} else {
|
||||
element.classList.remove('disabled');
|
||||
element.classList.remove('disabled')
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
if (!isAndroid) {
|
||||
@@ -725,8 +757,7 @@ const launchElementPicker = (root: ShadowRoot) => {
|
||||
}
|
||||
const setTitleBarColor = (bgcolor: number) => {
|
||||
const section = root.host as HTMLElement
|
||||
if (section)
|
||||
{
|
||||
if (section) {
|
||||
const r = (bgcolor >> 16) & 0xff
|
||||
const g = (bgcolor >> 8) & 0xff
|
||||
const b = bgcolor & 0xff
|
||||
@@ -736,23 +767,24 @@ const launchElementPicker = (root: ShadowRoot) => {
|
||||
const retrieveTheme = () => {
|
||||
api.getElementPickerThemeInfo(
|
||||
(isDarkModeEnabled: boolean, bgcolor: number) => {
|
||||
const bgcolorMaskOut = bgcolor & 0xFFFFFF
|
||||
const bgcolorMaskOut = bgcolor & 0xffffff
|
||||
const colorHex = `#${bgcolorMaskOut.toString(16).padStart(6, '0')}`
|
||||
section.style.setProperty('--theme-background-color', colorHex)
|
||||
setTitleBarColor(bgcolor)
|
||||
dispatchSelect()
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const handleColorSchemeChange = (event: MediaQueryListEvent) => {
|
||||
retrieveTheme()
|
||||
};
|
||||
prefersDarkScheme.addEventListener('change', handleColorSchemeChange);
|
||||
}
|
||||
prefersDarkScheme.addEventListener('change', handleColorSchemeChange)
|
||||
retrieveTheme()
|
||||
|
||||
const dispatchSelect = () => {
|
||||
const { isValid, selector } = elementPickerUserSelectedTarget(
|
||||
slider?.getValue() ?? 4
|
||||
slider?.getValue() ?? 4,
|
||||
)
|
||||
|
||||
hasSelectedTarget = isValid
|
||||
@@ -763,12 +795,12 @@ const launchElementPicker = (root: ShadowRoot) => {
|
||||
const oneClickEventHandler = (event: MouseEvent | TouchEvent) => {
|
||||
let elem: Element | null = null
|
||||
|
||||
setMinimizeState(false);
|
||||
setMinimizeState(false)
|
||||
|
||||
if (event instanceof MouseEvent) {
|
||||
elem = elementFromFrameCoords(event.clientX, event.clientY)
|
||||
} else if (event instanceof TouchEvent) {
|
||||
const touch = event.touches[0];
|
||||
const touch = event.touches[0]
|
||||
elem = elementFromFrameCoords(touch.clientX, touch.clientY)
|
||||
}
|
||||
|
||||
@@ -802,15 +834,17 @@ const launchElementPicker = (root: ShadowRoot) => {
|
||||
|
||||
const manageButton = root.getElementById('btn-manage')!
|
||||
manageButton.addEventListener('click', () => {
|
||||
api.cosmeticFilterManage();
|
||||
api.cosmeticFilterManage()
|
||||
})
|
||||
|
||||
const toggleDisplay = (target: HTMLElement | null,
|
||||
trigger: HTMLElement | null) => {
|
||||
const toggleDisplay = (
|
||||
target: HTMLElement | null,
|
||||
trigger: HTMLElement | null,
|
||||
) => {
|
||||
if (!target || !trigger) {
|
||||
return
|
||||
}
|
||||
trigger.addEventListener('click', e => {
|
||||
trigger.addEventListener('click', (e) => {
|
||||
if (target.style.display !== 'block') {
|
||||
target.style.display = 'block'
|
||||
setShowRulesHiddenBtnState(trigger, true)
|
||||
@@ -830,10 +864,10 @@ const highlightElements = () => {
|
||||
const svg = shadowRoot.getElementById('picker-ui')!
|
||||
const svgMask = shadowRoot.getElementById('highlight-mask')!
|
||||
|
||||
svg.querySelectorAll('.mask').forEach(el => el.remove());
|
||||
svg.querySelectorAll('.mask').forEach((el) => el.remove())
|
||||
|
||||
const svgMaskFragment = document.createDocumentFragment();
|
||||
const svgFragment = document.createDocumentFragment();
|
||||
const svgMaskFragment = document.createDocumentFragment()
|
||||
const svgFragment = document.createDocumentFragment()
|
||||
|
||||
const createMaskElement = (): SVGRectElement => {
|
||||
const mask = document.createElementNS(NSSVG, 'rect')
|
||||
@@ -847,11 +881,11 @@ const highlightElements = () => {
|
||||
for (const target of targetedElems.targets) {
|
||||
// Add the mask to the SVG definition so the dark background is removed
|
||||
const mask = createMaskElement()
|
||||
mask.x.baseVal.value = target.coord.x;
|
||||
mask.y.baseVal.value = target.coord.y;
|
||||
mask.width.baseVal.value = target.coord.width;
|
||||
mask.height.baseVal.value = target.coord.height;
|
||||
mask.rx.baseVal.value = 10;
|
||||
mask.x.baseVal.value = target.coord.x
|
||||
mask.y.baseVal.value = target.coord.y
|
||||
mask.width.baseVal.value = target.coord.width
|
||||
mask.height.baseVal.value = target.coord.height
|
||||
mask.rx.baseVal.value = 10
|
||||
svgMaskFragment.appendChild(mask)
|
||||
|
||||
// Use the same element, but add the target class which turns the
|
||||
@@ -866,13 +900,15 @@ const highlightElements = () => {
|
||||
svg.appendChild(svgFragment)
|
||||
}
|
||||
|
||||
const localizeTextData = (root: ShadowRoot,
|
||||
const localizeTextData = (
|
||||
root: ShadowRoot,
|
||||
btnCrDisText: string,
|
||||
btnCrEnblText: string,
|
||||
btnManageText: string,
|
||||
btnShowRulesText: string,
|
||||
btnHideRulesText: string,
|
||||
btnQuitText: string) => {
|
||||
btnQuitText: string,
|
||||
) => {
|
||||
btnCreateDisabledText = btnCrDisText
|
||||
btnCreateEnabledText = btnCrEnblText
|
||||
btnShowRulesBoxText = btnShowRulesText
|
||||
@@ -900,18 +936,26 @@ if (!active) {
|
||||
isAndroid = api.getPlatform() === 'android'
|
||||
const root = attachElementPicker()
|
||||
api.getLocalizedTexts(
|
||||
(btnCreateDisabledText: string,
|
||||
(
|
||||
btnCreateDisabledText: string,
|
||||
btnCreateEnabledText: string,
|
||||
btnManageText: string,
|
||||
btnShowRulesBoxText: string,
|
||||
btnHideRulesBoxText: string,
|
||||
btnQuitText: string) => {
|
||||
localizeTextData(root, btnCreateDisabledText,
|
||||
btnCreateEnabledText, btnManageText,
|
||||
btnShowRulesBoxText, btnHideRulesBoxText,
|
||||
btnQuitText)
|
||||
btnQuitText: string,
|
||||
) => {
|
||||
localizeTextData(
|
||||
root,
|
||||
btnCreateDisabledText,
|
||||
btnCreateEnabledText,
|
||||
btnManageText,
|
||||
btnShowRulesBoxText,
|
||||
btnHideRulesBoxText,
|
||||
btnQuitText,
|
||||
)
|
||||
launchElementPicker(root)
|
||||
});
|
||||
},
|
||||
)
|
||||
} else {
|
||||
active.classList.toggle('minimized', false)
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ type OperatorResult = HTMLElement[]
|
||||
type UnboundStringFunc = (arg: string, element: HTMLElement) => OperatorResult
|
||||
type UnboundChildRuleOrStringFunc = (
|
||||
arg: string | ProceduralSelector,
|
||||
element: HTMLElement) => OperatorResult
|
||||
element: HTMLElement,
|
||||
) => OperatorResult
|
||||
type UnboundOperatorFunc = UnboundStringFunc | UnboundChildRuleOrStringFunc
|
||||
type OperatorFunc = (element: HTMLElement) => OperatorResult
|
||||
|
||||
@@ -51,7 +52,7 @@ type KeyValueMatchRules = [
|
||||
const W = window
|
||||
|
||||
const _asHTMLElement = (node: Node): HTMLElement | null => {
|
||||
return (node instanceof HTMLElement) ? node : null
|
||||
return node instanceof HTMLElement ? node : null
|
||||
}
|
||||
|
||||
const _compileRegEx = (regexText: string): RegExp => {
|
||||
@@ -78,7 +79,11 @@ const _compileRegEx = (regexText: string): RegExp => {
|
||||
//
|
||||
// If `exact` is true, then the string case it tested
|
||||
// for an exact match (the regex case is not affected).
|
||||
const _testMatches = (test: string, value: string, exact: boolean = false): boolean => {
|
||||
const _testMatches = (
|
||||
test: string,
|
||||
value: string,
|
||||
exact: boolean = false,
|
||||
): boolean => {
|
||||
if (test[0] === '/') {
|
||||
return value.match(_compileRegEx(test)) !== null
|
||||
}
|
||||
@@ -110,7 +115,7 @@ const _extractKeyFromStr = (text: string): [string, number?] => {
|
||||
let key = text
|
||||
if (isQuotedCase) {
|
||||
if (!text.endsWith('"')) {
|
||||
throw new Error(`Quoted value '${text}' does not terminate with quote`);
|
||||
throw new Error(`Quoted value '${text}' does not terminate with quote`)
|
||||
}
|
||||
key = text.slice(1, text.length - 1)
|
||||
}
|
||||
@@ -123,17 +128,21 @@ const _extractKeyFromStr = (text: string): [string, number?] => {
|
||||
return [testCaseStr, finalNeedlePosition]
|
||||
}
|
||||
|
||||
const _extractValueMatchRuleFromStr = (text: string,
|
||||
uriEncode = false,
|
||||
needlePosition = 0): TextMatchRule => {
|
||||
const _extractValueMatchRuleFromStr = (
|
||||
text: string,
|
||||
uriEncode = false,
|
||||
needlePosition = 0,
|
||||
): TextMatchRule => {
|
||||
const testCaseStr = _extractValueFromStr(text, uriEncode, needlePosition)
|
||||
const testCaseFunc = _testMatches.bind(undefined, testCaseStr)
|
||||
return testCaseFunc
|
||||
}
|
||||
|
||||
const _extractValueFromStr = (text: string,
|
||||
uriEncode = false,
|
||||
needlePosition = 0): string => {
|
||||
const _extractValueFromStr = (
|
||||
text: string,
|
||||
uriEncode = false,
|
||||
needlePosition = 0,
|
||||
): string => {
|
||||
const isQuotedCase = text[needlePosition] === '"'
|
||||
let endIndex: number
|
||||
|
||||
@@ -141,18 +150,20 @@ const _extractValueFromStr = (text: string,
|
||||
if (text.at(-1) !== '"') {
|
||||
throw new Error(
|
||||
`Unable to parse value rule from ${text}. Value rule starts with `
|
||||
+ '" but doesn\'t end with "')
|
||||
+ '" but doesn\'t end with "',
|
||||
)
|
||||
}
|
||||
needlePosition += 1
|
||||
endIndex = text.length - 1
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
endIndex = text.length
|
||||
}
|
||||
|
||||
let testCaseStr = text.slice(needlePosition, endIndex)
|
||||
if (uriEncode) {
|
||||
testCaseStr = testCaseStr.replace(/\P{ASCII}/gu, c => encodeURIComponent(c))
|
||||
testCaseStr = testCaseStr.replace(/\P{ASCII}/gu, (c) =>
|
||||
encodeURIComponent(c),
|
||||
)
|
||||
}
|
||||
|
||||
return testCaseStr
|
||||
@@ -222,27 +233,30 @@ const _nextSiblingElement = (element: HTMLElement): HTMLElement | null => {
|
||||
|
||||
const _allChildren = (element: HTMLElement): HTMLElement[] => {
|
||||
return W.Array.from(element.children)
|
||||
.map(e => _asHTMLElement(e))
|
||||
.filter(e => e !== null)
|
||||
.map((e) => _asHTMLElement(e))
|
||||
.filter((e) => e !== null)
|
||||
}
|
||||
|
||||
const _allChildrenRecursive = (element: HTMLElement): HTMLElement[] => {
|
||||
return W.Array.from(element.querySelectorAll(':scope *'))
|
||||
.map(e => _asHTMLElement(e))
|
||||
.filter(e => e !== null)
|
||||
.map((e) => _asHTMLElement(e))
|
||||
.filter((e) => e !== null)
|
||||
}
|
||||
|
||||
const _stripCssOperator = (operator: string, selector: string) => {
|
||||
if (selector[0] !== operator) {
|
||||
throw new Error(
|
||||
`Expected to find ${operator} in initial position of "${selector}`)
|
||||
`Expected to find ${operator} in initial position of "${selector}`,
|
||||
)
|
||||
}
|
||||
return selector.replace(operator, '').trimStart()
|
||||
}
|
||||
|
||||
// Implementation of ":css-selector" rule
|
||||
const operatorCssSelector = (selector: CSSSelector,
|
||||
element: HTMLElement): OperatorResult => {
|
||||
const operatorCssSelector = (
|
||||
selector: CSSSelector,
|
||||
element: HTMLElement,
|
||||
): OperatorResult => {
|
||||
const trimmedSelector = selector.trimStart()
|
||||
if (trimmedSelector.startsWith('+')) {
|
||||
const subOperator = _stripCssOperator('+', trimmedSelector)
|
||||
@@ -254,24 +268,21 @@ const operatorCssSelector = (selector: CSSSelector,
|
||||
return []
|
||||
}
|
||||
return nextSibNode.matches(subOperator) ? [nextSibNode] : []
|
||||
}
|
||||
else if (trimmedSelector.startsWith('~')) {
|
||||
} else if (trimmedSelector.startsWith('~')) {
|
||||
const subOperator = _stripCssOperator('~', trimmedSelector)
|
||||
if (subOperator === null) {
|
||||
return []
|
||||
}
|
||||
const allSiblingNodes = _allOtherSiblings(element)
|
||||
return allSiblingNodes.filter(x => x.matches(subOperator))
|
||||
}
|
||||
else if (trimmedSelector.startsWith('>')) {
|
||||
return allSiblingNodes.filter((x) => x.matches(subOperator))
|
||||
} else if (trimmedSelector.startsWith('>')) {
|
||||
const subOperator = _stripCssOperator('>', trimmedSelector)
|
||||
if (subOperator === null) {
|
||||
return []
|
||||
}
|
||||
const allChildNodes = _allChildren(element)
|
||||
return allChildNodes.filter(x => x.matches(subOperator))
|
||||
}
|
||||
else if (selector.startsWith(' ')) {
|
||||
return allChildNodes.filter((x) => x.matches(subOperator))
|
||||
} else if (selector.startsWith(' ')) {
|
||||
return Array.from(element.querySelectorAll(':scope ' + trimmedSelector))
|
||||
}
|
||||
|
||||
@@ -281,13 +292,17 @@ const operatorCssSelector = (selector: CSSSelector,
|
||||
return []
|
||||
}
|
||||
|
||||
const _hasPlainSelectorCase = (selector: CSSSelector,
|
||||
element: HTMLElement): OperatorResult => {
|
||||
const _hasPlainSelectorCase = (
|
||||
selector: CSSSelector,
|
||||
element: HTMLElement,
|
||||
): OperatorResult => {
|
||||
return element.matches(selector) ? [element] : []
|
||||
}
|
||||
|
||||
const _hasProceduralSelectorCase = (selector: ProceduralSelector,
|
||||
element: HTMLElement): OperatorResult => {
|
||||
const _hasProceduralSelectorCase = (
|
||||
selector: ProceduralSelector,
|
||||
element: HTMLElement,
|
||||
): OperatorResult => {
|
||||
const shouldBeGreedy = selector[0]?.type !== 'css-selector'
|
||||
const initElements = shouldBeGreedy
|
||||
? _allChildrenRecursive(element)
|
||||
@@ -297,49 +312,59 @@ const _hasProceduralSelectorCase = (selector: ProceduralSelector,
|
||||
}
|
||||
|
||||
// Implementation of ":has" rule
|
||||
const operatorHas = (instruction: CSSSelector | ProceduralSelector,
|
||||
element: HTMLElement): OperatorResult => {
|
||||
const operatorHas = (
|
||||
instruction: CSSSelector | ProceduralSelector,
|
||||
element: HTMLElement,
|
||||
): OperatorResult => {
|
||||
if (W.Array.isArray(instruction)) {
|
||||
return _hasProceduralSelectorCase(instruction, element)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return _hasPlainSelectorCase(instruction, element)
|
||||
}
|
||||
}
|
||||
|
||||
// Implementation of ":has-text" rule
|
||||
const operatorHasText = (instruction: string,
|
||||
element: HTMLElement): OperatorResult => {
|
||||
const operatorHasText = (
|
||||
instruction: string,
|
||||
element: HTMLElement,
|
||||
): OperatorResult => {
|
||||
const text = element.innerText
|
||||
const valueTest = _extractValueMatchRuleFromStr(instruction)
|
||||
return valueTest(text) ? [element] : []
|
||||
}
|
||||
|
||||
const _notPlainSelectorCase = (selector: CSSSelector,
|
||||
element: HTMLElement): OperatorResult => {
|
||||
const _notPlainSelectorCase = (
|
||||
selector: CSSSelector,
|
||||
element: HTMLElement,
|
||||
): OperatorResult => {
|
||||
return element.matches(selector) ? [] : [element]
|
||||
}
|
||||
|
||||
const _notProceduralSelectorCase = (selector: ProceduralSelector,
|
||||
element: HTMLElement): OperatorResult => {
|
||||
const _notProceduralSelectorCase = (
|
||||
selector: ProceduralSelector,
|
||||
element: HTMLElement,
|
||||
): OperatorResult => {
|
||||
const matches = compileAndApplyProceduralSelector(selector, [element])
|
||||
return matches.length === 0 ? [element] : []
|
||||
}
|
||||
|
||||
// Implementation of ":not" rule
|
||||
const operatorNot = (instruction: CSSSelector | ProceduralSelector,
|
||||
element: HTMLElement): OperatorResult => {
|
||||
const operatorNot = (
|
||||
instruction: CSSSelector | ProceduralSelector,
|
||||
element: HTMLElement,
|
||||
): OperatorResult => {
|
||||
if (Array.isArray(instruction)) {
|
||||
return _notProceduralSelectorCase(instruction, element)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return _notPlainSelectorCase(instruction, element)
|
||||
}
|
||||
}
|
||||
|
||||
// Implementation of ":matches-property" rule
|
||||
const operatorMatchesProperty = (instruction: string,
|
||||
element: HTMLElement): OperatorResult => {
|
||||
const operatorMatchesProperty = (
|
||||
instruction: string,
|
||||
element: HTMLElement,
|
||||
): OperatorResult => {
|
||||
const [keyTest, valueTest] = _parseKeyValueMatchRules(instruction)
|
||||
for (const [propName, propValue] of Object.entries(element)) {
|
||||
if (!keyTest(propName)) {
|
||||
@@ -354,8 +379,10 @@ const operatorMatchesProperty = (instruction: string,
|
||||
}
|
||||
|
||||
// Implementation of ":min-text-length" rule
|
||||
const operatorMinTextLength = (instruction: string,
|
||||
element: HTMLElement): OperatorResult => {
|
||||
const operatorMinTextLength = (
|
||||
instruction: string,
|
||||
element: HTMLElement,
|
||||
): OperatorResult => {
|
||||
const minLength = +instruction
|
||||
if (minLength === W.NaN) {
|
||||
throw new Error(`min-text-length: Invalid arg, ${instruction}`)
|
||||
@@ -364,15 +391,20 @@ const operatorMinTextLength = (instruction: string,
|
||||
}
|
||||
|
||||
// Implementation of ":matches-attr" rule
|
||||
const operatorMatchesAttr = (instruction: string,
|
||||
element: HTMLElement): OperatorResult => {
|
||||
const operatorMatchesAttr = (
|
||||
instruction: string,
|
||||
element: HTMLElement,
|
||||
): OperatorResult => {
|
||||
const [keyTest, valueTest] = _parseKeyValueMatchRules(instruction)
|
||||
for (const attrName of element.getAttributeNames()) {
|
||||
if (!keyTest(attrName)) {
|
||||
continue
|
||||
}
|
||||
const attrValue = element.getAttribute(attrName)
|
||||
if (attrValue === null || (valueTest !== undefined && !valueTest(attrValue))) {
|
||||
if (
|
||||
attrValue === null
|
||||
|| (valueTest !== undefined && !valueTest(attrValue))
|
||||
) {
|
||||
continue
|
||||
}
|
||||
return [element]
|
||||
@@ -381,9 +413,11 @@ const operatorMatchesAttr = (instruction: string,
|
||||
}
|
||||
|
||||
// Implementation of ":matches-css-*" rules
|
||||
const operatorMatchesCSS = (beforeOrAfter: string | null,
|
||||
cssInstruction: string,
|
||||
element: HTMLElement): OperatorResult => {
|
||||
const operatorMatchesCSS = (
|
||||
beforeOrAfter: string | null,
|
||||
cssInstruction: string,
|
||||
element: HTMLElement,
|
||||
): OperatorResult => {
|
||||
const [cssKey, expectedVal] = _parseCSSInstruction(cssInstruction)
|
||||
const elmStyle = W.getComputedStyle(element, beforeOrAfter)
|
||||
const styleValue = elmStyle.getPropertyValue(cssKey)
|
||||
@@ -402,21 +436,27 @@ const operatorMatchesCSS = (beforeOrAfter: string | null,
|
||||
}
|
||||
|
||||
// Implementation of ":matches-media" rule
|
||||
const operatorMatchesMedia = (instruction: string,
|
||||
element: HTMLElement): OperatorResult => {
|
||||
const operatorMatchesMedia = (
|
||||
instruction: string,
|
||||
element: HTMLElement,
|
||||
): OperatorResult => {
|
||||
return W.matchMedia(instruction).matches ? [element] : []
|
||||
}
|
||||
|
||||
// Implementation of ":matches-path" rule
|
||||
const operatorMatchesPath = (instruction: string,
|
||||
element: HTMLElement): OperatorResult => {
|
||||
const operatorMatchesPath = (
|
||||
instruction: string,
|
||||
element: HTMLElement,
|
||||
): OperatorResult => {
|
||||
const pathAndQuery = W.location.pathname + W.location.search
|
||||
const matchRule = _extractValueMatchRuleFromStr(instruction, true)
|
||||
return matchRule(pathAndQuery) ? [element] : []
|
||||
}
|
||||
|
||||
const _upwardIntCase = (intNeedle: NeedlePosition,
|
||||
element: HTMLElement): OperatorResult => {
|
||||
const _upwardIntCase = (
|
||||
intNeedle: NeedlePosition,
|
||||
element: HTMLElement,
|
||||
): OperatorResult => {
|
||||
if (intNeedle < 1 || intNeedle >= 256) {
|
||||
throw new Error(`upward: invalid arg, ${intNeedle}`)
|
||||
}
|
||||
@@ -427,15 +467,16 @@ const _upwardIntCase = (intNeedle: NeedlePosition,
|
||||
}
|
||||
if (currentElement === null) {
|
||||
return []
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
const htmlElement = _asHTMLElement(currentElement)
|
||||
return (htmlElement === null) ? [] : [htmlElement]
|
||||
return htmlElement === null ? [] : [htmlElement]
|
||||
}
|
||||
}
|
||||
|
||||
const _upwardProceduralSelectorCase = (selector: ProceduralSelector,
|
||||
element: HTMLElement): OperatorResult => {
|
||||
const _upwardProceduralSelectorCase = (
|
||||
selector: ProceduralSelector,
|
||||
element: HTMLElement,
|
||||
): OperatorResult => {
|
||||
const childFilter = compileProceduralSelector(selector)
|
||||
let needle: ParentNode | HTMLElement | null = element
|
||||
while (needle !== null) {
|
||||
@@ -452,8 +493,10 @@ const _upwardProceduralSelectorCase = (selector: ProceduralSelector,
|
||||
return []
|
||||
}
|
||||
|
||||
const _upwardPlainSelectorCase = (selector: CSSSelector,
|
||||
element: HTMLElement): OperatorResult => {
|
||||
const _upwardPlainSelectorCase = (
|
||||
selector: CSSSelector,
|
||||
element: HTMLElement,
|
||||
): OperatorResult => {
|
||||
let needle: ParentNode | HTMLDocument | null = element
|
||||
while (needle !== null) {
|
||||
const currentElement = _asHTMLElement(needle)
|
||||
@@ -469,25 +512,31 @@ const _upwardPlainSelectorCase = (selector: CSSSelector,
|
||||
}
|
||||
|
||||
// Implementation of ":upward" rule
|
||||
const operatorUpward = (instruction: string | ProceduralSelector,
|
||||
element: HTMLElement): OperatorResult => {
|
||||
const operatorUpward = (
|
||||
instruction: string | ProceduralSelector,
|
||||
element: HTMLElement,
|
||||
): OperatorResult => {
|
||||
if (W.Number.isInteger(+instruction)) {
|
||||
return _upwardIntCase(+instruction, element)
|
||||
}
|
||||
else if (W.Array.isArray(instruction)) {
|
||||
} else if (W.Array.isArray(instruction)) {
|
||||
return _upwardProceduralSelectorCase(instruction, element)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return _upwardPlainSelectorCase(instruction, element)
|
||||
}
|
||||
}
|
||||
|
||||
// Implementation of ":xpath" rule
|
||||
const operatorXPath = (instruction: string,
|
||||
element: HTMLElement): HTMLElement[] => {
|
||||
const result = W.document.evaluate(instruction, element, null,
|
||||
W.XPathResult.UNORDERED_NODE_ITERATOR_TYPE,
|
||||
null)
|
||||
const operatorXPath = (
|
||||
instruction: string,
|
||||
element: HTMLElement,
|
||||
): HTMLElement[] => {
|
||||
const result = W.document.evaluate(
|
||||
instruction,
|
||||
element,
|
||||
null,
|
||||
W.XPathResult.UNORDERED_NODE_ITERATOR_TYPE,
|
||||
null,
|
||||
)
|
||||
const matches: HTMLElement[] = []
|
||||
let currentNode: Node | null
|
||||
while ((currentNode = result.iterateNext())) {
|
||||
@@ -517,13 +566,17 @@ const ruleTypeToFuncMap: Record<OperatorType, UnboundOperatorFunc> = {
|
||||
'xpath': operatorXPath,
|
||||
}
|
||||
|
||||
const compileProceduralSelector = (operators: ProceduralSelector): CompiledProceduralSelector => {
|
||||
const compileProceduralSelector = (
|
||||
operators: ProceduralSelector,
|
||||
): CompiledProceduralSelector => {
|
||||
const outputOperatorList = []
|
||||
for (const operator of operators) {
|
||||
const anOperatorFunc = ruleTypeToFuncMap[operator.type]
|
||||
const args = [operator.arg]
|
||||
if (anOperatorFunc === undefined) {
|
||||
throw new Error(`Not sure what to do with operator of type ${operator.type}`)
|
||||
throw new Error(
|
||||
`Not sure what to do with operator of type ${operator.type}`,
|
||||
)
|
||||
}
|
||||
|
||||
outputOperatorList.push({
|
||||
@@ -540,13 +593,12 @@ const compileProceduralSelector = (operators: ProceduralSelector): CompiledProce
|
||||
// independent of the passed element. We use this list to optimize
|
||||
// applying each operator (i.e., we just check the first element, and then
|
||||
// accept or reject all elements in the consideration set accordingly).
|
||||
const fastPathOperatorTypes: OperatorType[] = [
|
||||
'matches-media',
|
||||
'matches-path',
|
||||
]
|
||||
const fastPathOperatorTypes: OperatorType[] = ['matches-media', 'matches-path']
|
||||
|
||||
const _determineInitNodesAndIndex = (selector: CompiledProceduralSelector,
|
||||
initNodes?: HTMLElement[]): [number, HTMLElement[]] => {
|
||||
const _determineInitNodesAndIndex = (
|
||||
selector: CompiledProceduralSelector,
|
||||
initNodes?: HTMLElement[],
|
||||
): [number, HTMLElement[]] => {
|
||||
let nodesToConsider: HTMLElement[] = []
|
||||
let index = 0
|
||||
|
||||
@@ -561,21 +613,18 @@ const _determineInitNodesAndIndex = (selector: CompiledProceduralSelector,
|
||||
|
||||
if (initNodes !== undefined) {
|
||||
nodesToConsider = W.Array.from(initNodes)
|
||||
}
|
||||
else if (firstOperatorType === 'css-selector') {
|
||||
} else if (firstOperatorType === 'css-selector') {
|
||||
const selector = firstArg as CSSSelector
|
||||
// Case two: we're considering the entire document, and the first operator
|
||||
// is a 'css-selector'. Here, we just special case using querySelectorAll
|
||||
// instead of starting with the full set of possible nodes.
|
||||
nodesToConsider = W.Array.from(W.document.querySelectorAll(selector))
|
||||
index += 1
|
||||
}
|
||||
else if (firstOperatorType === 'xpath') {
|
||||
} else if (firstOperatorType === 'xpath') {
|
||||
const xpath = firstArg as string
|
||||
nodesToConsider = operatorXPath(xpath, W.document.documentElement)
|
||||
index += 1
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// Case three: we gotta apply the first operator to the entire document.
|
||||
// Yuck but un-avoidable.
|
||||
const allNodes = W.Array.from(W.document.all)
|
||||
@@ -584,8 +633,10 @@ const _determineInitNodesAndIndex = (selector: CompiledProceduralSelector,
|
||||
return [index, nodesToConsider]
|
||||
}
|
||||
|
||||
const applyCompiledSelector = (selector: CompiledProceduralSelector,
|
||||
initNodes?: HTMLElement[]): HTMLElement[] => {
|
||||
const applyCompiledSelector = (
|
||||
selector: CompiledProceduralSelector,
|
||||
initNodes?: HTMLElement[],
|
||||
): HTMLElement[] => {
|
||||
const initState = _determineInitNodesAndIndex(selector, initNodes)
|
||||
let [index, nodesToConsider] = initState
|
||||
const numOperators = selector.length
|
||||
@@ -618,8 +669,10 @@ const applyCompiledSelector = (selector: CompiledProceduralSelector,
|
||||
return nodesToConsider
|
||||
}
|
||||
|
||||
const compileAndApplyProceduralSelector = (selector: ProceduralSelector,
|
||||
initElements: HTMLElement[]): HTMLElement[] => {
|
||||
const compileAndApplyProceduralSelector = (
|
||||
selector: ProceduralSelector,
|
||||
initElements: HTMLElement[],
|
||||
): HTMLElement[] => {
|
||||
const compiled = compileProceduralSelector(selector)
|
||||
return applyCompiledSelector(compiled, initElements)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user