From 17434e41e71b076baea454f3024d4bc9bcc8f921 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sat, 4 Jul 2026 00:43:58 -0700 Subject: [PATCH 1/3] fix: keep clutter selector from removing majority-content containers --- src/removals/selectors.ts | 15 ++++- ...s--283-clutter-selector-removes-content.md | 26 +++++++++ ...-283-clutter-selector-removes-content.html | 57 +++++++++++++++++++ 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 tests/expected/issues--283-clutter-selector-removes-content.md create mode 100644 tests/fixtures/issues--283-clutter-selector-removes-content.html diff --git a/src/removals/selectors.ts b/src/removals/selectors.ts index 20e62c7ce..b3136118c 100644 --- a/src/removals/selectors.ts +++ b/src/removals/selectors.ts @@ -9,13 +9,18 @@ import { FOOTNOTE_LIST_SELECTORS } from '../constants'; import { DebugRemoval } from '../types'; -import { textPreview, logDebug } from '../utils'; +import { textPreview, logDebug, normalizeText } from '../utils'; import { getClassName, hasResponsiveShowClass } from '../utils/dom'; +const MAJORITY_CONTENT_MIN_TEXT_LENGTH = 200; +const MAJORITY_CONTENT_RATIO = 0.8; + export function removeBySelector(doc: Document, debug: boolean, removeExact: boolean = true, removePartial: boolean = true, mainContent?: Element | null, debugRemovals?: DebugRemoval[], skipHiddenExactSelectors: boolean = false) { const startTime = Date.now(); let exactSelectorCount = 0; let partialSelectorCount = 0; + const mainContentTextLength = mainContent ? normalizeText(mainContent.textContent || '').length : 0; + const shouldGuardMajorityContent = mainContentTextLength >= MAJORITY_CONTENT_MIN_TEXT_LENGTH; // Track all elements to be removed, with their match type const elementsToRemove = new Map(); @@ -138,6 +143,14 @@ export function removeBySelector(doc: Document, debug: boolean, removeExact: boo if (mainContent && el.contains(mainContent)) { return; } + if ( + shouldGuardMajorityContent && + type === 'exact' && + !el.matches(HIDDEN_EXACT_SELECTOR) && + normalizeText(el.textContent || '').length >= mainContentTextLength * MAJORITY_CONTENT_RATIO + ) { + return; + } if (el.tagName === 'A' && el.closest('h1, h2, h3, h4, h5, h6')) { return; } diff --git a/tests/expected/issues--283-clutter-selector-removes-content.md b/tests/expected/issues--283-clutter-selector-removes-content.md new file mode 100644 index 000000000..95001aaec --- /dev/null +++ b/tests/expected/issues--283-clutter-selector-removes-content.md @@ -0,0 +1,26 @@ +```json +{ + "title": "Modular Supply Cart", + "author": "", + "site": "Example Catalog", + "published": "" +} +``` + +The Modular Supply Cart keeps shared equipment organized for small teams that need a predictable workflow without a dedicated storage room. + +## Description + +Each cart ships with removable bins, a labeled front panel, and locking casters for stable positioning during inventory checks. The frame is designed for common shop, studio, and classroom supplies. + +## Features + +- Adjustable shelves with three height positions +- Four removable bins for cables, tools, and consumables +- Integrated label rail for team-specific organization + +## Specifications + +- Powder-coated steel frame with recycled plastic bins +- Supports up to 120 pounds of evenly distributed equipment +- Ships flat with hardware and assembly instructions diff --git a/tests/fixtures/issues--283-clutter-selector-removes-content.html b/tests/fixtures/issues--283-clutter-selector-removes-content.html new file mode 100644 index 000000000..c007cd889 --- /dev/null +++ b/tests/fixtures/issues--283-clutter-selector-removes-content.html @@ -0,0 +1,57 @@ + + + + + + + + Modular Supply Cart | Example Catalog + + + + + +

Modular Supply Cart

+

+ The Modular Supply Cart keeps shared equipment organized for small teams that need + a predictable workflow without a dedicated storage room. +

+ +

Description

+

+ Each cart ships with removable bins, a labeled front panel, and locking casters for + stable positioning during inventory checks. The frame is designed for common shop, + studio, and classroom supplies. +

+ +

Features

+ + +

Specifications

+ +
+ + + + + + From c2594a5172b235e80ef1357f0d6b4741e3817a10 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sat, 4 Jul 2026 00:50:28 -0700 Subject: [PATCH 2/3] fix: always allow unsafe non-content tags to be removed by exact selectors --- src/removals/selectors.ts | 19 +++++++++++++++++++ ...-283-clutter-selector-removes-content.html | 1 + ...-283-clutter-selector-removes-content.html | 3 +++ 3 files changed, 23 insertions(+) create mode 100644 tests/expected/issues--283-clutter-selector-removes-content.html diff --git a/src/removals/selectors.ts b/src/removals/selectors.ts index b3136118c..07503f789 100644 --- a/src/removals/selectors.ts +++ b/src/removals/selectors.ts @@ -14,6 +14,24 @@ import { getClassName, hasResponsiveShowClass } from '../utils/dom'; const MAJORITY_CONTENT_MIN_TEXT_LENGTH = 200; const MAJORITY_CONTENT_RATIO = 0.8; +const ALWAYS_REMOVABLE_EXACT_TAGS = new Set([ + 'SCRIPT', + 'STYLE', + 'NOSCRIPT', + 'IFRAME', + 'FRAME', + 'FRAMESET', + 'OBJECT', + 'EMBED', + 'APPLET', + 'BASE', + 'META', + 'LINK' +]); + +function isAlwaysRemovableExactElement(el: Element): boolean { + return ALWAYS_REMOVABLE_EXACT_TAGS.has(el.tagName.toUpperCase()); +} export function removeBySelector(doc: Document, debug: boolean, removeExact: boolean = true, removePartial: boolean = true, mainContent?: Element | null, debugRemovals?: DebugRemoval[], skipHiddenExactSelectors: boolean = false) { const startTime = Date.now(); @@ -147,6 +165,7 @@ export function removeBySelector(doc: Document, debug: boolean, removeExact: boo shouldGuardMajorityContent && type === 'exact' && !el.matches(HIDDEN_EXACT_SELECTOR) && + !isAlwaysRemovableExactElement(el) && normalizeText(el.textContent || '').length >= mainContentTextLength * MAJORITY_CONTENT_RATIO ) { return; diff --git a/tests/expected/issues--283-clutter-selector-removes-content.html b/tests/expected/issues--283-clutter-selector-removes-content.html new file mode 100644 index 000000000..9c94cfe14 --- /dev/null +++ b/tests/expected/issues--283-clutter-selector-removes-content.html @@ -0,0 +1 @@ +

The Modular Supply Cart keeps shared equipment organized for small teams that need a predictable workflow without a dedicated storage room.

Description

Each cart ships with removable bins, a labeled front panel, and locking casters for stable positioning during inventory checks. The frame is designed for common shop, studio, and classroom supplies.

Features

Specifications

diff --git a/tests/fixtures/issues--283-clutter-selector-removes-content.html b/tests/fixtures/issues--283-clutter-selector-removes-content.html index c007cd889..67fd61c62 100644 --- a/tests/fixtures/issues--283-clutter-selector-removes-content.html +++ b/tests/fixtures/issues--283-clutter-selector-removes-content.html @@ -17,6 +17,9 @@ +

Modular Supply Cart

The Modular Supply Cart keeps shared equipment organized for small teams that need From cd8f7dd2905f22c15548f2650a3fa654a8080a47 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sat, 4 Jul 2026 00:56:15 -0700 Subject: [PATCH 3/3] fix: narrow majority-content guard to ambiguous ad-like wrappers --- src/removals/selectors.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/removals/selectors.ts b/src/removals/selectors.ts index 07503f789..d0b6a7a4d 100644 --- a/src/removals/selectors.ts +++ b/src/removals/selectors.ts @@ -14,6 +14,12 @@ import { getClassName, hasResponsiveShowClass } from '../utils/dom'; const MAJORITY_CONTENT_MIN_TEXT_LENGTH = 200; const MAJORITY_CONTENT_RATIO = 0.8; +const AMBIGUOUS_MAJORITY_EXACT_SELECTOR = [ + '[class^="ad-" i]', + '[class$="-ad" i]', + '[id^="ad-" i]', + '[id$="-ad" i]' +].join(','); const ALWAYS_REMOVABLE_EXACT_TAGS = new Set([ 'SCRIPT', 'STYLE', @@ -33,6 +39,13 @@ function isAlwaysRemovableExactElement(el: Element): boolean { return ALWAYS_REMOVABLE_EXACT_TAGS.has(el.tagName.toUpperCase()); } +function isAmbiguousMajorityExactElement(el: Element): boolean { + return ( + (el.tagName.includes('-') && el.matches('.ad:not([class*="gradient"])')) || + el.matches(AMBIGUOUS_MAJORITY_EXACT_SELECTOR) + ); +} + export function removeBySelector(doc: Document, debug: boolean, removeExact: boolean = true, removePartial: boolean = true, mainContent?: Element | null, debugRemovals?: DebugRemoval[], skipHiddenExactSelectors: boolean = false) { const startTime = Date.now(); let exactSelectorCount = 0; @@ -166,6 +179,7 @@ export function removeBySelector(doc: Document, debug: boolean, removeExact: boo type === 'exact' && !el.matches(HIDDEN_EXACT_SELECTOR) && !isAlwaysRemovableExactElement(el) && + isAmbiguousMajorityExactElement(el) && normalizeText(el.textContent || '').length >= mainContentTextLength * MAJORITY_CONTENT_RATIO ) { return;