import Handler from "../handler.js";
import csstree from "css-tree";
import { calculateSpecificity } from "clear-cut";
import { cleanSelector } from "../../utils/css.js";
/**
* Handler that identifies and marks elements styled with `display: none` from CSS or inline styles.
*
* @class
* @extends Handler
*/
class UndisplayedFilter extends Handler {
/**
* Creates an instance of UndisplayedFilter.
*
* @param {Object} chunker - The chunker managing document flow.
* @param {Object} polisher - The polisher managing post-processing.
* @param {Object} caller - The entity invoking the handler.
*/
constructor(chunker, polisher, caller) {
super(chunker, polisher, caller);
/**
* A map of CSS selectors to display rules (`display: none`, etc).
* @type {Object.<string, Object>}
*/
this.displayRules = {};
}
/**
* Captures display declarations during CSS parsing.
*
* @param {Object} declaration - The CSS declaration node.
* @param {Object} dItem - The declaration item in the AST.
* @param {Array} dList - The list of declarations.
* @param {Object} rule - The associated CSS rule.
*/
onDeclaration(declaration, dItem, dList, rule) {
if (declaration.property === "display") {
const selector = csstree.generate(rule.ruleNode.prelude);
const value = declaration.value.children.first().name;
selector.split(",").forEach((s) => {
this.displayRules[s] = {
value: value,
selector: s,
specificity: calculateSpecificity(s),
important: declaration.important,
};
});
}
}
/**
* Filters out or marks elements that are not meant to be displayed.
*
* @param {HTMLElement | DocumentFragment} content - The DOM content to be filtered.
*/
filter(content) {
const { matches, selectors } = this.sortDisplayedSelectors(
content,
this.displayRules,
);
// Process CSS-based display: none
for (let i = 0; i < matches.length; i++) {
const element = matches[i];
const selector = selectors[i];
const displayValue = selector[selector.length - 1].value;
if (this.removable(element) && displayValue === "none") {
element.dataset.undisplayed = "undisplayed";
}
}
// Process inline styles
const styledElements = content.querySelectorAll("[style]");
for (let i = 0; i < styledElements.length; i++) {
const element = styledElements[i];
if (this.removable(element)) {
element.dataset.undisplayed = "undisplayed";
}
}
}
/**
* Sorts display rules based on `!important` and specificity, used for resolving conflicts.
*
* @private
* @param {Object} a - First display rule.
* @param {Object} b - Second display rule.
* @returns {number} Sort order.
*/
sorter(a, b) {
if (a.important && !b.important) {
return 1;
}
if (b.important && !a.important) {
return -1;
}
return a.specificity - b.specificity;
}
/**
* Matches display rules against elements and sorts them by specificity and importance.
*
* @param {HTMLElement | DocumentFragment} content - The DOM content to search.
* @param {Object.<string, Object>} displayRules - CSS display rules to apply.
* @returns {{ matches: HTMLElement[], selectors: Object[][] }} Matched elements and their rules.
*/
sortDisplayedSelectors(content, displayRules = {}) {
let matches = [];
let selectors = [];
for (let d in displayRules) {
const displayItem = displayRules[d];
const selector = displayItem.selector;
let query = [];
try {
try {
query = content.querySelectorAll(selector);
} catch (e) {
query = content.querySelectorAll(cleanSelector(selector));
}
} catch (e) {
query = [];
}
const elements = Array.from(query);
for (let e of elements) {
const index = matches.indexOf(e);
if (index !== -1) {
selectors[index].push(displayItem);
selectors[index] = selectors[index].sort(this.sorter);
} else {
matches.push(e);
selectors.push([displayItem]);
}
}
}
return { matches, selectors };
}
/**
* Determines whether an element is removable based on its inline display style.
*
* @param {HTMLElement} element - The element to check.
* @returns {boolean} True if the element is considered removable.
*/
removable(element) {
if (
element.style &&
element.style.display !== "" &&
element.style.display !== "none"
) {
return false;
}
return true;
}
}
export default UndisplayedFilter;