import Handler from "../handler.js";
import csstree from "css-tree";
import pageSizes from "../../polisher/sizes.js";
import { findElement, rebuildAncestors } from "../../utils/dom.js";
import { CSSValueToString } from "../../utils/utils.js";
/**
* A class to do all the @page conversion and update in the css.
*
* @classls
* @classdesc
*/
class AtPage extends Handler {
constructor(chunker, polisher, caller) {
super(chunker, polisher, caller);
this.pages = {};
this.width = undefined;
this.height = undefined;
this.orientation = undefined;
this.marginalia = {};
}
pageModel(selector) {
return {
selector: selector,
name: undefined,
psuedo: undefined,
nth: undefined,
marginalia: {},
width: undefined,
height: undefined,
orientation: undefined,
margin: {
top: {},
right: {},
left: {},
bottom: {},
},
padding: {
top: {},
right: {},
left: {},
bottom: {},
},
border: {
top: {},
right: {},
left: {},
bottom: {},
},
backgroundOrigin: undefined,
block: {},
marks: undefined,
notes: undefined,
added: false,
};
}
/**
* Processes a CSS `@page` rule node and integrates it into the internal `pages` model.
* Handles merging of existing page data, extracting selectors, marginalia, size, bleed,
* marks, margins, padding, and borders. Also removes the processed item from the rule list.
*
* @param {Object} node - The AST node representing the `@page` rule.
* @param {Object} item - The list item in the AST that contains the rule (used for removal).
* @param {Object} list - The parent list of rules, typically from the CSS AST (csstree.List).
*/
onAtPage(node, item, list) {
let page, marginalia;
let selector = "";
let named, psuedo, nth;
let needsMerge = false;
if (node.prelude) {
named = this.getTypeSelector(node);
psuedo = this.getPsuedoSelector(node);
nth = this.getNthSelector(node);
selector = csstree.generate(node.prelude);
} else {
selector = "*";
}
if (selector in this.pages) {
// this.pages[selector] = Object.assign(this.pages[selector], page);
// console.log("after", selector, this.pages[selector]);
// this.pages[selector].added = false;
page = this.pages[selector];
marginalia = this.replaceMarginalia(node);
needsMerge = true;
// Mark page for getting classes added again
page.added = false;
} else {
page = this.pageModel(selector);
marginalia = this.replaceMarginalia(node);
this.pages[selector] = page;
}
page.name = named;
page.psuedo = psuedo;
page.nth = nth;
if (needsMerge) {
page.marginalia = Object.assign(page.marginalia, marginalia);
} else {
page.marginalia = marginalia;
}
let notes = this.replaceNotes(node);
page.notes = notes;
let declarations = this.replaceDeclarations(node);
if (declarations.size) {
page.size = declarations.size;
page.width = declarations.size.width;
page.height = declarations.size.height;
page.orientation = declarations.size.orientation;
page.format = declarations.size.format;
}
if (declarations.bleed && declarations.bleed[0] != "auto") {
switch (declarations.bleed.length) {
case 4: // top right bottom left
page.bleed = {
top: declarations.bleed[0],
right: declarations.bleed[1],
bottom: declarations.bleed[2],
left: declarations.bleed[3],
};
break;
case 3: // top right bottom right
page.bleed = {
top: declarations.bleed[0],
right: declarations.bleed[1],
bottom: declarations.bleed[2],
left: declarations.bleed[1],
};
break;
case 2: // top right top right
page.bleed = {
top: declarations.bleed[0],
right: declarations.bleed[1],
bottom: declarations.bleed[0],
left: declarations.bleed[1],
};
break;
default:
page.bleed = {
top: declarations.bleed[0],
right: declarations.bleed[0],
bottom: declarations.bleed[0],
left: declarations.bleed[0],
};
}
}
if (declarations.marks) {
if (
!declarations.bleed ||
(declarations.bleed && declarations.bleed[0] === "auto")
) {
// Spec say 6pt, but needs more space for marks
page.bleed = {
top: { value: 6, unit: "mm" },
right: { value: 6, unit: "mm" },
bottom: { value: 6, unit: "mm" },
left: { value: 6, unit: "mm" },
};
}
page.marks = declarations.marks;
}
if (declarations.margin) {
page.margin = declarations.margin;
}
if (declarations.padding) {
page.padding = declarations.padding;
}
if (declarations.border) {
page.border = declarations.border;
}
if (declarations.marks) {
page.marks = declarations.marks;
}
if (needsMerge) {
page.block.children.appendList(node.block.children);
} else {
page.block = node.block;
}
// Remove the rule
list.remove(item);
}
/* Handled in breaks */
/*
afterParsed(parsed) {
for (let b in this.named) {
// Find elements
let elements = parsed.querySelectorAll(b);
// Add break data
for (var i = 0; i < elements.length; i++) {
elements[i].setAttribute("data-page", this.named[b]);
}
}
}
*/
/**
* Finalizes processing after the CSS AST tree has been walked.
* Applies page-level classes and, if a default `@page` rule (`*`) is marked as dirty (i.e., changed),
* it updates root-level CSS variables and emits size and page-related metadata.
*
* @param {Object} ast - The full CSS AST (typically from csstree) representing the stylesheet.
* @param {Object} sheet - The current stylesheet being processed (contextual information, optional).
*/
afterTreeWalk(ast, sheet) {
let dirtyPage = "*" in this.pages && this.pages["*"].added === false;
this.addPageClasses(this.pages, ast, sheet);
if (dirtyPage) {
let width = this.pages["*"].width;
let height = this.pages["*"].height;
let format = this.pages["*"].format;
let orientation = this.pages["*"].orientation;
let bleed = this.pages["*"].bleed;
let marks = this.pages["*"].marks;
let bleedverso = undefined;
let bleedrecto = undefined;
if (":left" in this.pages) {
bleedverso = this.pages[":left"].bleed;
}
if (":right" in this.pages) {
bleedrecto = this.pages[":right"].bleed;
}
if (width && height && (this.width !== width || this.height !== height)) {
this.width = width;
this.height = height;
this.format = format;
this.orientation = orientation;
this.addRootVars(
ast,
width,
height,
orientation,
bleed,
bleedrecto,
bleedverso,
marks,
);
this.addRootPage(
ast,
this.pages["*"].size,
bleed,
bleedrecto,
bleedverso,
);
this.emit("size", { width, height, orientation, format, bleed });
this.emit("atpages", this.pages);
}
}
}
/**
* Extracts the type selector (page name) from the `@page` rule prelude.
* For example, in `@page myPage {}`, this returns `"myPage"`.
*
* @param {Object} ast - The AST node for the `@page` rule (should contain a `prelude`).
* @returns {string|undefined} The type selector name if found, otherwise `undefined`.
*/
getTypeSelector(ast) {
// Find page name
let name;
csstree.walk(ast, {
visit: "TypeSelector",
enter: (node, item, list) => {
name = node.name;
},
});
return name;
}
/**
* Extracts a pseudo-class selector from the `@page` prelude.
* Looks for values like `:left`, `:right`, `:first`, etc., and returns the name.
* Skips `:nth` pseudo-classes (handled separately).
*
* @param {Object} ast - The AST node for the `@page` rule.
* @returns {string|undefined} The pseudo-class name if found, otherwise `undefined`.
*/
getPsuedoSelector(ast) {
// Find if it has :left & :right & :black & :first
let name;
csstree.walk(ast, {
visit: "PseudoClassSelector",
enter: (node, item, list) => {
if (node.name !== "nth") {
name = node.name;
}
},
});
return name;
}
/**
* Extracts the argument of an `:nth` pseudo-class selector, if present.
* For example, in `@page :nth(3n) {}`, this returns `"3n"`.
*
* @param {Object} ast - The AST node for the `@page` rule.
* @returns {string|undefined} The `:nth` selector argument if found, otherwise `undefined`.
*/
getNthSelector(ast) {
// Find if it has :nth
let nth;
csstree.walk(ast, {
visit: "PseudoClassSelector",
enter: (node, item, list) => {
if (node.name === "nth" && node.children) {
let raw = node.children.first();
nth = raw.value;
}
},
});
return nth;
}
/**
* Extracts and removes `@margin-*` style at-rules from the block of a `@page` rule.
* These are stored in a dictionary keyed by their normalized region names.
*
* @param {Object} ast - The AST node for the `@page` rule.
* @returns {Object} A dictionary of marginalia region names to their blocks.
*/
replaceMarginalia(ast) {
let parsed = {};
const MARGINS = [
"top-left-corner",
"top-left",
"top",
"top-center",
"top-right",
"top-right-corner",
"bottom-left-corner",
"bottom-left",
"bottom",
"bottom-center",
"bottom-right",
"bottom-right-corner",
"left-top",
"left-middle",
"left",
"left-bottom",
"top-right-corner",
"right-top",
"right-middle",
"right",
"right-bottom",
"right-right-corner",
];
csstree.walk(ast.block, {
visit: "Atrule",
enter: (node, item, list) => {
let name = node.name;
if (MARGINS.includes(name)) {
if (name === "top") {
name = "top-center";
}
if (name === "right") {
name = "right-middle";
}
if (name === "left") {
name = "left-middle";
}
if (name === "bottom") {
name = "bottom-center";
}
parsed[name] = node.block;
list.remove(item);
}
},
});
return parsed;
}
/**
* Extracts and removes `@footnote` at-rules from the block of a `@page` rule.
* Returns a dictionary of extracted footnote blocks.
*
* @param {Object} ast - The AST node for the `@page` rule.
* @returns {Object} A dictionary of note names (currently only `footnote`) to their blocks.
*/
replaceNotes(ast) {
let parsed = {};
csstree.walk(ast.block, {
visit: "Atrule",
enter: (node, item, list) => {
let name = node.name;
if (name === "footnote") {
parsed[name] = node.block;
list.remove(item);
}
},
});
return parsed;
}
/**
* Extracts and removes relevant declarations from the `@page` block such as:
* - size
* - bleed
* - marks
* - margin / margin-*
* - padding / padding-*
* - border / border-*
*
* Converts them into structured objects for internal processing.
*
* @param {Object} ast - The AST node for the `@page` rule.
* @returns {Object} A parsed object containing size, bleed, marks, margin, padding, and border properties.
*/
replaceDeclarations(ast) {
let parsed = {};
csstree.walk(ast.block, {
visit: "Declaration",
enter: (declaration, dItem, dList) => {
let prop = csstree.property(declaration.property).name;
// let value = declaration.value;
if (prop === "marks") {
parsed.marks = [];
csstree.walk(declaration, {
visit: "Identifier",
enter: (ident) => {
parsed.marks.push(ident.name);
},
});
dList.remove(dItem);
} else if (prop === "margin") {
parsed.margin = this.getMargins(declaration);
dList.remove(dItem);
} else if (prop.indexOf("margin-") === 0) {
let m = prop.substring("margin-".length);
if (!parsed.margin) {
parsed.margin = {
top: {},
right: {},
left: {},
bottom: {},
};
}
parsed.margin[m] = declaration.value.children.first();
dList.remove(dItem);
} else if (prop === "padding") {
parsed.padding = this.getPaddings(declaration.value);
dList.remove(dItem);
} else if (prop.indexOf("padding-") === 0) {
let p = prop.substring("padding-".length);
if (!parsed.padding) {
parsed.padding = {
top: {},
right: {},
left: {},
bottom: {},
};
}
parsed.padding[p] = declaration.value.children.first();
dList.remove(dItem);
} else if (prop === "border") {
if (!parsed.border) {
parsed.border = {
top: {},
right: {},
left: {},
bottom: {},
};
}
parsed.border.top = csstree.generate(declaration.value);
parsed.border.right = csstree.generate(declaration.value);
parsed.border.left = csstree.generate(declaration.value);
parsed.border.bottom = csstree.generate(declaration.value);
dList.remove(dItem);
} else if (prop.indexOf("border-") === 0) {
if (!parsed.border) {
parsed.border = {
top: {},
right: {},
left: {},
bottom: {},
};
}
let p = prop.substring("border-".length);
parsed.border[p] = csstree.generate(declaration.value);
dList.remove(dItem);
} else if (prop === "size") {
parsed.size = this.getSize(declaration);
dList.remove(dItem);
} else if (prop === "bleed") {
parsed.bleed = [];
csstree.walk(declaration, {
enter: (subNode) => {
switch (subNode.type) {
case "String": // bleed: "auto"
if (subNode.value.indexOf("auto") > -1) {
parsed.bleed.push("auto");
}
break;
case "Dimension": // bleed: 1in 2in, bleed: 20px ect.
parsed.bleed.push({
value: subNode.value,
unit: subNode.unit,
});
break;
case "Number":
parsed.bleed.push({
value: subNode.value,
unit: "px",
});
break;
default:
// ignore
}
},
});
dList.remove(dItem);
}
},
});
return parsed;
}
getSize(declaration) {
let width, height, orientation, format;
// Get size: Xmm Ymm
csstree.walk(declaration, {
visit: "Dimension",
enter: (node, item, list) => {
let { value, unit } = node;
if (typeof width === "undefined") {
width = { value, unit };
} else if (typeof height === "undefined") {
height = { value, unit };
}
},
});
// Get size: "A4"
csstree.walk(declaration, {
visit: "String",
enter: (node, item, list) => {
let name = node.value.replace(/["|']/g, "");
let s = pageSizes[name];
if (s) {
width = s.width;
height = s.height;
}
},
});
// Get Format or Landscape or Portrait
csstree.walk(declaration, {
visit: "Identifier",
enter: (node, item, list) => {
let name = node.name;
if (name === "landscape" || name === "portrait") {
orientation = node.name;
} else if (name !== "auto") {
let s = pageSizes[name];
if (s) {
width = s.width;
height = s.height;
}
format = name;
}
},
});
return {
width,
height,
orientation,
format,
};
}
/**
* Parses a shorthand or longhand `margin` declaration and expands it into
* individual `top`, `right`, `bottom`, and `left` sides.
*
* Supports values like:
* - `margin: 10px`
* - `margin: 10px 20px`
* - `margin: 10px 20px 30px`
* - `margin: 10px 20px 30px 40px`
*
* @param {Object} declaration - The AST node representing the `margin` declaration.
* @returns {Object} An object with `top`, `right`, `bottom`, and `left` properties.
*/
getMargins(declaration) {
let margins = [];
let margin = {
top: {},
right: {},
left: {},
bottom: {},
};
csstree.walk(declaration, {
enter: (node) => {
switch (node.type) {
case "Dimension": // margin: 1in 2in, margin: 20px, etc...
margins.push(node);
break;
case "Number": // margin: 0
margins.push({ value: node.value, unit: "px" });
break;
default:
// ignore
}
},
});
if (margins.length === 1) {
for (let m in margin) {
margin[m] = margins[0];
}
} else if (margins.length === 2) {
margin.top = margins[0];
margin.right = margins[1];
margin.bottom = margins[0];
margin.left = margins[1];
} else if (margins.length === 3) {
margin.top = margins[0];
margin.right = margins[1];
margin.bottom = margins[2];
margin.left = margins[1];
} else if (margins.length === 4) {
margin.top = margins[0];
margin.right = margins[1];
margin.bottom = margins[2];
margin.left = margins[3];
}
return margin;
}
/**
* Parses a shorthand or longhand `padding` declaration and expands it into
* `top`, `right`, `bottom`, and `left` properties.
*
* Supports values like:
* - `padding: 10px`
* - `padding: 10px 20px`
* - `padding: 10px 20px 30px`
* - `padding: 10px 20px 30px 40px`
*
* @param {Object} declaration - The AST node representing the `padding` declaration.
* @returns {Object} An object with `top`, `right`, `bottom`, and `left` properties.
*/
getPaddings(declaration) {
let paddings = [];
let padding = {
top: {},
right: {},
left: {},
bottom: {},
};
csstree.walk(declaration, {
enter: (node) => {
switch (node.type) {
case "Dimension": // padding: 1in 2in, padding: 20px, etc...
paddings.push(node);
break;
case "Number": // padding: 0
paddings.push({ value: node.value, unit: "px" });
break;
default:
// ignore
}
},
});
if (paddings.length === 1) {
for (let p in padding) {
padding[p] = paddings[0];
}
} else if (paddings.length === 2) {
padding.top = paddings[0];
padding.right = paddings[1];
padding.bottom = paddings[0];
padding.left = paddings[1];
} else if (paddings.length === 3) {
padding.top = paddings[0];
padding.right = paddings[1];
padding.bottom = paddings[2];
padding.left = paddings[1];
} else if (paddings.length === 4) {
padding.top = paddings[0];
padding.right = paddings[1];
padding.bottom = paddings[2];
padding.left = paddings[3];
}
return padding;
}
/**
* Parses border-related declarations (`border`, `border-top`, etc.)
* and expands them into an object representing each side.
*
* This is used to apply page-level borders on generated content (e.g. `.pagedjs_area`).
*
* @param {Object} declaration - A declaration node with a `prop` and `value`.
* @returns {Object} An object with `top`, `right`, `bottom`, and `left` properties.
*/
getBorders(declaration) {
let border = {
top: {},
right: {},
left: {},
bottom: {},
};
if (declaration.prop == "border") {
border.top = csstree.generate(declaration.value);
border.right = csstree.generate(declaration.value);
border.bottom = csstree.generate(declaration.value);
border.left = csstree.generate(declaration.value);
} else if (declaration.prop == "border-top") {
border.top = csstree.generate(declaration.value);
} else if (declaration.prop == "border-right") {
border.right = csstree.generate(declaration.value);
} else if (declaration.prop == "border-bottom") {
border.bottom = csstree.generate(declaration.value);
} else if (declaration.prop == "border-left") {
border.left = csstree.generate(declaration.value);
}
return border;
}
/**
* Adds dynamically generated page classes (rules) to the stylesheet.
* These are based on parsed `@page` rules with selectors like:
* - `*` (default)
* - `:left`, `:right`, `:first`, `:blank`
* - `:nth(...)`
* - Named pages (e.g., `@page chapter`)
*
* Ensures each rule is only added once.
*
* @param {Object} pages - A dictionary of parsed `@page` definitions.
* @param {Object} ast - The stylesheet AST (typically from `csstree`).
* @param {Object} sheet - The stylesheet object that supports `insertRule()`.
*/
addPageClasses(pages, ast, sheet) {
// First add * page
if ("*" in pages && pages["*"].added === false) {
let p = this.createPage(pages["*"], ast.children, sheet);
sheet.insertRule(p);
pages["*"].added = true;
}
// Add :left & :right
if (":left" in pages && pages[":left"].added === false) {
let left = this.createPage(pages[":left"], ast.children, sheet);
sheet.insertRule(left);
pages[":left"].added = true;
}
if (":right" in pages && pages[":right"].added === false) {
let right = this.createPage(pages[":right"], ast.children, sheet);
sheet.insertRule(right);
pages[":right"].added = true;
}
// Add :first & :blank
if (":first" in pages && pages[":first"].added === false) {
let first = this.createPage(pages[":first"], ast.children, sheet);
sheet.insertRule(first);
pages[":first"].added = true;
}
if (":blank" in pages && pages[":blank"].added === false) {
let blank = this.createPage(pages[":blank"], ast.children, sheet);
sheet.insertRule(blank);
pages[":blank"].added = true;
}
// Add nth pages
for (let pg in pages) {
if (pages[pg].nth && pages[pg].added === false) {
let nth = this.createPage(pages[pg], ast.children, sheet);
sheet.insertRule(nth);
pages[pg].added = true;
}
}
// Add named pages
for (let pg in pages) {
if (pages[pg].name && pages[pg].added === false) {
let named = this.createPage(pages[pg], ast.children, sheet);
sheet.insertRule(named);
pages[pg].added = true;
}
}
}
/**
* Creates a CSS rule for a page, applying margin, padding, border, and
* dimension variables. Also adds marginalia and notes if present.
*
* @param {Object} page - The page object containing properties like `width`, `margin`, etc.
* @param {Object} ruleList - The list of AST rules to which new rules may be appended.
* @param {Object} sheet - The stylesheet object used to insert rules.
* @returns {Object} A CSS rule representing the page.
*/
createPage(page, ruleList, sheet) {
let selectors = this.selectorsForPage(page);
let children = page.block.children.copy();
let block = {
type: "Block",
loc: 0,
children: children,
};
let rule = this.createRule(selectors, block);
this.addMarginVars(page.margin, children, children.first());
this.addPaddingVars(page.padding, children, children.first());
this.addBorderVars(page.border, children, children.first());
if (page.width) {
this.addDimensions(
page.width,
page.height,
page.orientation,
children,
children.first(),
);
}
if (page.marginalia) {
this.addMarginaliaStyles(page, ruleList, rule, sheet);
this.addMarginaliaContent(page, ruleList, rule, sheet);
}
if (page.notes) {
this.addNotesStyles(page.notes, page, ruleList, rule, sheet);
}
return rule;
}
/**
* Adds CSS custom properties (variables) for page margins to the rule block.
*
* @param {Object} margin - An object with `top`, `right`, `bottom`, and `left` values.
* @param {Object} list - The list of declarations (typically from a Block AST node).
* @param {Object} item - Reference item used for insertion position.
*/
addMarginVars(margin, list, item) {
// variables for margins
for (let m in margin) {
if (typeof margin[m].value !== "undefined") {
let value = margin[m].value + (margin[m].unit || "");
let mVar = list.createItem({
type: "Declaration",
property: "--pagedjs-margin-" + m,
value: {
type: "Raw",
value: value,
},
});
list.append(mVar, item);
}
}
}
/**
* Adds CSS custom properties (variables) for page padding to the rule block.
*
* @param {Object} padding - An object with `top`, `right`, `bottom`, and `left` values.
* @param {Object} list - The list of declarations.
* @param {Object} item - Reference node for insertion.
*/
addPaddingVars(padding, list, item) {
// variables for padding
for (let p in padding) {
if (typeof padding[p].value !== "undefined") {
let value = padding[p].value + (padding[p].unit || "");
let pVar = list.createItem({
type: "Declaration",
property: "--pagedjs-padding-" + p,
value: {
type: "Raw",
value: value,
},
});
list.append(pVar, item);
}
}
}
/**
* Adds CSS custom properties (variables) for page borders to the rule block.
*
* @param {Object} border - An object with string values for `top`, `right`, `bottom`, and `left`.
* @param {Object} list - The list of declarations.
* @param {Object} item - Reference node for insertion.
*/
addBorderVars(border, list, item) {
// variables for borders
for (const name of Object.keys(border)) {
const value = border[name];
// value is an empty object when undefined
if (typeof value === "string") {
const borderItem = list.createItem({
type: "Declaration",
property: "--pagedjs-border-" + name,
value: {
type: "Raw",
value: value,
},
});
list.append(borderItem, item);
}
}
}
/**
* Adds CSS custom properties for page width and height based on orientation.
*
* @param {Object} width - Width value (e.g., {value: 210, unit: "mm"}).
* @param {Object} height - Height value (same structure as width).
* @param {string} orientation - Either 'portrait' or 'landscape'.
* @param {Object} list - Declaration list to which variables are added.
* @param {Object} item - Reference node.
*/
addDimensions(width, height, orientation, list, item) {
let widthString, heightString;
widthString = CSSValueToString(width);
heightString = CSSValueToString(height);
if (orientation && orientation !== "portrait") {
// reverse for orientation
[widthString, heightString] = [heightString, widthString];
}
// width variable
let wVar = this.createVariable("--pagedjs-pagebox-width", widthString);
list.appendData(wVar);
// height variable
let hVar = this.createVariable("--pagedjs-pagebox-height", heightString);
list.appendData(hVar);
// let w = this.createDimension("width", width);
// let h = this.createDimension("height", height);
// list.appendData(w);
// list.appendData(h);
}
/**
* Adds marginalia rules (styles) for specified page regions (e.g., top-left, right-middle).
* Handles:
* - Content detection
* - Vertical alignment conversion
* - max-width/max-height additions
*
* @param {Object} page - The page object containing marginalia blocks.
* @param {Object} list - Rule list to append to.
* @param {Object} item - The current rule or rule block.
* @param {Object} sheet - The stylesheet to insert rules into.
*/
addMarginaliaStyles(page, list, item, sheet) {
for (let loc in page.marginalia) {
let block = csstree.clone(page.marginalia[loc]);
let hasContent = false;
if (block.children.isEmpty()) {
continue;
}
csstree.walk(block, {
visit: "Declaration",
enter: (node, item, list) => {
if (node.property === "content") {
if (
node.value.children &&
node.value.children.first().name === "none"
) {
hasContent = false;
} else {
hasContent = true;
}
list.remove(item);
}
if (node.property === "vertical-align") {
csstree.walk(node, {
visit: "Identifier",
enter: (identNode, identItem, identlist) => {
let name = identNode.name;
if (name === "top") {
identNode.name = "flex-start";
} else if (name === "middle") {
identNode.name = "center";
} else if (name === "bottom") {
identNode.name = "flex-end";
}
},
});
node.property = "align-items";
}
if (
node.property === "width" &&
(loc === "top-left" ||
loc === "top-center" ||
loc === "top-right" ||
loc === "bottom-left" ||
loc === "bottom-center" ||
loc === "bottom-right")
) {
let c = csstree.clone(node);
c.property = "max-width";
list.appendData(c);
}
if (
node.property === "height" &&
(loc === "left-top" ||
loc === "left-middle" ||
loc === "left-bottom" ||
loc === "right-top" ||
loc === "right-middle" ||
loc === "right-bottom")
) {
let c = csstree.clone(node);
c.property = "max-height";
list.appendData(c);
}
},
});
let marginSelectors = this.selectorsForPageMargin(page, loc);
let marginRule = this.createRule(marginSelectors, block);
list.appendData(marginRule);
let sel = csstree.generate({
type: "Selector",
children: marginSelectors,
});
this.marginalia[sel] = {
page: page,
selector: sel,
block: page.marginalia[loc],
hasContent: hasContent,
};
}
}
/**
* Generates the content-only display rules for marginalia.
* Adds `display: none` or `display: block` for margin content depending on whether `content: none` is used.
*
* @param {Object} page - Page object with marginalia blocks.
* @param {Object} list - Rule list.
* @param {Object} item - Rule being built.
* @param {Object} sheet - Stylesheet to which rules are inserted.
*/
addMarginaliaContent(page, list, item, sheet) {
let displayNone;
// Just content
for (let loc in page.marginalia) {
let content = csstree.clone(page.marginalia[loc]);
csstree.walk(content, {
visit: "Declaration",
enter: (node, item, list) => {
if (node.property !== "content") {
list.remove(item);
}
if (
node.value.children &&
node.value.children.first().name === "none"
) {
displayNone = true;
}
},
});
if (content.children.isEmpty()) {
continue;
}
let displaySelectors = this.selectorsForPageMargin(page, loc);
let displayDeclaration;
displaySelectors.insertData({
type: "Combinator",
name: ">",
});
displaySelectors.insertData({
type: "ClassSelector",
name: "pagedjs_margin-content",
});
displaySelectors.insertData({
type: "Combinator",
name: ">",
});
displaySelectors.insertData({
type: "TypeSelector",
name: "*",
});
if (displayNone) {
displayDeclaration = this.createDeclaration("display", "none");
} else {
displayDeclaration = this.createDeclaration("display", "block");
}
let displayRule = this.createRule(displaySelectors, [displayDeclaration]);
sheet.insertRule(displayRule);
// insert content rule
let contentSelectors = this.selectorsForPageMargin(page, loc);
contentSelectors.insertData({
type: "Combinator",
name: ">",
});
contentSelectors.insertData({
type: "ClassSelector",
name: "pagedjs_margin-content",
});
contentSelectors.insertData({
type: "PseudoElementSelector",
name: "after",
children: null,
});
let contentRule = this.createRule(contentSelectors, content);
sheet.insertRule(contentRule);
}
}
addRootVars(
ast,
width,
height,
orientation,
bleed,
bleedrecto,
bleedverso,
marks,
) {
let rules = [];
let selectors = new csstree.List();
selectors.insertData({
type: "PseudoClassSelector",
name: "root",
children: null,
});
let widthString, heightString;
let widthStringRight, heightStringRight;
let widthStringLeft, heightStringLeft;
if (!bleed) {
widthString = CSSValueToString(width);
heightString = CSSValueToString(height);
widthStringRight = CSSValueToString(width);
heightStringRight = CSSValueToString(height);
widthStringLeft = CSSValueToString(width);
heightStringLeft = CSSValueToString(height);
} else {
widthString = `calc( ${CSSValueToString(width)} + ${CSSValueToString(bleed.left)} + ${CSSValueToString(bleed.right)} )`;
heightString = `calc( ${CSSValueToString(height)} + ${CSSValueToString(bleed.top)} + ${CSSValueToString(bleed.bottom)} )`;
widthStringRight = `calc( ${CSSValueToString(width)} + ${CSSValueToString(bleed.left)} + ${CSSValueToString(bleed.right)} )`;
heightStringRight = `calc( ${CSSValueToString(height)} + ${CSSValueToString(bleed.top)} + ${CSSValueToString(bleed.bottom)} )`;
widthStringLeft = `calc( ${CSSValueToString(width)} + ${CSSValueToString(bleed.left)} + ${CSSValueToString(bleed.right)} )`;
heightStringLeft = `calc( ${CSSValueToString(height)} + ${CSSValueToString(bleed.top)} + ${CSSValueToString(bleed.bottom)} )`;
let bleedTop = this.createVariable(
"--pagedjs-bleed-top",
CSSValueToString(bleed.top),
);
let bleedRight = this.createVariable(
"--pagedjs-bleed-right",
CSSValueToString(bleed.right),
);
let bleedBottom = this.createVariable(
"--pagedjs-bleed-bottom",
CSSValueToString(bleed.bottom),
);
let bleedLeft = this.createVariable(
"--pagedjs-bleed-left",
CSSValueToString(bleed.left),
);
let bleedTopRecto = this.createVariable(
"--pagedjs-bleed-right-top",
CSSValueToString(bleed.top),
);
let bleedRightRecto = this.createVariable(
"--pagedjs-bleed-right-right",
CSSValueToString(bleed.right),
);
let bleedBottomRecto = this.createVariable(
"--pagedjs-bleed-right-bottom",
CSSValueToString(bleed.bottom),
);
let bleedLeftRecto = this.createVariable(
"--pagedjs-bleed-right-left",
CSSValueToString(bleed.left),
);
let bleedTopVerso = this.createVariable(
"--pagedjs-bleed-left-top",
CSSValueToString(bleed.top),
);
let bleedRightVerso = this.createVariable(
"--pagedjs-bleed-left-right",
CSSValueToString(bleed.right),
);
let bleedBottomVerso = this.createVariable(
"--pagedjs-bleed-left-bottom",
CSSValueToString(bleed.bottom),
);
let bleedLeftVerso = this.createVariable(
"--pagedjs-bleed-left-left",
CSSValueToString(bleed.left),
);
if (bleedrecto) {
bleedTopRecto = this.createVariable(
"--pagedjs-bleed-right-top",
CSSValueToString(bleedrecto.top),
);
bleedRightRecto = this.createVariable(
"--pagedjs-bleed-right-right",
CSSValueToString(bleedrecto.right),
);
bleedBottomRecto = this.createVariable(
"--pagedjs-bleed-right-bottom",
CSSValueToString(bleedrecto.bottom),
);
bleedLeftRecto = this.createVariable(
"--pagedjs-bleed-right-left",
CSSValueToString(bleedrecto.left),
);
widthStringRight = `calc( ${CSSValueToString(width)} + ${CSSValueToString(bleedrecto.left)} + ${CSSValueToString(bleedrecto.right)} )`;
heightStringRight = `calc( ${CSSValueToString(height)} + ${CSSValueToString(bleedrecto.top)} + ${CSSValueToString(bleedrecto.bottom)} )`;
}
if (bleedverso) {
bleedTopVerso = this.createVariable(
"--pagedjs-bleed-left-top",
CSSValueToString(bleedverso.top),
);
bleedRightVerso = this.createVariable(
"--pagedjs-bleed-left-right",
CSSValueToString(bleedverso.right),
);
bleedBottomVerso = this.createVariable(
"--pagedjs-bleed-left-bottom",
CSSValueToString(bleedverso.bottom),
);
bleedLeftVerso = this.createVariable(
"--pagedjs-bleed-left-left",
CSSValueToString(bleedverso.left),
);
widthStringLeft = `calc( ${CSSValueToString(width)} + ${CSSValueToString(bleedverso.left)} + ${CSSValueToString(bleedverso.right)} )`;
heightStringLeft = `calc( ${CSSValueToString(height)} + ${CSSValueToString(bleedverso.top)} + ${CSSValueToString(bleedverso.bottom)} )`;
}
let pageWidthVar = this.createVariable(
"--pagedjs-width",
CSSValueToString(width),
);
let pageHeightVar = this.createVariable(
"--pagedjs-height",
CSSValueToString(height),
);
rules.push(
bleedTop,
bleedRight,
bleedBottom,
bleedLeft,
bleedTopRecto,
bleedRightRecto,
bleedBottomRecto,
bleedLeftRecto,
bleedTopVerso,
bleedRightVerso,
bleedBottomVerso,
bleedLeftVerso,
pageWidthVar,
pageHeightVar,
);
}
if (marks) {
marks.forEach((mark) => {
let markDisplay = this.createVariable(
"--pagedjs-mark-" + mark + "-display",
"block",
);
rules.push(markDisplay);
});
}
// orientation variable
if (orientation) {
let oVar = this.createVariable("--pagedjs-orientation", orientation);
rules.push(oVar);
if (orientation !== "portrait") {
// reverse for orientation
[widthString, heightString] = [heightString, widthString];
[widthStringRight, heightStringRight] = [
heightStringRight,
widthStringRight,
];
[widthStringLeft, heightStringLeft] = [
heightStringLeft,
widthStringLeft,
];
}
}
let wVar = this.createVariable("--pagedjs-width", widthString);
let hVar = this.createVariable("--pagedjs-height", heightString);
let wVarR = this.createVariable("--pagedjs-width-right", widthStringRight);
let hVarR = this.createVariable(
"--pagedjs-height-right",
heightStringRight,
);
let wVarL = this.createVariable("--pagedjs-width-left", widthStringLeft);
let hVarL = this.createVariable("--pagedjs-height-left", heightStringLeft);
rules.push(wVar, hVar, wVarR, hVarR, wVarL, hVarL);
let rule = this.createRule(selectors, rules);
ast.children.appendData(rule);
}
/**
* Appends CSS rules for footnotes, sidenotes, or other types of page notes.
*
* Each note rule targets a `.pagedjs_<type>_content` class inside the given page selector.
*
* @param {Object} notes - Object where each key is a note type (e.g. "footnote") and value is a Block node.
* @param {Object} page - The page object.
* @param {Object} list - The CSS rule list to append new note rules to.
* @param {Object} item - Not used here, but may be for future insertion reference.
* @param {Object} sheet - The stylesheet object (not used in this function).
*/
addNotesStyles(notes, page, list, item, sheet) {
for (const note in notes) {
let selectors = this.selectorsForPage(page);
selectors.insertData({
type: "Combinator",
name: " ",
});
selectors.insertData({
type: "ClassSelector",
name: "pagedjs_" + note + "_content",
});
let notesRule = this.createRule(selectors, notes[note]);
list.appendData(notesRule);
}
}
/*
@page {
size: var(--pagedjs-width) var(--pagedjs-height);
margin: 0;
padding: 0;
}
*/
addRootPage(ast, size, bleed, bleedrecto, bleedverso) {
let { width, height, orientation, format } = size;
let children = new csstree.List();
let childrenLeft = new csstree.List();
let childrenRight = new csstree.List();
let dimensions = new csstree.List();
let dimensionsLeft = new csstree.List();
let dimensionsRight = new csstree.List();
if (bleed) {
let widthCalculations = new csstree.List();
let heightCalculations = new csstree.List();
// width
widthCalculations.appendData({
type: "Dimension",
unit: width.unit,
value: width.value,
});
widthCalculations.appendData({
type: "WhiteSpace",
value: " ",
});
widthCalculations.appendData({
type: "Operator",
value: "+",
});
widthCalculations.appendData({
type: "WhiteSpace",
value: " ",
});
widthCalculations.appendData({
type: "Dimension",
unit: bleed.left.unit,
value: bleed.left.value,
});
widthCalculations.appendData({
type: "WhiteSpace",
value: " ",
});
widthCalculations.appendData({
type: "Operator",
value: "+",
});
widthCalculations.appendData({
type: "WhiteSpace",
value: " ",
});
widthCalculations.appendData({
type: "Dimension",
unit: bleed.right.unit,
value: bleed.right.value,
});
// height
heightCalculations.appendData({
type: "Dimension",
unit: height.unit,
value: height.value,
});
heightCalculations.appendData({
type: "WhiteSpace",
value: " ",
});
heightCalculations.appendData({
type: "Operator",
value: "+",
});
heightCalculations.appendData({
type: "WhiteSpace",
value: " ",
});
heightCalculations.appendData({
type: "Dimension",
unit: bleed.top.unit,
value: bleed.top.value,
});
heightCalculations.appendData({
type: "WhiteSpace",
value: " ",
});
heightCalculations.appendData({
type: "Operator",
value: "+",
});
heightCalculations.appendData({
type: "WhiteSpace",
value: " ",
});
heightCalculations.appendData({
type: "Dimension",
unit: bleed.bottom.unit,
value: bleed.bottom.value,
});
dimensions.appendData({
type: "Function",
name: "calc",
children: widthCalculations,
});
dimensions.appendData({
type: "WhiteSpace",
value: " ",
});
dimensions.appendData({
type: "Function",
name: "calc",
children: heightCalculations,
});
} else if (format) {
dimensions.appendData({
type: "Identifier",
name: format,
});
if (orientation) {
dimensions.appendData({
type: "WhiteSpace",
value: " ",
});
dimensions.appendData({
type: "Identifier",
name: orientation,
});
}
} else {
dimensions.appendData({
type: "Dimension",
unit: width.unit,
value: width.value,
});
dimensions.appendData({
type: "WhiteSpace",
value: " ",
});
dimensions.appendData({
type: "Dimension",
unit: height.unit,
value: height.value,
});
}
children.appendData({
type: "Declaration",
property: "size",
loc: null,
value: {
type: "Value",
children: dimensions,
},
});
children.appendData({
type: "Declaration",
property: "margin",
loc: null,
value: {
type: "Value",
children: [
{
type: "Dimension",
unit: "px",
value: 0,
},
],
},
});
children.appendData({
type: "Declaration",
property: "padding",
loc: null,
value: {
type: "Value",
children: [
{
type: "Dimension",
unit: "px",
value: 0,
},
],
},
});
children.appendData({
type: "Declaration",
property: "padding",
loc: null,
value: {
type: "Value",
children: [
{
type: "Dimension",
unit: "px",
value: 0,
},
],
},
});
let rule = ast.children.createItem({
type: "Atrule",
prelude: null,
name: "page",
block: {
type: "Block",
loc: null,
children: children,
},
});
ast.children.append(rule);
if (bleedverso) {
let widthCalculationsLeft = new csstree.List();
let heightCalculationsLeft = new csstree.List();
// width
widthCalculationsLeft.appendData({
type: "Dimension",
unit: width.unit,
value: width.value,
});
widthCalculationsLeft.appendData({
type: "WhiteSpace",
value: " ",
});
widthCalculationsLeft.appendData({
type: "Operator",
value: "+",
});
widthCalculationsLeft.appendData({
type: "WhiteSpace",
value: " ",
});
widthCalculationsLeft.appendData({
type: "Dimension",
unit: bleedverso.left.unit,
value: bleedverso.left.value,
});
widthCalculationsLeft.appendData({
type: "WhiteSpace",
value: " ",
});
widthCalculationsLeft.appendData({
type: "Operator",
value: "+",
});
widthCalculationsLeft.appendData({
type: "WhiteSpace",
value: " ",
});
widthCalculationsLeft.appendData({
type: "Dimension",
unit: bleedverso.right.unit,
value: bleedverso.right.value,
});
// height
heightCalculationsLeft.appendData({
type: "Dimension",
unit: height.unit,
value: height.value,
});
heightCalculationsLeft.appendData({
type: "WhiteSpace",
value: " ",
});
heightCalculationsLeft.appendData({
type: "Operator",
value: "+",
});
heightCalculationsLeft.appendData({
type: "WhiteSpace",
value: " ",
});
heightCalculationsLeft.appendData({
type: "Dimension",
unit: bleedverso.top.unit,
value: bleedverso.top.value,
});
heightCalculationsLeft.appendData({
type: "WhiteSpace",
value: " ",
});
heightCalculationsLeft.appendData({
type: "Operator",
value: "+",
});
heightCalculationsLeft.appendData({
type: "WhiteSpace",
value: " ",
});
heightCalculationsLeft.appendData({
type: "Dimension",
unit: bleedverso.bottom.unit,
value: bleedverso.bottom.value,
});
dimensionsLeft.appendData({
type: "Function",
name: "calc",
children: widthCalculationsLeft,
});
dimensionsLeft.appendData({
type: "WhiteSpace",
value: " ",
});
dimensionsLeft.appendData({
type: "Function",
name: "calc",
children: heightCalculationsLeft,
});
childrenLeft.appendData({
type: "Declaration",
property: "size",
loc: null,
value: {
type: "Value",
children: dimensionsLeft,
},
});
let ruleLeft = ast.children.createItem({
type: "Atrule",
prelude: null,
name: "page :left",
block: {
type: "Block",
loc: null,
children: childrenLeft,
},
});
ast.children.append(ruleLeft);
}
if (bleedrecto) {
let widthCalculationsRight = new csstree.List();
let heightCalculationsRight = new csstree.List();
// width
widthCalculationsRight.appendData({
type: "Dimension",
unit: width.unit,
value: width.value,
});
widthCalculationsRight.appendData({
type: "WhiteSpace",
value: " ",
});
widthCalculationsRight.appendData({
type: "Operator",
value: "+",
});
widthCalculationsRight.appendData({
type: "WhiteSpace",
value: " ",
});
widthCalculationsRight.appendData({
type: "Dimension",
unit: bleedrecto.left.unit,
value: bleedrecto.left.value,
});
widthCalculationsRight.appendData({
type: "WhiteSpace",
value: " ",
});
widthCalculationsRight.appendData({
type: "Operator",
value: "+",
});
widthCalculationsRight.appendData({
type: "WhiteSpace",
value: " ",
});
widthCalculationsRight.appendData({
type: "Dimension",
unit: bleedrecto.right.unit,
value: bleedrecto.right.value,
});
// height
heightCalculationsRight.appendData({
type: "Dimension",
unit: height.unit,
value: height.value,
});
heightCalculationsRight.appendData({
type: "WhiteSpace",
value: " ",
});
heightCalculationsRight.appendData({
type: "Operator",
value: "+",
});
heightCalculationsRight.appendData({
type: "WhiteSpace",
value: " ",
});
heightCalculationsRight.appendData({
type: "Dimension",
unit: bleedrecto.top.unit,
value: bleedrecto.top.value,
});
heightCalculationsRight.appendData({
type: "WhiteSpace",
value: " ",
});
heightCalculationsRight.appendData({
type: "Operator",
value: "+",
});
heightCalculationsRight.appendData({
type: "WhiteSpace",
value: " ",
});
heightCalculationsRight.appendData({
type: "Dimension",
unit: bleedrecto.bottom.unit,
value: bleedrecto.bottom.value,
});
dimensionsRight.appendData({
type: "Function",
name: "calc",
children: widthCalculationsRight,
});
dimensionsRight.appendData({
type: "WhiteSpace",
value: " ",
});
dimensionsRight.appendData({
type: "Function",
name: "calc",
children: heightCalculationsRight,
});
childrenRight.appendData({
type: "Declaration",
property: "size",
loc: null,
value: {
type: "Value",
children: dimensionsRight,
},
});
let ruleRight = ast.children.createItem({
type: "Atrule",
prelude: null,
name: "page :right",
block: {
type: "Block",
loc: null,
children: childrenRight,
},
});
ast.children.append(ruleRight);
}
}
/**
* Parses an nth selector string (e.g., "2n+1") into its components.
* @param {string} nth - The nth selector string.
* @returns {object} Parsed nth object in An+B format.
*/
getNth(nth) {
let n = nth.indexOf("n");
let plus = nth.indexOf("+");
let splitN = nth.split("n");
let splitP = nth.split("+");
let a = null;
let b = null;
if (n > -1) {
a = splitN[0];
if (plus > -1) {
b = splitP[1];
}
} else {
b = nth;
}
return {
type: "Nth",
loc: null,
selector: null,
nth: {
type: "AnPlusB",
loc: null,
a: a,
b: b,
},
};
}
/**
* Adds page-specific classes based on dataset attributes.
* @param {object} page - The page object.
* @param {HTMLElement} start - The element marking the start of the page.
* @param {Array} pages - The array of all pages.
*/
addPageAttributes(page, start, pages) {
let namedPages = [start.dataset.page];
if (namedPages && namedPages.length) {
for (const named of namedPages) {
if (!named) {
continue;
}
page.name = named;
page.element.classList.add("pagedjs_named_page");
page.element.classList.add("pagedjs_" + named + "_page");
if (!start.dataset.splitFrom) {
page.element.classList.add("pagedjs_" + named + "_first_page");
}
}
}
}
/**
* Determines the start element for content on a new page.
* @param {HTMLElement} content - The content container.
* @param {object} breakToken - The token representing where the break occurred.
* @returns {HTMLElement|undefined} The starting element.
*/
getStartElement(content, breakToken) {
// If we have a breaktoken, we want the first node that will be added next.
let node = breakToken && (breakToken.overflow[0]?.node || breakToken.node);
if (!content && !breakToken) {
return;
}
// No break
if (!node) {
return content.children[0];
}
if (breakToken && breakToken.node && breakToken.overflow[0]?.topLevel) {
return findElement(breakToken.node, content);
}
// Top level element
if (node.nodeType === 1 && node.parentNode.nodeType === 11) {
return node;
}
// Named page
if (node.nodeType === 1 && node.dataset.page) {
return node;
}
// Get top level Named parent
let fragment = rebuildAncestors(node);
let pages = fragment.querySelectorAll("[data-page]");
if (pages.length) {
return pages[pages.length - 1];
} else {
return fragment.children[0];
}
}
/**
* Hook called before a page is laid out.
* @param {object} page - The page object.
* @param {HTMLElement} contents - The content container.
* @param {object} breakToken - The token where layout breaks.
* @param {object} chunker - The chunking utility.
*/
beforePageLayout(page, contents, breakToken, chunker) {
let start = this.getStartElement(contents, breakToken);
if (start) {
this.addPageAttributes(page, start, chunker.pages);
}
// page.element.querySelector('.paged_area').style.color = red;
}
/**
* Hook called after a page has been laid out.
* @param {object} page - The page object.
* @param {HTMLElement} contents - The content container.
* @param {object} breakToken - The break token for the current page.
* @param {object} chunker - The chunking utility.
*/
afterPageLayout(page, contents, breakToken, chunker) {
let thisPage = chunker.pages[chunker.pages.length - 1];
// If only footnotes were added, attribs should be like the previous page.
let emptyBody =
!thisPage.area.firstElementChild ||
!thisPage.area.firstElementChild.childElementCount ||
!thisPage.area.firstElementChild.firstElementChild.getBoundingClientRect()
.height;
let emptyFootnotes =
!thisPage.footnotesArea.firstElementChild.childElementCount ||
!thisPage.footnotesArea.firstElementChild.firstElementChild.getBoundingClientRect()
.height;
if (emptyBody && !emptyFootnotes && chunker.pages.length > 1) {
// Start element for the previous page.
let prevBreakToken = chunker.pages[chunker.pages.length - 2].startToken;
let start = this.getStartElement(contents, prevBreakToken);
if (start) {
this.addPageAttributes(thisPage, start, chunker.pages);
}
}
}
/**
* Final adjustments to the page after layout including margin styling.
* @param {HTMLElement} fragment - The document fragment.
* @param {object} page - The page object.
* @param {object} breakToken - The break token.
* @param {object} chunker - The chunker object.
*/
finalizePage(fragment, page, breakToken, chunker) {
for (let m in this.marginalia) {
let margin = this.marginalia[m];
let sels = m.split(" ");
let content;
if (page.element.matches(sels[0]) && margin.hasContent) {
content = page.element.querySelector(sels[1]);
content.classList.add("hasContent");
}
}
// check center
["top", "bottom"].forEach((loc) => {
let marginGroup = page.element.querySelector(".pagedjs_margin-" + loc);
let center = page.element.querySelector(
".pagedjs_margin-" + loc + "-center",
);
let left = page.element.querySelector(".pagedjs_margin-" + loc + "-left");
let right = page.element.querySelector(
".pagedjs_margin-" + loc + "-right",
);
let centerContent = center.classList.contains("hasContent");
let leftContent = left.classList.contains("hasContent");
let rightContent = right.classList.contains("hasContent");
let centerWidth, leftWidth, rightWidth;
if (leftContent) {
leftWidth = window.getComputedStyle(left)["max-width"];
}
if (rightContent) {
rightWidth = window.getComputedStyle(right)["max-width"];
}
if (centerContent) {
centerWidth = window.getComputedStyle(center)["max-width"];
if (centerWidth === "none" || centerWidth === "auto") {
if (!leftContent && !rightContent) {
marginGroup.style["grid-template-columns"] = "0 1fr 0";
} else if (leftContent) {
if (!rightContent) {
if (leftWidth !== "none" && leftWidth !== "auto") {
marginGroup.style["grid-template-columns"] =
leftWidth + " 1fr " + leftWidth;
} else {
marginGroup.style["grid-template-columns"] = "auto auto 1fr";
left.style["white-space"] = "nowrap";
center.style["white-space"] = "nowrap";
let leftOuterWidth = left.offsetWidth;
let centerOuterWidth = center.offsetWidth;
let outerwidths = leftOuterWidth + centerOuterWidth;
let newcenterWidth = (centerOuterWidth * 100) / outerwidths;
marginGroup.style["grid-template-columns"] =
"minmax(16.66%, 1fr) minmax(33%, " +
newcenterWidth +
"%) minmax(16.66%, 1fr)";
left.style["white-space"] = "normal";
center.style["white-space"] = "normal";
}
} else {
if (leftWidth !== "none" && leftWidth !== "auto") {
if (rightWidth !== "none" && rightWidth !== "auto") {
marginGroup.style["grid-template-columns"] =
leftWidth + " 1fr " + rightWidth;
} else {
marginGroup.style["grid-template-columns"] =
leftWidth + " 1fr " + leftWidth;
}
} else {
if (rightWidth !== "none" && rightWidth !== "auto") {
marginGroup.style["grid-template-columns"] =
rightWidth + " 1fr " + rightWidth;
} else {
marginGroup.style["grid-template-columns"] = "auto auto 1fr";
left.style["white-space"] = "nowrap";
center.style["white-space"] = "nowrap";
right.style["white-space"] = "nowrap";
let leftOuterWidth = left.offsetWidth;
let centerOuterWidth = center.offsetWidth;
let rightOuterWidth = right.offsetWidth;
let outerwidths =
leftOuterWidth + centerOuterWidth + rightOuterWidth;
let newcenterWidth = (centerOuterWidth * 100) / outerwidths;
if (newcenterWidth > 40) {
marginGroup.style["grid-template-columns"] =
"minmax(16.66%, 1fr) minmax(33%, " +
newcenterWidth +
"%) minmax(16.66%, 1fr)";
} else {
marginGroup.style["grid-template-columns"] =
"repeat(3, 1fr)";
}
left.style["white-space"] = "normal";
center.style["white-space"] = "normal";
right.style["white-space"] = "normal";
}
}
}
} else {
if (rightWidth !== "none" && rightWidth !== "auto") {
marginGroup.style["grid-template-columns"] =
rightWidth + " 1fr " + rightWidth;
} else {
marginGroup.style["grid-template-columns"] = "auto auto 1fr";
right.style["white-space"] = "nowrap";
center.style["white-space"] = "nowrap";
let rightOuterWidth = right.offsetWidth;
let centerOuterWidth = center.offsetWidth;
let outerwidths = rightOuterWidth + centerOuterWidth;
let newcenterWidth = (centerOuterWidth * 100) / outerwidths;
marginGroup.style["grid-template-columns"] =
"minmax(16.66%, 1fr) minmax(33%, " +
newcenterWidth +
"%) minmax(16.66%, 1fr)";
right.style["white-space"] = "normal";
center.style["white-space"] = "normal";
}
}
} else if (centerWidth !== "none" && centerWidth !== "auto") {
if (leftContent && leftWidth !== "none" && leftWidth !== "auto") {
marginGroup.style["grid-template-columns"] =
leftWidth + " " + centerWidth + " 1fr";
} else if (
rightContent &&
rightWidth !== "none" &&
rightWidth !== "auto"
) {
marginGroup.style["grid-template-columns"] =
"1fr " + centerWidth + " " + rightWidth;
} else {
marginGroup.style["grid-template-columns"] =
"1fr " + centerWidth + " 1fr";
}
}
} else {
if (leftContent) {
if (!rightContent) {
marginGroup.style["grid-template-columns"] = "1fr 0 0";
} else {
if (leftWidth !== "none" && leftWidth !== "auto") {
if (rightWidth !== "none" && rightWidth !== "auto") {
marginGroup.style["grid-template-columns"] =
leftWidth + " 1fr " + rightWidth;
} else {
marginGroup.style["grid-template-columns"] =
leftWidth + " 0 1fr";
}
} else {
if (rightWidth !== "none" && rightWidth !== "auto") {
marginGroup.style["grid-template-columns"] =
"1fr 0 " + rightWidth;
} else {
marginGroup.style["grid-template-columns"] = "auto 1fr auto";
left.style["white-space"] = "nowrap";
right.style["white-space"] = "nowrap";
let leftOuterWidth = left.offsetWidth;
let rightOuterWidth = right.offsetWidth;
let outerwidths = leftOuterWidth + rightOuterWidth;
let newLeftWidth = (leftOuterWidth * 100) / outerwidths;
marginGroup.style["grid-template-columns"] =
"minmax(16.66%, " + newLeftWidth + "%) 0 1fr";
left.style["white-space"] = "normal";
right.style["white-space"] = "normal";
}
}
}
} else {
if (rightWidth !== "none" && rightWidth !== "auto") {
marginGroup.style["grid-template-columns"] = "1fr 0 " + rightWidth;
} else {
marginGroup.style["grid-template-columns"] = "0 0 1fr";
}
}
}
});
// check middle
["left", "right"].forEach((loc) => {
let middle = page.element.querySelector(
".pagedjs_margin-" + loc + "-middle.hasContent",
);
let marginGroup = page.element.querySelector(".pagedjs_margin-" + loc);
let top = page.element.querySelector(".pagedjs_margin-" + loc + "-top");
let bottom = page.element.querySelector(
".pagedjs_margin-" + loc + "-bottom",
);
let topContent = top.classList.contains("hasContent");
let bottomContent = bottom.classList.contains("hasContent");
let middleHeight, topHeight, bottomHeight;
if (topContent) {
topHeight = window.getComputedStyle(top)["max-height"];
}
if (bottomContent) {
bottomHeight = window.getComputedStyle(bottom)["max-height"];
}
if (middle) {
middleHeight = window.getComputedStyle(middle)["max-height"];
if (middleHeight === "none" || middleHeight === "auto") {
if (!topContent && !bottomContent) {
marginGroup.style["grid-template-rows"] = "0 1fr 0";
} else if (topContent) {
if (!bottomContent) {
if (topHeight !== "none" && topHeight !== "auto") {
marginGroup.style["grid-template-rows"] =
topHeight + " calc(100% - " + topHeight + "*2) " + topHeight;
}
} else {
if (topHeight !== "none" && topHeight !== "auto") {
if (bottomHeight !== "none" && bottomHeight !== "auto") {
marginGroup.style["grid-template-rows"] =
topHeight +
" calc(100% - " +
topHeight +
" - " +
bottomHeight +
") " +
bottomHeight;
} else {
marginGroup.style["grid-template-rows"] =
topHeight +
" calc(100% - " +
topHeight +
"*2) " +
topHeight;
}
} else {
if (bottomHeight !== "none" && bottomHeight !== "auto") {
marginGroup.style["grid-template-rows"] =
bottomHeight +
" calc(100% - " +
bottomHeight +
"*2) " +
bottomHeight;
}
}
}
} else {
if (bottomHeight !== "none" && bottomHeight !== "auto") {
marginGroup.style["grid-template-rows"] =
bottomHeight +
" calc(100% - " +
bottomHeight +
"*2) " +
bottomHeight;
}
}
} else {
if (topContent && topHeight !== "none" && topHeight !== "auto") {
marginGroup.style["grid-template-rows"] =
topHeight +
" " +
middleHeight +
" calc(100% - (" +
topHeight +
" + " +
middleHeight +
"))";
} else if (
bottomContent &&
bottomHeight !== "none" &&
bottomHeight !== "auto"
) {
marginGroup.style["grid-template-rows"] =
"1fr " + middleHeight + " " + bottomHeight;
} else {
marginGroup.style["grid-template-rows"] =
"calc((100% - " +
middleHeight +
")/2) " +
middleHeight +
" calc((100% - " +
middleHeight +
")/2)";
}
}
} else {
if (topContent) {
if (!bottomContent) {
marginGroup.style["grid-template-rows"] = "1fr 0 0";
} else {
if (topHeight !== "none" && topHeight !== "auto") {
if (bottomHeight !== "none" && bottomHeight !== "auto") {
marginGroup.style["grid-template-rows"] =
topHeight + " 1fr " + bottomHeight;
} else {
marginGroup.style["grid-template-rows"] = topHeight + " 0 1fr";
}
} else {
if (bottomHeight !== "none" && bottomHeight !== "auto") {
marginGroup.style["grid-template-rows"] =
"1fr 0 " + bottomHeight;
} else {
marginGroup.style["grid-template-rows"] = "1fr 0 1fr";
}
}
}
} else {
if (bottomHeight !== "none" && bottomHeight !== "auto") {
marginGroup.style["grid-template-rows"] = "1fr 0 " + bottomHeight;
} else {
marginGroup.style["grid-template-rows"] = "0 0 1fr";
}
}
}
});
}
// CSS Tree Helpers
/**
* Builds a list of CSS selectors for a given page.
* @param {object} page - The page object.
* @returns {csstree.List} A list of CSS selector nodes.
*/
selectorsForPage(page) {
let nthlist;
let nth;
let selectors = new csstree.List();
selectors.insertData({
type: "ClassSelector",
name: "pagedjs_page",
});
// Named page
if (page.name) {
selectors.insertData({
type: "ClassSelector",
name: "pagedjs_named_page",
});
selectors.insertData({
type: "ClassSelector",
name: "pagedjs_" + page.name + "_page",
});
}
// PsuedoSelector
if (page.psuedo && !(page.name && page.psuedo === "first")) {
selectors.insertData({
type: "ClassSelector",
name: "pagedjs_" + page.psuedo + "_page",
});
}
if (page.name && page.psuedo === "first") {
selectors.insertData({
type: "ClassSelector",
name: "pagedjs_" + page.name + "_" + page.psuedo + "_page",
});
}
// Nth
if (page.nth) {
nthlist = new csstree.List();
nth = this.getNth(page.nth);
nthlist.insertData(nth);
selectors.insertData({
type: "PseudoClassSelector",
name: "nth-of-type",
children: nthlist,
});
}
return selectors;
}
/**
* Builds CSS selectors for a specific margin area of a page.
* @param {object} page - The page object.
* @param {string} margin - The margin position (e.g. "top", "bottom").
* @returns {csstree.List} A list of CSS selector nodes for the margin.
*/
selectorsForPageMargin(page, margin) {
let selectors = this.selectorsForPage(page);
selectors.insertData({
type: "Combinator",
name: " ",
});
selectors.insertData({
type: "ClassSelector",
name: "pagedjs_margin-" + margin,
});
return selectors;
}
/**
* Creates a CSS declaration for a property with a simple identifier value.
* @param {string} property - The CSS property name.
* @param {string} value - The CSS value.
* @param {boolean} important - Whether the declaration is !important.
* @returns {object} A CSSTree declaration node.
*/
createDeclaration(property, value, important) {
let children = new csstree.List();
children.insertData({
type: "Identifier",
loc: null,
name: value,
});
return {
type: "Declaration",
loc: null,
important: important,
property: property,
value: {
type: "Value",
loc: null,
children: children,
},
};
}
/**
* Creates a raw CSS variable declaration.
* @param {string} property - The variable name.
* @param {string} value - The raw CSS value.
* @returns {object} A CSSTree declaration node.
*/
createVariable(property, value) {
return {
type: "Declaration",
loc: null,
property: property,
value: {
type: "Raw",
value: value,
},
};
}
/**
* Creates a CSS calc() declaration from multiple dimensions.
* @param {string} property - The CSS property name.
* @param {Array} items - Array of {value, unit} objects.
* @param {boolean} important - Whether the declaration is !important.
* @param {string} [operator='+'] - Math operator (e.g. '+', '-', etc.).
* @returns {object} A CSSTree declaration node.
*/
createCalculatedDimension(property, items, important, operator = "+") {
let children = new csstree.List();
let calculations = new csstree.List();
items.forEach((item, index) => {
calculations.appendData({
type: "Dimension",
unit: item.unit,
value: item.value,
});
calculations.appendData({
type: "WhiteSpace",
value: " ",
});
if (index + 1 < items.length) {
calculations.appendData({
type: "Operator",
value: operator,
});
calculations.appendData({
type: "WhiteSpace",
value: " ",
});
}
});
children.insertData({
type: "Function",
loc: null,
name: "calc",
children: calculations,
});
return {
type: "Declaration",
loc: null,
important: important,
property: property,
value: {
type: "Value",
loc: null,
children: children,
},
};
}
/**
* Creates a CSS dimension-based declaration.
* @param {string} property - The CSS property.
* @param {object} cssValue - Object with `value` and `unit` keys.
* @param {boolean} important - Whether the declaration is !important.
* @returns {object} A CSSTree declaration node.
*/
createDimension(property, cssValue, important) {
let children = new csstree.List();
children.insertData({
type: "Dimension",
loc: null,
value: cssValue.value,
unit: cssValue.unit,
});
return {
type: "Declaration",
loc: null,
important: important,
property: property,
value: {
type: "Value",
loc: null,
children: children,
},
};
}
/**
* Creates a CSSTree Block node from an array of declarations.
* @param {Array} declarations - Array of CSSTree declaration nodes.
* @returns {object} A CSSTree block node.
*/
createBlock(declarations) {
let block = new csstree.List();
declarations.forEach((declaration) => {
block.insertData(declaration);
});
return {
type: "Block",
loc: null,
children: block,
};
}
/**
* Creates a CSSTree Rule node from selectors and a block.
* @param {csstree.List} selectors - List of selector nodes.
* @param {object|Array} block - A block node or array of declarations.
* @returns {object} A CSSTree rule node.
*/
createRule(selectors, block) {
let selectorList = new csstree.List();
selectorList.insertData({
type: "Selector",
children: selectors,
});
if (Array.isArray(block)) {
block = this.createBlock(block);
}
return {
type: "Rule",
prelude: {
type: "SelectorList",
children: selectorList,
},
block: block,
};
}
}
export default AtPage;