import Handler from "../handler.js";
import csstree from "css-tree";
import {
displayedElementAfter,
displayedElementBefore,
needsPageBreak,
} from "../../utils/dom.js";
class Breaks extends Handler {
/**
* Handles CSS break properties for paged media.
* @param {Object} chunker - The chunker instance managing the pagination.
* @param {Object} polisher - The polisher instance managing CSS and styles.
* @param {Object} caller - The caller instance (optional, context info).
*/
constructor(chunker, polisher, caller) {
super(chunker, polisher, caller);
/**
* Stores break rules keyed by CSS selector.
* @type {Object.<string, Array.<Object>>}
*/
this.breaks = {};
}
/**
* Processes a CSS declaration related to breaks.
* Extracts break properties and stores them by selector.
* Removes the declaration from the CSS to avoid duplication.
*
* @param {Object} declaration - The CSS declaration node.
* @param {Object} dItem - The item in the declaration list.
* @param {Object} dList - The list of declarations.
* @param {Object} rule - The CSS rule node containing the declaration.
*/
onDeclaration(declaration, dItem, dList, rule) {
let property = declaration.property;
if (property === "page") {
let children = declaration.value.children.first();
let value = children.name;
let selector = csstree.generate(rule.ruleNode.prelude);
let name = value;
let breaker = {
property: property,
value: value,
selector: selector,
name: name,
};
selector.split(",").forEach((s) => {
if (!this.breaks[s]) {
this.breaks[s] = [breaker];
} else {
this.breaks[s].push(breaker);
}
});
dList.remove(dItem);
}
if (
property === "break-before" ||
property === "break-after" ||
property === "page-break-before" ||
property === "page-break-after"
) {
let child = declaration.value.children.first();
let value = child.name;
let selector = csstree.generate(rule.ruleNode.prelude);
// Normalize legacy page-break properties
if (property === "page-break-before") {
property = "break-before";
} else if (property === "page-break-after") {
property = "break-after";
}
let breaker = {
property: property,
value: value,
selector: selector,
};
selector.split(",").forEach((s) => {
if (!this.breaks[s]) {
this.breaks[s] = [breaker];
} else {
this.breaks[s].push(breaker);
}
});
// Remove from CSS -- break rules handled by script
dList.remove(dItem);
}
}
/**
* Called after CSS is parsed.
* Applies break rules to elements in the parsed document.
*
* @param {DocumentFragment} parsed - The parsed DOM fragment.
*/
afterParsed(parsed) {
this.processBreaks(parsed, this.breaks);
}
/**
* Applies stored break rules to matching elements.
*
* @param {DocumentFragment} parsed - The parsed DOM fragment.
* @param {Object.<string, Array.<Object>>} breaks - Break rules keyed by selectors.
*/
processBreaks(parsed, breaks) {
for (let b in breaks) {
// Find elements matching the selector
let elements = parsed.querySelectorAll(b);
for (var i = 0; i < elements.length; i++) {
for (let prop of breaks[b]) {
if (prop.property === "break-after") {
let nodeAfter = displayedElementAfter(elements[i], parsed);
elements[i].setAttribute("data-break-after", prop.value);
if (nodeAfter) {
nodeAfter.setAttribute("data-previous-break-after", prop.value);
}
} else if (prop.property === "break-before") {
let nodeBefore = displayedElementBefore(elements[i], parsed, true);
// Breaks are only allowed between siblings, not between a box and its container.
// If we cannot find a node before we should not break!
// https://drafts.csswg.org/css-break-3/#break-propagation
if (nodeBefore) {
if (
prop.value === "page" &&
needsPageBreak(elements[i], nodeBefore)
) {
// Ignore explicit page break if implicit break already needed
continue;
}
elements[i].setAttribute("data-break-before", prop.value);
nodeBefore.setAttribute("data-next-break-before", prop.value);
}
} else if (prop.property === "page") {
elements[i].setAttribute("data-page", prop.value);
let nodeAfter = displayedElementAfter(elements[i], parsed);
if (nodeAfter) {
nodeAfter.setAttribute("data-after-page", prop.value);
}
} else {
elements[i].setAttribute("data-" + prop.property, prop.value);
}
}
}
}
}
/**
* Merges new break rules into existing break rules.
*
* @param {Object.<string, Array.<Object>>} pageBreaks - Existing break rules.
* @param {Object.<string, Array.<Object>>} newBreaks - New break rules to merge.
* @returns {Object.<string, Array.<Object>>} The merged break rules.
*/
mergeBreaks(pageBreaks, newBreaks) {
for (let b in newBreaks) {
if (b in pageBreaks) {
pageBreaks[b] = pageBreaks[b].concat(newBreaks[b]);
} else {
pageBreaks[b] = newBreaks[b];
}
}
return pageBreaks;
}
/**
* Adds break-related data attributes from elements on the page to the page object.
*
* @param {Element} pageElement - The page DOM element.
* @param {Object} page - The page metadata object to update.
*/
addBreakAttributes(pageElement, page) {
let before = pageElement.querySelector("[data-break-before]");
let after = pageElement.querySelector("[data-break-after]");
let previousBreakAfter = pageElement.querySelector(
"[data-previous-break-after]",
);
if (before) {
if (before.dataset.splitFrom) {
page.splitFrom = before.dataset.splitFrom;
pageElement.setAttribute("data-split-from", before.dataset.splitFrom);
} else if (
before.dataset.breakBefore &&
before.dataset.breakBefore !== "avoid"
) {
page.breakBefore = before.dataset.breakBefore;
pageElement.setAttribute(
"data-break-before",
before.dataset.breakBefore,
);
}
}
if (after && after.dataset) {
if (after.dataset.splitTo) {
page.splitTo = after.dataset.splitTo;
pageElement.setAttribute("data-split-to", after.dataset.splitTo);
} else if (
after.dataset.breakAfter &&
after.dataset.breakAfter !== "avoid"
) {
page.breakAfter = after.dataset.breakAfter;
pageElement.setAttribute("data-break-after", after.dataset.breakAfter);
}
}
if (previousBreakAfter && previousBreakAfter.dataset) {
if (
previousBreakAfter.dataset.previousBreakAfter &&
previousBreakAfter.dataset.previousBreakAfter !== "avoid"
) {
page.previousBreakAfter = previousBreakAfter.dataset.previousBreakAfter;
}
}
}
/**
* Hook called after a page layout is finished.
* Adds break-related data attributes from page elements to the page metadata.
*
* @param {Element} pageElement - The page DOM element.
* @param {Object} page - The page metadata object.
*/
afterPageLayout(pageElement, page) {
this.addBreakAttributes(pageElement, page);
}
}
export default Breaks;