Source: modules/paged-media/nth-of-type.js

import Handler from "../handler.js";
import csstree from "css-tree";
import { UUID } from "../../utils/utils.js";

/**
 * Handler for emulating pseudo-selectors like `:first-of-type`, `:last-of-type`, and `:nth-of-type`
 * by converting them into attribute-based selectors that can be used for styling after layout.
 */
class NthOfType extends Handler {
	/**
	 * Constructs the NthOfType handler.
	 * @param {Object} chunker - The chunker instance used to split content into pages.
	 * @param {Object} polisher - The polisher instance for handling styles.
	 * @param {Object} caller - The caller instance managing lifecycle hooks.
	 */
	constructor(chunker, polisher, caller) {
		super(chunker, polisher, caller);

		/** @type {CSSStyleSheet} */
		this.styleSheet = polisher.styleSheet;

		/**
		 * Map of selectors and their associated UUID and declarations.
		 * @type {Object}
		 */
		this.selectors = {};
	}

	/**
	 * Hook called during CSS rule parsing.
	 * Intercepts `:first-of-type`, `:last-of-type`, and `:nth-of-type` selectors,
	 * removes the rule, and stores the relevant declarations and a UUID for later use.
	 *
	 * @param {Object} ruleNode - The AST node representing the CSS rule.
	 * @param {Object} ruleItem - The rule item in the CSS rule list.
	 * @param {Object} rulelist - The list of all CSS rules.
	 */
	onRule(ruleNode, ruleItem, rulelist) {
		let selector = csstree.generate(ruleNode.prelude);
		if (selector.match(/:(first|last|nth)-of-type/)) {
			let declarations = csstree.generate(ruleNode.block);
			declarations = declarations.replace(/[{}]/g, "");

			let uuid = "nth-of-type-" + UUID();

			selector.split(",").forEach((s) => {
				if (!this.selectors[s]) {
					this.selectors[s] = [uuid, declarations];
				} else {
					this.selectors[s][1] = `${this.selectors[s][1]};${declarations}`;
				}
			});

			rulelist.remove(ruleItem); // Remove original pseudo selector rule
		}
	}

	/**
	 * Hook called after the entire content is parsed but before layout.
	 * Applies the transformed attribute-based selectors to relevant elements.
	 *
	 * @param {Document|HTMLElement} parsed - The parsed document or content element.
	 */
	afterParsed(parsed) {
		this.processSelectors(parsed, this.selectors);
	}

	/**
	 * Applies unique `data-nth-of-type` attributes to elements matching selectors,
	 * and dynamically inserts the converted rules into the stylesheet.
	 *
	 * @param {Document|HTMLElement} parsed - The parsed content.
	 * @param {Object} selectors - Map of selectors and their UUID + declarations.
	 */
	processSelectors(parsed, selectors) {
		for (let s in selectors) {
			let elements = parsed.querySelectorAll(s);

			for (let i = 0; i < elements.length; i++) {
				let dataNthOfType = elements[i].getAttribute("data-nth-of-type");

				if (dataNthOfType && dataNthOfType !== "") {
					// Append new UUID to existing attribute
					dataNthOfType = `${dataNthOfType},${selectors[s][0]}`;
					elements[i].setAttribute("data-nth-of-type", dataNthOfType);
				} else {
					elements[i].setAttribute("data-nth-of-type", selectors[s][0]);
				}
			}

			// Insert a new CSS rule using attribute selector
			let rule = `*[data-nth-of-type*='${selectors[s][0]}'] { ${selectors[s][1]}; }`;
			this.styleSheet.insertRule(rule, this.styleSheet.cssRules.length);
		}
	}
}

export default NthOfType;