Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 35 additions & 28 deletions extension/src/lib/dom-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,51 +63,58 @@ export function isInsidePlaceholder(element: Element): boolean {
return element.closest(`.${PLACEHOLDER_CLASS}`) !== null;
}

// True if any element in the candidate list is an ancestor of `element`.
// Used to dedupe to the outermost match when multiple nested candidates all
// satisfy a rule.
function hasAncestorIn<T>(
candidate: T,
candidates: readonly T[],
getElement: (item: T) => Element,
): boolean {
const element = getElement(candidate);
return candidates.some(
(other) => other !== candidate && getElement(other).contains(element),
);
}

// True if any element in the candidate list is a descendant of `element`.
function hasDescendantIn<T>(
candidate: T,
candidates: readonly T[],
getElement: (item: T) => Element,
): boolean {
const element = getElement(candidate);
return candidates.some(
(other) => other !== candidate && element.contains(getElement(other)),
);
}

// Keep only candidates that have no candidate ancestor — the outermost
// match of each nested group. Use when hiding a wrapper should subsume its
// nested matches (prompt injection, irrelevant sections).
//
// O(C·D) where D is the maximum depth: build a Set of candidate elements
// once, then walk each candidate's parent chain checking Set membership.
// The naive `candidates.some(other.contains(element))` shape was O(C²·D),
// which matters on feeds where the same rule sees hundreds of candidates
// accumulate across a scroll session.
export function filterToOutermost<T>(
candidates: readonly T[],
getElement: (item: T) => Element = (item) => item as unknown as Element,
): T[] {
return candidates.filter((c) => !hasAncestorIn(c, candidates, getElement));
const elementSet = new Set<Element>(candidates.map(getElement));
return candidates.filter((candidate) => {
let parent = getElement(candidate).parentElement;
while (parent) {
if (elementSet.has(parent)) {
return false;
}
parent = parent.parentElement;
}
return true;
});
}

// Keep only candidates that have no candidate descendant — the innermost
// match of each nested group. Use when the urgency/scarcity/match lives on
// a small leaf inside a larger card we shouldn't black out (countdown,
// scarcity, cart-addon).
//
// Computed as the dual of filterToOutermost: for each candidate, walk up
// and mark any candidate-ancestors encountered as "has descendant." Anything
// not marked is innermost. O(C·D) total.
export function filterToInnermost<T>(
candidates: readonly T[],
getElement: (item: T) => Element = (item) => item as unknown as Element,
): T[] {
return candidates.filter((c) => !hasDescendantIn(c, candidates, getElement));
const elementSet = new Set<Element>(candidates.map(getElement));
const hasCandidateDescendant = new Set<Element>();
for (const candidate of candidates) {
let parent = getElement(candidate).parentElement;
while (parent) {
if (elementSet.has(parent)) {
hasCandidateDescendant.add(parent);
}
parent = parent.parentElement;
}
}
return candidates.filter(
(candidate) => !hasCandidateDescendant.has(getElement(candidate)),
);
}

interface FindInnermostMatchesOptions<T> {
Expand Down
27 changes: 9 additions & 18 deletions extension/src/lib/selector-hide-rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import type { URLPattern } from "urlpattern-polyfill";
import type { Rule } from "../rules/types";
import { HIDDEN_ATTR, REVEALED_ATTR } from "./dom-markers";
import { filterToOutermost } from "./dom-utils";
import { PLACEHOLDER_CLASS, replaceWithBlockPlaceholder } from "./placeholder";
import type { RuleId } from "./storage";
import { createSubtreeWatcher } from "./subtree-watcher";
Expand Down Expand Up @@ -120,23 +121,16 @@ export function createSelectorHideRule(
candidates = candidates.filter(candidateFilter);
}

// Outermost-match dedupe: build a Set once, then for each candidate walk
// parentElement up to body. O(C·D) instead of the prior O(C²)
// `candidates.some(other.contains(element))` — matters on feeds where C
// grows into the hundreds over a scroll session.
const candidateSet = new Set<HTMLElement>(candidates);
function hasCandidateAncestor(element: HTMLElement): boolean {
let parent = element.parentElement;
while (parent) {
if (candidateSet.has(parent)) {
return true;
}
parent = parent.parentElement;
}
return false;
}
// Outermost-match dedupe via the shared helper. Pre-filtering up front
// (instead of an inline ancestor check inside the loop) keeps the loop
// body focused on the placeholder/marker skips, and means only one
// implementation of "outermost" exists across the codebase.
const outermost = new Set<HTMLElement>(filterToOutermost(candidates));

for (const element of candidates) {
if (!outermost.has(element)) {
continue;
}
if (!element.isConnected) {
continue;
}
Expand All @@ -155,9 +149,6 @@ export function createSelectorHideRule(
if (element.getAttribute(HIDDEN_ATTR) === id) {
continue;
}
if (hasCandidateAncestor(element)) {
continue;
}
if (removeEntirely) {
// Don't detach: if the page renders this overlay through a framework
// that retains DOM references (React's fiber, Vue's vnode), removing
Expand Down
Loading