← Files DefuddleARCHIVED FILE

skills/defuddle-web-content/references/source/src/removals/hidden.ts

3.19 KB · Oct 5, 2026 · 18:35 UTC

↓ Download file

import { DebugRemoval } from '../types';
import { textPreview, logDebug } from '../utils';
import { hasResponsiveShowClass } from '../utils/dom';

export function removeHiddenElements(doc: Document, debug: boolean, debugRemovals?: DebugRemoval[]) {
	let count = 0;
	const elementsToRemove = new Map<Element, string>();

	// Check inline styles and CSS class-based hidden patterns.
	const hiddenStylePattern = /(?:^|;\s*)(?:display\s*:\s*none|visibility\s*:\s*hidden|opacity\s*:\s*0)(?:\s*;|\s*$)/i;

	// Only use getComputedStyle in browser environments where it's meaningful.
	// In JSDOM/linkedom without stylesheets, it's extremely slow and unreliable.
	const defaultView = doc.defaultView;
	const isBrowser = typeof window !== 'undefined' && defaultView === window;

	const allElements = doc.querySelectorAll('*');
	for (const element of allElements) {
		// Skip elements that contain math — sites like Wikipedia wrap MathML
		// in display:none spans for accessibility (the visible version is an
		// image/SVG fallback). We need to preserve these for math extraction.
		if (element.querySelector('math, [data-mathml], .katex-mathml') ||
			element.tagName.toLowerCase() === 'math') {
			continue;
		}

		// Check inline style for hidden patterns
		const style = element.getAttribute('style');
		if (style && hiddenStylePattern.test(style)) {
			const reason = style.includes('display') ? 'display:none' :
				style.includes('visibility') ? 'visibility:hidden' : 'opacity:0';
			elementsToRemove.set(element, reason);
			count++;
			continue;
		}

		// Use getComputedStyle only in real browser environments
		if (isBrowser) {
			try {
				const computedStyle = defaultView!.getComputedStyle(element);
				let reason = '';
				if (computedStyle.display === 'none') reason = 'display:none';
				else if (computedStyle.visibility === 'hidden') reason = 'visibility:hidden';
				else if (computedStyle.opacity === '0') reason = 'opacity:0';
				if (reason) {
					elementsToRemove.set(element, reason);
					count++;
					continue;
				}
			} catch (e) {}
		}

		// Detect CSS framework hidden utilities (e.g. Tailwind's "hidden",
		// "sm:hidden", "not-machine:hidden")
		const className = element.getAttribute('class') || '';
		if (className) {
			const tokens = className.split(/\s+/);
			if (hasResponsiveShowClass(className)) continue;

			for (const token of tokens) {
				// Match exact "hidden"/"invisible" and variant prefixes like
				// "sm:hidden", "lg:invisible". Skip Tailwind arbitrary variants
				// containing "[" (e.g. "[&_.class]:hidden", "group-[.state]:hidden")
				// — those are conditional state selectors, not unconditional hiding.
				const isExact = token === 'hidden' || token === 'invisible';
				const isVariant = !token.includes('[') &&
					(token.endsWith(':hidden') || token.endsWith(':invisible'));

				if (isExact || isVariant) {
					elementsToRemove.set(element, `class:${token}`);
					count++;
					break;
				}
			}
		}
	}

	// Batch remove all hidden elements
	elementsToRemove.forEach((reason, el) => {
		if (debug && debugRemovals) {
			debugRemovals.push({
				step: 'removeHiddenElements',
				reason,
				text: textPreview(el)
			});
		}
		el.remove();
	});
	logDebug(debug, 'Removed hidden elements:', count);
}

SHA-256: 6d49462df9a7e87ea7d0c54d07025934bd695157321fb2773ce13e61f96e3ef6