import { sendToBackground } from '@plasmohq/messaging'; import type { PlasmoCSConfig } from 'plasmo'; import { DEFAULT_DOMAIN_CONFIG, DEFAULT_EXTENSION_DATA } from '~utils/constants'; import { formatDomainFromURL, validateSupport } from '~utils/domain'; import type { DomainConfig, ExtensionData } from '~utils/types'; export const config: PlasmoCSConfig = { all_frames: true, matches: ['http://*/*', 'https://*/*'], run_at: 'document_start', }; class NotifiableSet extends Set { constructor(...args: any[]) { super(...args); } add(value: any): this { super.add(value); sendToBackground({ body: { value: super.size }, name: 'extension/updateBadge' }); return this; } } let { actions, exclusions, keywords, tokens }: ExtensionData = DEFAULT_EXTENSION_DATA; let domainConfig: DomainConfig = DEFAULT_DOMAIN_CONFIG; let initiallyVisible: boolean = false; const domain = formatDomainFromURL(new URL(location.href)); const log = new NotifiableSet(); const observer = new MutationObserver(mutationHandler); const options: MutationObserverInit = { childList: true, subtree: true }; const seen = new Set(); document.addEventListener('visibilitychange', setUpAfterWaitForBody); window.addEventListener('pageshow', setUpAfterWaitForBody); setUpAfterWaitForBody(); function clean(elements: readonly HTMLElement[], skipMatch?: boolean): void { let index = 0; const size = 50; function chunk() { const end = Math.min(index + size, elements.length); for (; index < end; index++) { const element = elements[index]; if (match(element, skipMatch)) { if (element instanceof HTMLDialogElement) element.close(); hide(element); log.add(`${Date.now()}`); } seen.add(element); } if (index < elements.length) { requestAnimationFrame(chunk); } } requestAnimationFrame(chunk); } function forceClean(from: HTMLElement): void { const elements = getElements(tokens.selectors, { filterEarly: true, from }); if (elements.length) { fix(); clean(elements, true); } } function hasKeyword(element: HTMLElement): boolean { return !!keywords.length && !!element.outerHTML.match(new RegExp(keywords.join('|'))); } function filterNodeEarly(node: Node, stopRecursion?: boolean): readonly HTMLElement[] { if (node.nodeType !== Node.ELEMENT_NODE || !(node instanceof HTMLElement)) { return []; } if (hasKeyword(node) && !stopRecursion) { return [node, ...[...node.children].flatMap((node) => filterNodeEarly(node, true))]; } return [node]; } function fix(): void { for (const action of actions) { const { name, property, selector } = action; if (domain.match(action.domain.replaceAll(/\*/g, '[^ ]*'))) { switch (name) { case 'click': { const element = document.querySelector(selector); if (element instanceof HTMLElement) { element.click(); log.add(name); } break; } case 'remove': { const element = document.querySelector(selector); if (element instanceof HTMLElement && property) { element.style.removeProperty(property); log.add(name); } break; } case 'reload': { window.location.reload(); break; } case 'reset': { const element = document.querySelector(selector); if (element instanceof HTMLElement && property) { element.style.setProperty(property, 'initial', 'important'); log.add(name); } break; } case 'resetAll': { const elements = getElements(selector); if (property) { elements.forEach((e) => e?.style?.setProperty(property, 'initial', 'important')); log.add(name); } break; } } } } const backdrops = getElements(tokens.backdrops); for (const backdrop of backdrops) { if (backdrop.children.length === 0 && !seen.has(backdrop)) { log.add(`${Date.now()}`); seen.add(backdrop); hide(backdrop); } } const skips = exclusions.overflows.map((x) => (x.split('.').length < 3 ? `*${x}` : x)); if (!skips.some((x) => domain.match(x.replaceAll(/\*/g, '[^ ]*')))) { for (const element of [document.body, document.documentElement]) { element?.classList.remove(...(tokens.classes ?? [])); element?.style.setProperty('position', 'initial', 'important'); element?.style.setProperty('overflow-y', 'initial', 'important'); } } const ionRouterOutlet = document.getElementsByTagName('ion-router-outlet')[0]; if (ionRouterOutlet) { // 2024-08-02: fix #644 temporarily ionRouterOutlet.removeAttribute('inert'); log.add('ion-router-outlet'); } const t4Wrapper = document.getElementsByClassName('t4-wrapper')[0]; if (t4Wrapper) { log.add('t4-wrapper'); // 2024-09-12: fix #945 temporarily t4Wrapper.removeAttribute('inert'); } } function getElements(selector: Selector, params: GetElementsParams = {}): readonly HTMLElement[] { const { filterEarly, from } = params; let result: HTMLElement[] = []; if (selector.length) { result = [ ...(from ?? document).querySelectorAll(selector as string), ] as unknown as HTMLElement[]; if (filterEarly) { result = result.flatMap((node) => filterNodeEarly(node)); } } return result; } function getElementsWithChildren( selector: Selector, params: GetElementsParams = {} ): readonly HTMLElement[] { const elements = getElements(selector, params); const elementsWithChildren = elements.flatMap((element) => [element, ...element.children]); return elementsWithChildren as unknown as readonly HTMLElement[]; } function hide(element: HTMLElement) { element.style.setProperty('clip-path', 'circle(0px)', 'important'); element.style.setProperty('display', 'none', 'important'); element.style.setProperty('height', '0px', 'important'); element.style.setProperty('overflow', 'hidden', 'important'); element.style.setProperty('transform', 'scale(0)', 'important'); } function isInViewport(element: HTMLElement): boolean { const styles = window.getComputedStyle(element); const height = window.innerHeight || document.documentElement.clientHeight; const position = element.getBoundingClientRect(); const scroll = window.scrollY; return ( position.bottom === position.top || (scroll + position.top <= scroll + height && scroll + position.bottom >= scroll) || styles.animationDuration !== '0s' || styles.transitionDuration !== '0s' ); } function match(element: HTMLElement, skipMatch?: boolean): boolean { if (!exclusions.tags.length || !tokens.selectors.length) { return false; } if (!(element instanceof HTMLElement) || !element.tagName) { return false; } if (seen.has(element)) { return false; } const tagName = element.tagName.toUpperCase(); if (exclusions.tags.includes(tagName)) { return false; } const hasAttributes = !!element.getAttributeNames().filter((x) => x !== 'data-nosnippet').length; if (!hasAttributes && !tagName.includes('-')) { forceClean(element); } // 2023-06-10: fix #113 temporarily if (element.classList.contains('chat-line__message')) { return false; } // 2024-08-03: fix #701 temporarily if (element.classList.contains('sellos')) { return false; } const isDialog = tagName === 'DIALOG' && element.getAttribute('open') === 'true'; const isFakeDialog = tagName === 'DIV' && element.className.includes('cmp'); return ( (isDialog || isFakeDialog || isInViewport(element)) && (skipMatch || element.matches(tokens.selectors as unknown as string)) ); } function mutationHandler(mutations: readonly MutationRecord[]): void { if (!domainConfig.on || !tokens.selectors.length) { return; } const nodes = mutations.flatMap((mutation) => [...mutation.addedNodes]); const elements = nodes.flatMap((node) => filterNodeEarly(node)); run({ elements }); } function run(params: RunParams = {}): void { const { containers, elements, skipMatch } = params; if (document.body?.children.length && domainConfig.on && tokens.selectors.length) { fix(); if (elements?.length) { clean(elements, skipMatch); } if (elements === undefined && containers?.length) { clean(containers.flatMap((x) => getElementsWithChildren(x, { filterEarly: true }))); } } } function runtimeMessageHandler(message: RuntimeMessage): void { switch (message.name) { case 'INCREASE_LOG_COUNT': { log.add(message.value); break; } default: break; } } async function setUp(params: SetUpParams = {}): Promise { const { data } = await sendToBackground({ name: 'database/get' }); exclusions = data?.exclusions ?? exclusions; sendToBackground({ body: { domain }, name: 'extension/updateIcon' }); if (!validateSupport(location.hostname, exclusions.domains)) { observer.disconnect(); return; } domainConfig = (await sendToBackground({ body: { domain }, name: 'domain/config' }))?.data; if (domainConfig.on) { chrome.runtime.onMessage.addListener(runtimeMessageHandler); actions = data?.actions ?? actions; keywords = data?.keywords ?? keywords; tokens = data?.tokens ?? tokens; observer.observe(document.body ?? document.documentElement, options); if (!params.skipRunFn) run({ containers: tokens.containers }); } } async function setUpAfterWaitForBody(): Promise { if (document.visibilityState === 'visible' && !initiallyVisible) { if (document.body) { initiallyVisible = true; await setUp(); return; } setTimeout(setUpAfterWaitForBody, 50); } } interface GetElementsParams { readonly filterEarly?: boolean; readonly from?: HTMLElement; } interface RunParams { readonly containers?: readonly string[]; readonly elements?: readonly HTMLElement[]; readonly skipMatch?: boolean; } interface RuntimeMessage { readonly name: string; readonly value?: string; } type Selector = string | readonly string[]; interface SetUpParams { readonly skipRunFn?: boolean; }