Skip to content
Open
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
84 changes: 72 additions & 12 deletions src/removals/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,70 @@ import { DebugRemoval } from '../types';
import { textPreview, logDebug } from '../utils';
import { getClassName, hasResponsiveShowClass } from '../utils/dom';

const HERO_PARTIAL_SELECTOR = 'hero[_\\-a-z]';
const HERO_PARTIAL_SELECTOR_REGEX = new RegExp(HERO_PARTIAL_SELECTOR, 'i');
const NON_HERO_PARTIAL_SELECTOR_REGEXES = PARTIAL_SELECTORS
.filter(pattern => pattern !== HERO_PARTIAL_SELECTOR)
.map(pattern => new RegExp(pattern, 'i'));
const CAPTION_CLASS_REGEX = /caption|credit/i;

function getSelectorAttrs(el: Element): string {
const tag = el.tagName;
const isHeading = /^H[1-6]$/.test(tag);
return (isHeading
? getClassName(el)
: getClassName(el) + ' ' +
(el.id || '') + ' ' +
(el.getAttribute('data-component') || '') + ' ' +
(el.getAttribute('data-test') || '') + ' ' +
(el.getAttribute('data-testid') || '') + ' ' +
(el.getAttribute('data-test-id') || '') + ' ' +
(el.getAttribute('data-qa') || '') + ' ' +
(el.getAttribute('data-cy') || '')
).toLowerCase();
}

function hasCaptionClass(el: Element): boolean {
return CAPTION_CLASS_REGEX.test(getClassName(el));
}

function hasImageWithCaptionClass(el: Element): boolean {
if (!el.querySelector('img')) {
return false;
}
return Array.from(el.querySelectorAll('[class]')).some(hasCaptionClass);
}

function hasCaptionedFigure(el: Element): boolean {
const figure = el.tagName === 'FIGURE' ? el : el.closest('figure');
if (figure?.querySelector('figcaption')) {
return true;
}
return Array.from(el.querySelectorAll('figure')).some(figureEl => !!figureEl.querySelector('figcaption'));
}

function hasHeroMediaWithCaption(el: Element): boolean {
if (hasCaptionedFigure(el) || hasImageWithCaptionClass(el)) {
return true;
}

let ancestor = el.parentElement;
while (ancestor) {
if (
HERO_PARTIAL_SELECTOR_REGEX.test(getSelectorAttrs(ancestor)) &&
(hasCaptionedFigure(ancestor) || hasImageWithCaptionClass(ancestor))
) {
return true;
}
ancestor = ancestor.parentElement;
}
return false;
}

function hasNonHeroPartialSelector(attrs: string): boolean {
return NON_HERO_PARTIAL_SELECTOR_REGEXES.some(regex => regex.test(attrs));
}

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;
Expand Down Expand Up @@ -83,18 +147,7 @@ export function removeBySelector(doc: Document, debug: boolean, removeExact: boo
// (Hardcoded to match TEST_ATTRIBUTES in constants.ts — avoids array allocation per element)
// For headings, only check class — IDs are auto-slugs and data-testid
// values (e.g. "article-header") cause false positives.
const isHeading = /^H[1-6]$/.test(tag);
const attrs = (isHeading
? getClassName(el)
: getClassName(el) + ' ' +
(el.id || '') + ' ' +
(el.getAttribute('data-component') || '') + ' ' +
(el.getAttribute('data-test') || '') + ' ' +
(el.getAttribute('data-testid') || '') + ' ' +
(el.getAttribute('data-test-id') || '') + ' ' +
(el.getAttribute('data-qa') || '') + ' ' +
(el.getAttribute('data-cy') || '')
).toLowerCase();
const attrs = getSelectorAttrs(el);

// Skip if no attributes to check
if (!attrs.trim()) {
Expand All @@ -106,6 +159,13 @@ export function removeBySelector(doc: Document, debug: boolean, removeExact: boo
const matchedPattern = individualRegexes
? individualRegexes.find(r => r.regex.test(attrs))?.pattern
: undefined;
if (
HERO_PARTIAL_SELECTOR_REGEX.test(attrs) &&
hasHeroMediaWithCaption(el) &&
!hasNonHeroPartialSelector(attrs)
) {
return;
}
elementsToRemove.set(el, { type: 'partial', selector: matchedPattern });
partialSelectorCount++;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
```json
{
"title": "Artificial Life in Practice",
"author": "",
"site": "",
"published": ""
}
```

![Artificial organisms moving through a simulated environment](https://cdn.example.com/images/artificial-life-hero.jpg)

Artificial organisms move through a simulated environment. Credit: Example Lab.

Artificial life research studies simple systems that produce complex behavior over time.

Researchers use these models to compare how local rules can create lifelike global patterns.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<!-- {"url":"https://example.com/blog/artificial-life"} -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>Artificial Life in Practice</title>
<meta name="description" content="A short article about artificial life systems.">
<meta property="og:image" content="https://cdn.example.com/social/artificial-life-card.jpg">
</head>
<body>
<header class="site-header">
<a href="/">Example Lab</a>
</header>
<main>
<article class="blog-post-wrap">
<h1>Artificial Life in Practice</h1>
<div class="blog-hero-image-wrap">
<img class="blog-hero-image" src="https://cdn.example.com/images/artificial-life-hero.jpg" alt="Artificial organisms moving through a simulated environment">
<div class="blogherocaption">Artificial organisms move through a simulated environment. Credit: Example Lab.</div>
</div>
<div class="rich-text-element">
<p>Artificial life research studies simple systems that produce complex behavior over time.</p>
<p>Researchers use these models to compare how local rules can create lifelike global patterns.</p>
</div>
</article>
</main>
<footer class="site-footer">
<a href="/privacy">Privacy</a>
</footer>
</body>
</html>