Source: chunker/layout.js

import { getBoundingClientRect } from "../utils/utils.js";
import {
	child,
	cloneNode,
	findElement,
	hasContent,
	indexOf,
	indexOfTextNode,
	isContainer,
	isElement,
	isText,
	letters,
	needsBreakBefore,
	needsPageBreak,
	needsPreviousBreakAfter,
	nodeAfter,
	nodeBefore,
	parentOf,
	prevValidNode,
	rebuildTree,
	replaceOrAppendElement,
	validNode,
	walk,
	words
} from "../utils/dom.js";
import BreakToken from "./breaktoken.js";
import RenderResult from "./renderresult.js";
import EventEmitter from "event-emitter";
import Hook from "../utils/hook.js";
import Overflow from "./overflow.js";

const MAX_CHARS_PER_BREAK = 1500;

/**
 * Layout
 * @class
 */
class Layout {

	constructor(element, hooks, options) {
		this.element = element;

		this.bounds = this.element.getBoundingClientRect();
		this.parentBounds = this.element.offsetParent?.getBoundingClientRect() ||
			{ left: 0 };
		let gap = parseFloat(window.getComputedStyle(this.element).columnGap);

		if (gap) {
			let leftMargin = this.bounds.left - this.parentBounds.left;
			this.gap = gap - leftMargin;
		} else {
			this.gap = 0;
		}

		if (hooks) {
			this.hooks = hooks;
		} else {
			this.hooks = {};
			this.hooks.onPageLayout = new Hook();
			this.hooks.layout = new Hook();
			this.hooks.renderNode = new Hook();
			this.hooks.layoutNode = new Hook();
			this.hooks.beforeOverflow = new Hook();
			this.hooks.onOverflow = new Hook();
			this.hooks.afterOverflowRemoved = new Hook();
			this.hooks.afterOverflowAdded = new Hook();
			this.hooks.onBreakToken = new Hook();
			this.hooks.beforeRenderResult = new Hook();
		}

		this.settings = options || {};

		this.maxChars = this.settings.maxChars || MAX_CHARS_PER_BREAK;
		this.forceRenderBreak = false;

		this.temporaryIndex = 0;
	}

	async renderTo(wrapper, source, breakToken, prevPage = null, bounds = this.bounds) {
		let start = this.getStart(source, breakToken);
		let firstDivisible = source;

		while (firstDivisible.children.length == 1) {
			firstDivisible = firstDivisible.children[0];
		}

		let walker = walk(start, source);

		let node;
		let done;
		let next;
		let forcedBreakQueue = [];

		let prevBreakToken = breakToken || new BreakToken(start);

		this.hooks && this.hooks.onPageLayout.trigger(wrapper, prevBreakToken, this);

		// Add overflow, and check that it doesn't have overflow itself.
		this.addOverflowToPage(wrapper, breakToken, prevPage);

		// Footnotes may change the bounds.
		bounds = this.element.getBoundingClientRect();

		let newBreakToken = this.findBreakToken(wrapper, source, bounds, prevBreakToken, start);

		if (prevBreakToken.isFinished()) {
			if (newBreakToken) {
				newBreakToken.setFinished();
			}
			return new RenderResult(newBreakToken);
		}

		let hasRenderedContent = !!wrapper.childNodes.length;

		if (prevBreakToken) {
			forcedBreakQueue = prevBreakToken.getForcedBreakQueue();
		}

		while (!done && !newBreakToken) {
			next = walker.next();
			node = next.value;
			done = next.done;

			if (node) {
				this.hooks && this.hooks.layoutNode.trigger(node);

				// Footnotes may change the bounds.
				bounds = this.element.getBoundingClientRect();

				// Check if the rendered element has a break set
				// Remember the node but don't apply the break until we have laid
				// out the rest of any parent content - this lets a table or divs
				// side by side still add content to this page before we start a new
				// one.
				if (this.shouldBreak(node) && hasRenderedContent) {
					forcedBreakQueue.push(node);
				}

				if (!forcedBreakQueue.length && node.dataset && node.dataset.page) {
					let named = node.dataset.page;
					let page = this.element.closest(".pagedjs_page");
					page.classList.add("pagejs_named_page");
					page.classList.add("pagedjs_" + named + "_page");
					if (!node.dataset.splitFrom) {
						page.classList.add("pagedjs_" + named + "_first_page");
					}
				}
			}

			// Check whether we have overflow when we've completed laying out a top
			// level element. This lets it have multiple children overflowing and
			// allows us to move all of the overflows onto the next page together.
			if (forcedBreakQueue.length || !node || !node.parentElement) {
				this.hooks && this.hooks.layout.trigger(wrapper, this);

				let imgs = wrapper.querySelectorAll("img");
				if (imgs.length) {
					await this.waitForImages(imgs);
				}

				newBreakToken = this.findBreakToken(wrapper, source, bounds, prevBreakToken, node);

				if (newBreakToken && node === undefined) {
					// We have run out of content. Do add the overflow to a new page but
					// don't repeat the whole thing again.
					newBreakToken.setFinished();
				}

				if (forcedBreakQueue.length) {
					if (newBreakToken) {
						newBreakToken.setForcedBreakQueue(forcedBreakQueue);
					}
					else {
						newBreakToken = this.breakAt(forcedBreakQueue.shift(), 0, forcedBreakQueue);
					}
				}

				if (newBreakToken && newBreakToken.equals(prevBreakToken)) {
					this.failed = true;
					return new RenderResult(undefined, "Unable to layout item: " + node);
				}

				if (!node || newBreakToken) {
					return new RenderResult(newBreakToken);
				}
			}

			// Should the Node be a shallow or deep clone?
			let shallow = isContainer(node);

			this.append(node, wrapper, source, breakToken, shallow);
			bounds = this.element.getBoundingClientRect();

			// Check whether layout has content yet.
			if (!hasRenderedContent) {
				hasRenderedContent = hasContent(node);
			}

			// Skip to the next node if a deep clone was rendered.
			if (!shallow) {
				walker = walk(nodeAfter(node, source), source);
			}
		}

		this.hooks && this.hooks.beforeRenderResult.trigger(newBreakToken, wrapper, this);
		return new RenderResult(newBreakToken);
	}

	breakAt(node, offset = 0, forcedBreakQueue = []) {
		let newBreakToken = new BreakToken(
			node,
			offset,
			forcedBreakQueue
		);
		let breakHooks = this.hooks.onBreakToken.triggerSync(newBreakToken, undefined, node, this);
		breakHooks.forEach((newToken) => {
			if (typeof newToken != "undefined") {
				newBreakToken = newToken;
			}
		});

		return newBreakToken;
	}

	shouldBreak(node, limiter) {
		let previousNode = nodeBefore(node, limiter);
		let parentNode = node.parentNode;
		let parentBreakBefore = needsBreakBefore(node) && parentNode && !previousNode && needsBreakBefore(parentNode);
		let doubleBreakBefore;

		if (parentBreakBefore) {
			doubleBreakBefore = node.dataset.breakBefore === parentNode.dataset.breakBefore;
		}

		return !doubleBreakBefore && needsBreakBefore(node) || needsPreviousBreakAfter(node) || needsPageBreak(node, previousNode);
	}

	forceBreak() {
		this.forceRenderBreak = true;
	}

	getStart(source, breakToken) {
		let start;
		let node = breakToken && breakToken.node;
		let finished = breakToken && breakToken.finished;

		if (node) {
			start = node;
		} else {
			start = source.firstChild;
		}

		return finished ? undefined : start;
	}

	/**
	 * Merge items from source into dest which don't yet exist in dest.
	 *
	 * @param {element} dest
	 *   A destination DOM node tree.
	 * @param {element} source
	 *   A source DOM node tree.
	 *
	 * @returns {void}
	 */
	addOverflowNodes(dest, source) {
		// Since we are modifying source as we go, we need to remember what
		Array.from(source.childNodes).forEach((item) => {
			if (isText(item)) {
				// If we get to a text node, we assume for now an earlier element
				// would have prevented duplication.
				dest.append(item);
			} else {
				let match = findElement(item, dest);
				if (match) {
					this.addOverflowNodes(match, item);
				} else {
					dest.appendChild(item);
				}
			}
		});
	}

	/**
	 * Add overflow to new page.
	 *
	 * @param {element} dest
	 *   The page content being built.
	 * @param {breakToken} breakToken
	 *   The current break cotent.
	 * @param {element} alreadyRendered
	 *   The content that has already been rendered.
	 *
	 * @returns {void}
	 */
	addOverflowToPage(dest, breakToken, alreadyRendered) {

		if (!breakToken || !breakToken.overflow.length) {
			return;
		}

		let fragment;

		breakToken.overflow.forEach((overflow) => {
			// A handy way to dump the contents of a fragment.
			// console.log([].map.call(overflow.content.children, e => e.outerHTML).join('\n'));

			fragment = rebuildTree(overflow.node, fragment, alreadyRendered);
			// Find the parent to which overflow.content should be added.
			// Overflow.content can be a much shallower start than
			// overflow.node, if the range end was outside of the range
			// start part of the tree. For this reason, we use a match against
			// the parent element of overflow.content if it exists, or fall back
			// to overflow.node's parent element.
			let addTo = overflow.ancestor ? findElement(overflow.ancestor, fragment) : fragment;
			this.addOverflowNodes(addTo, overflow.content);
		});

		// Record refs.
		Array.from(fragment.querySelectorAll('[data-ref]')).forEach(ref => {
			let refId = ref.dataset.ref;
			if (!dest.querySelector(`[data-ref='${refId}']`)) {
				if (!dest.indexOfRefs) {
					dest.indexOfRefs = {};
				}
				dest.indexOfRefs[refId] = ref;
			}
		});

		let tags = [ 'overflow-tagged', 'overflow-partial', 'range-start-overflow', 'range-end-overflow' ];
		tags.forEach((tag) => {
			let camel = tag.replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) {
				return index == 0 ? word.toLowerCase() : word.toUpperCase();
			}).replace(/[-\s]+/g, '');
			let instances = fragment.querySelectorAll(`[data-${tag}]`);
			instances.forEach((instance) => {
				delete instance.dataset[camel];
			})
		})

		dest.appendChild(fragment);

		this.hooks && this.hooks.afterOverflowAdded.trigger(dest);
	}

	/**
	 * Add text to new page.
	 *
	 * @param {element} node
	 *   The node being appended to the destination.
	 * @param {element} dest
	 *   The destination to which content is being added.
	 * @param {element} source
	 *   The source DOM
	 * @param {breakToken} breakToken
	 *   The current breakToken.
	 * @param {bool} shallow
	 *	 Whether to do a shallow copy of the node.
	 * @param {bool} rebuild
	 *   Whether to rebuild parents.
	 *
	 * @returns {ChildNode}
	 *   The cloned node.
	 */
	append(node, dest, source, breakToken, shallow = true, rebuild = true) {

		let clone = cloneNode(node, !shallow);

		if (node.parentNode && isElement(node.parentNode)) {
			let parent = findElement(node.parentNode, dest);
			if (parent) {
				replaceOrAppendElement(parent, clone);
			} else if (rebuild) {
				let fragment = rebuildTree(node.parentElement, undefined, source);
				parent = findElement(node.parentNode, fragment);
				replaceOrAppendElement(parent, clone);
				dest.appendChild(fragment);
			} else {
				dest.appendChild(clone);
			}

		} else {
			dest.appendChild(clone);
		}

		if (clone.dataset && clone.dataset.ref) {
			if (!dest.indexOfRefs) {
				dest.indexOfRefs = {};
			}
			dest.indexOfRefs[clone.dataset.ref] = clone;
		}

		let nodeHooks = this.hooks.renderNode.triggerSync(clone, node, this);
		nodeHooks.forEach((newNode) => {
			if (typeof newNode != "undefined") {
				clone = newNode;
			}
		});

		return clone;
	}

	rebuildTableFromBreakToken(breakToken, dest, source) {
		if (!breakToken || !breakToken.node) {
			return;
		}
		let node = breakToken.node;
		let td = isElement(node) ? node.closest("td") : node.parentElement.closest("td");
		if (td) {
			let rendered = findElement(td, dest, true);
			if (!rendered) {
				return;
			}
			while ((td = td.nextElementSibling)) {
				this.append(td, dest, source, null, true);
			}
		}
	}

	async waitForImages(imgs) {
		let results = Array.from(imgs).map(async (img) => {
			return this.awaitImageLoaded(img);
		});
		await Promise.all(results);
	}

	async awaitImageLoaded(image) {
		return new Promise(resolve => {
			if (image.complete !== true) {
				image.onload = function () {
					let { width, height } = window.getComputedStyle(image);
					resolve(width, height);
				};
				image.onerror = function (e) {
					let { width, height } = window.getComputedStyle(image);
					resolve(width, height, e);
				};
			} else {
				let { width, height } = window.getComputedStyle(image);
				resolve(width, height);
			}
		});
	}

	avoidBreakInside(node, limiter) {
		let breakNode;

		while (node.parentNode) {
			if (node === limiter) {
				break;
			}

			if (isElement(node) && node.dataset.originalBreakInside === "avoid") {
				breakNode = node;
				break;
			}

			node = node.parentNode;
		}
		return breakNode;
	}

	createOverflow(overflow, rendered, source) {
		let container = overflow.startContainer;
		let offset = overflow.startOffset;
		let node, renderedNode, parent, index, temp;
		let hyphen = this.settings.hyphenGlyph || "\u2011";
		let topLevel = false;

		if (isElement(container)) {
			if (container.nodeName == "INPUT") {
				temp = container;
			} else {
				temp = child(container, offset);
			}

			if (isElement(temp)) {
				renderedNode = findElement(temp, rendered);

				if (!renderedNode) {
					// Find closest element with data-ref
					let prevNode = prevValidNode(temp);
					if (!isElement(prevNode)) {
						prevNode = prevNode.parentElement;
					}
					renderedNode = findElement(prevNode, rendered);
					// Check if temp is the last rendered node at its level.
					if (!temp.nextSibling) {
						// We need to ensure that the previous sibling of temp is fully rendered.
						const renderedNodeFromSource = findElement(renderedNode, source);
						const walker = document.createTreeWalker(renderedNodeFromSource, NodeFilter.SHOW_ELEMENT);
						const lastChildOfRenderedNodeFromSource = walker.lastChild();
						const lastChildOfRenderedNodeMatchingFromRendered = findElement(lastChildOfRenderedNodeFromSource, rendered);
						// Check if we found that the last child in source
						if (!lastChildOfRenderedNodeMatchingFromRendered) {
							// Pending content to be rendered before virtual break token
							return;
						}
						// Otherwise we will return a break token as per below
					}
					// renderedNode is actually the last unbroken box that does not overflow.
					// Break Token is therefore the next sibling of renderedNode within source node.
					node = findElement(renderedNode, source).nextSibling;
					offset = 0;
				} else {
					node = findElement(renderedNode, source);
					offset = 0;
				}
			} else {
				if (container == rendered) {
					parent = renderedNode = source;
					topLevel = true;
				}
				else {
					renderedNode = findElement(container, rendered);

					if (!renderedNode) {
						renderedNode = findElement(prevValidNode(container), rendered);
					}

					parent = findElement(renderedNode, source);
				}
				index = indexOfTextNode(temp, parent, hyphen);
				// No seperation for the first textNode of an element
				if (index === 0) {
					node = parent;
					offset = 0;
				} else {
					node = child(parent, index);
					offset = 0;
				}
			}
		} else {
			renderedNode = findElement(container.parentNode, rendered);

			if (!renderedNode) {
				renderedNode = findElement(prevValidNode(container.parentNode), rendered);
			}

			parent = findElement(renderedNode, source);
			index = indexOfTextNode(container, parent, hyphen);

			if (index === -1) {
				return;
			}

			node = child(parent, index);

			offset += node.textContent.indexOf(container.textContent);
		}

		if (!node) {
			return;
		}

		return new Overflow(
			node,
			offset,
			overflow.getBoundingClientRect().height,
			overflow,
			topLevel
		);

	}

	lastChildCheck(parentElement, rootElement) {
		if (parentElement.childElementCount) {
			this.lastChildCheck(parentElement.lastElementChild, rootElement);
		}

		let refId = parentElement.dataset.ref;

		// A table row, math element or paragraph from which all content has been removed
		// can itself also be removed. It will be added on the next page.
		if (parentElement.dataset.overflowTagged && parentElement.textContent.trim() == '') {
			parentElement.parentNode.removeChild(parentElement);
		}
		else if (refId && !rootElement.indexOfRefs[refId]) {
			rootElement.indexOfRefs[refId] = parentElement;
		}
	}

	processOverflowResult(ranges, rendered, source, bounds, prevBreakToken, node, extract) {
		let breakToken, breakLetter;

		ranges.forEach((overflowRange) => {

			let overflowHooks = this.hooks.onOverflow.triggerSync(overflowRange, rendered, bounds, this);
			overflowHooks.forEach((newOverflow) => {
				if (typeof newOverflow != "undefined") {
					overflowRange = newOverflow;
				}
			});

			let overflow = this.createOverflow(overflowRange, rendered, source);
			if (!breakToken) {
				breakToken = new BreakToken(node, [overflow]);
			} else {
				breakToken.overflow.push(overflow);
			}

			// breakToken is nullable
			let breakHooks = this.hooks.onBreakToken.triggerSync(breakToken, overflowRange, rendered, this);
			breakHooks.forEach((newToken) => {
				if (typeof newToken != "undefined") {
					breakToken = newToken;
				}
			});

			// Stop removal if we are in a loop
			if (breakToken.equals(prevBreakToken)) {
				return;
			}

			if (overflow?.node && overflow?.offset && overflow?.node?.textContent) {
				breakLetter = overflow.node.textContent.charAt(overflow.offset);
			} else {
				breakLetter = undefined;
			}

			if (overflow?.node && extract) {
				overflow.ancestor = findElement(overflow.range.commonAncestorContainer, source);
				overflow.content = this.removeOverflow(overflowRange, breakLetter);
			}
		});

		// For each overflow that is removed, see if we have an empty td that can be removed.
		// Also check that the data-ref is set so we get all the split-froms and tos. If a copy
		// of a node wasn't shallow, the indexOfRefs entry won't be there yet.
		ranges.forEach((overflowRange) => {
			this.lastChildCheck(rendered, rendered);
		});

		// And then see if the last element has been completely removed and not split.
		if (rendered.indexOfRefs && extract && breakToken.overflow.length) {
			let firstOverflow = breakToken.overflow[0];
			if (firstOverflow?.node && firstOverflow.content) {
				// Remove data-refs in the overflow from the index.
				Array.from(firstOverflow.content.querySelectorAll('[data-ref]')).forEach(ref => {
					let refId = ref.dataset.ref;
					if (!rendered.querySelector(`[data-ref='${refId}']`)) {
						delete(rendered.indexOfRefs[refId]);
					}
				});
			}
		}

		breakToken.overflow.forEach((overflow) => {
				this.hooks && this.hooks.afterOverflowRemoved.trigger(overflow.content, rendered, this);
		})


		return breakToken;
	}

	findBreakToken(rendered, source, bounds = this.bounds, prevBreakToken, node = null, extract = true) {
		let breakToken, overflow = [];

		let overflowResult = this.findOverflow(rendered, bounds, source);
		while (overflowResult) {
			// Check whether overflow already added - multiple overflows might result in the
			// same range via avoid break rules.
			let existing = false;
			overflow.forEach((item) => {
				if (
					item.startContainer == overflowResult.startContainer &&
					item.endContainer == overflowResult.endContainer) {
					if (item.startOffset >= overflowResult.startOffset &&
						item.endOffset <= overflowResult.endOffset) {
						item.setStart(overflowResult.startContainer, overflowResult.startOffset);
						existing = true;
					}
					if (item.endOffset > overflowResult.endOffset &&
						item.startOffset == overflowResult.startOffset) {
						item.EndOffset = overflowResult.EndOffset;
						item.setEnd(overflowResult.endContainer, overflowResult.endOffset);
						existing = true;
					}
				}
			})
			if (!existing) {
				overflow.push(overflowResult);
			}
			overflowResult = this.findOverflow(rendered, bounds, source);
		}

		if (overflow.length) {
			breakToken = this.processOverflowResult(overflow, rendered, source, bounds, prevBreakToken, node, extract);
		}
		return breakToken;
	}

	/**
	 * Does the element exceed the bounds?
	 *
	 * @param {element} element
	 *   The element being constrained.
	 * @param {array} bounds
	 *   The bounding element.
	 *
	 * @returns {bool}
	 *   Whether the element is within bounds.
	 */
	hasOverflow(element, bounds = this.bounds) {
		let constrainingElement = element && element.parentNode; // this gets the element, instead of the wrapper for the width workaround
		if (constrainingElement.classList.contains("pagedjs_page_content")) {
			constrainingElement = element;
		}
		let { width, height } = element.getBoundingClientRect();
		let scrollWidth = constrainingElement ? constrainingElement.scrollWidth : 0;
		let scrollHeight = constrainingElement ? constrainingElement.scrollHeight : 0;
		return (Math.max(Math.ceil(width), scrollWidth) > Math.ceil(bounds.width)) ||
			Math.max(Math.ceil(height), scrollHeight) > Math.ceil(bounds.height);
	}

	/**
	 * Sums padding, borders and margins for bottom/right of parent elements.
	 *
	 * Assumes no margin collapsing because we're considering overflow
	 * on a page.
	 *
	 * This and callers need to be extended to handle right-to-left text and
	 * flow but I'll get LTR going first in the hope that it will simplify
	 * the task of getting RTL sorted later. Need test cases too.
	 */
	getAncestorPaddingBorderAndMarginSums(element) {
		let attribs = [
			'padding-top',
			'padding-right',
			'padding-bottom',
			'padding-left',
			'border-top-width',
			'border-right-width',
			'border-bottom-width',
			'border-left-width',
			'margin-top',
			'margin-right',
			'margin-bottom',
			'margin-left',
		];
		let result = {};
		attribs.forEach(attrib => result[attrib] = 0);

		while (element &&
			!element.classList.contains('pagedjs_page_content') &&
			!element.classList.contains('pagedjs_footnote_inner_content')) {
			let style = window.getComputedStyle(element);
			attribs.forEach(attrib => result[attrib] += parseInt(style[attrib]));
			element = element.parentElement;
		}

		return result;
	}

	/**
	 * Checks whether an element is within a table and gets any THEAD sizes.
	 */
	getAncestorTheadSizes(element) {
		let result = 0;

		while (element &&
			!element.classList.contains('pagedjs_page_content') &&
			!element.classList.contains('pagedjs_footnote_inner_content')) {
			if (element.tagName == 'TABLE') {
				element.childNodes.forEach(node => {
					if (node.tagName == 'THEAD') {
						let style = getComputedStyle(node);
						result += parseInt(style.height);
					}
				});
			}
			element = element.parentElement;
		}

		return result;
	}

	/**
	 * Adds temporary data-split-to/from attribute where needed.
	 *
	 * @param DomElement element
	 *   The deepest child, from which to start.
	 */
	addTemporarySplit(element, isTo = true) {
		this.temporaryIndex++;
		let name = isTo ? 'data-split-to' : 'data-split-from';
		while (element &&
			!element.classList.contains('pagedjs_page_content') &&
			!element.classList.contains('pagedjs_footnote_inner_content')) {

			if (!element.getAttribute(name)) {
				element.setAttribute(name, 'temp-' + this.temporaryIndex);
			}

			element = element.parentElement;
		}
	}

	/**
	 * Removes temporary data-split-to/from attribute where added.
	 *
	 * @param DomElement element
	 *   The deepest child, from which to start.
	 * @param boolean isTo
	 *   Whether a split-to or -from was added.
	 */
	deleteTemporarySplit(element, isTo = true) {
		let name = isTo ? 'data-split-to' : 'data-split-from';
		while (element &&
			!element.classList.contains('pagedjs_page_content') &&
			!element.classList.contains('pagedjs_footnote_inner_content')) {

			let value = element.getAttribute(name);
			if (value == 'temp-' + this.temporaryIndex) {
				element.removeAttribute(name);
			}

			element = element.parentElement;
		}
	}

	/**
	 * Returns the first child that overflows the bounds.
	 *
	 * There may be no children that overflow (the height might be extended
	 * by a sibling). In this case, this function returns NULL.
	 *
	 * @param {node} node
	 *   The parent node of the children we are searching.
	 * @param {array} bounds
	 *   The bounds of the page area.
	 * @returns {ChildNode | null | undefined}
	 *   The first overflowing child within the node.
	 */
	firstOverflowingChild(node, bounds) {
		let bLeft = Math.ceil(bounds.left);
		let bRight = Math.floor(bounds.right);
		let bTop = Math.ceil(bounds.top);
		let bBottom = Math.floor(bounds.bottom);
		let result = undefined;
		let skipRange = false;
		let parentBottomPaddingBorder = 0, parentBottomMargin = 0;

		if (isElement(node)) {
			let result = this.getAncestorPaddingBorderAndMarginSums(node);
			parentBottomPaddingBorder = result['border-bottom-width'];
			parentBottomMargin = result['margin-bottom'];
		}

		for (const child of node.childNodes) {
			if (child.tagName == "COLGROUP") {
				continue;
			}

			let pos = getBoundingClientRect(child);
			let bottomMargin = 0;

			if (isElement(child)) {
				let styles = window.getComputedStyle(child);
				let skipThis = false;

				bottomMargin = parseInt(styles["margin-bottom"]);

				if (child.dataset.rangeStartOverflow !== undefined) {
					skipRange = skipThis = true;
					result = null;
					// Don't continue. The start may also be the end.
				}

				if (child.dataset.rangeEndOverflow !== undefined) {
					skipRange = false;
					result = undefined;
					continue;
				}

				if (child.dataset.overflowTagged !== undefined) {
					continue;
				}

			}
			else {
				bottomMargin = parentBottomMargin;
			}

			if (skipRange) {
				continue;
			}

			let left = Math.ceil(pos.left);
			let right = Math.floor(pos.right);
			let top = Math.ceil(pos.top);
			let bottom = Math.floor(pos.bottom + bottomMargin +
				(node.lastChild == child ? parentBottomPaddingBorder : 0));

			if (!(pos.height + bottomMargin)) {
				continue;
			}

			if (left < bLeft || right > bRight || top < bTop || bottom > bBottom) {
				return child;
			}
		}

		return result;
	}

	removeHeightConstraint(element) {
		let pageBox = element.parentElement.closest('.pagedjs_page');
		pageBox.style.setProperty('--pagedjs-pagebox-height', '5000px');
		this.addTemporarySplit(element.parentElement, false);
	}

	restoreHeightConstraint(element) {
		let pageBox = element.parentElement.closest('.pagedjs_page');
		this.deleteTemporarySplit(element.parentElement, false);
		pageBox.style.removeProperty('--pagedjs-pagebox-height');
	}

	getUnconstrainedElementHeight(element, includeAncestors = true, includeTableHead = true) {
		this.removeHeightConstraint(element);
		let unconstrainedHeight = getBoundingClientRect(element).height;
		if (includeAncestors) {
			let extra = this.getAncestorPaddingBorderAndMarginSums(element.parentElement);
			['top', 'bottom'].forEach(direction => {
				unconstrainedHeight += extra[`padding-${direction}`] +
					extra[`border-${direction}-width`] +
					extra[`margin-${direction}`];
			});
		}
		if (includeTableHead) {
			unconstrainedHeight += this.getAncestorTheadSizes(element.parentElement);
		}
		this.restoreHeightConstraint(element);
		return unconstrainedHeight;
	}

	getRange(rangeStart, offset, rangeEnd) {
		let range = document.createRange();
		if (isText(rangeStart)) {
			range.setStart(rangeStart, offset);
		} else {
			range.selectNode(rangeStart);
		}

		// Additional nodes may have been added that will overflow further beyond
		// node. Include them in the range.
		rangeEnd = rangeEnd || rangeStart;
		range.setEndAfter(rangeEnd);
		return range;
	}

	startOfNewOverflow(node, rendered, bounds) {
		let childNode, done = false;
		let prev;
		let anyOverflowFound = false;
		let topNode = node;

		do {
			prev = node;
			do {
				let parentBottomPaddingBorder, parentBottomMargin;
				childNode = this.firstOverflowingChild(node, bounds);
				if (childNode) {
					anyOverflowFound = true;
				} else if (childNode === undefined) {
					// The overflow isn't caused by children. It could be caused by:
					// * a sibling div / td / element with height that stretches this
					//   element
					// * margin / padding on this element
					// In the former case, we want to ignore this node and take the
					// sibling. In the later case, we want to move this node.
					let intrinsicBottom = 0, intrinsicRight = 0;
					let childBounds = getBoundingClientRect(node);
					let styles;
					if (isElement(node)) {
						// Assume that any height is the result of matching the
						// height of surrounding content if there's no content.
						let result = this.getAncestorPaddingBorderAndMarginSums(node);
						parentBottomPaddingBorder = result['border-bottom-width'] + result['padding-bottom'];
						parentBottomMargin = result['margin-bottom'];

						if (node.childNodes.length) {
							let lastChild = node.lastChild;
							if (
								(isText(lastChild) && !node.dataset.overflowTagged) ||
								(!isText(lastChild) && !lastChild.dataset.overflowTagged)
								) {
									childBounds = getBoundingClientRect(lastChild);
									intrinsicRight = childBounds.right;
									intrinsicBottom = childBounds.bottom;
								}
						}
						else {
							// Do we count this node even though it has no children?
							// Seems to only be needed for BR.
							if (node instanceof HTMLBRElement) {
								intrinsicRight = childBounds.right;
								intrinsicBottom = childBounds.bottom;
							}
						}

					} else {
						intrinsicRight = childBounds.right;
						intrinsicBottom = childBounds.bottom;

						let result = this.getAncestorPaddingBorderAndMarginSums(node.parentElement);
						parentBottomPaddingBorder = result['border-bottom-width'];
						parentBottomMargin = result['margin-bottom'];
					}
					intrinsicBottom += parentBottomPaddingBorder + parentBottomMargin;
					if (intrinsicBottom <= bounds.bottom &&
						intrinsicRight <= bounds.right) {
						let ascended;
						do {
							ascended = false;
							do {
								node = node.nextElementSibling;
							} while (node && node.dataset.overflowTagged)
							if (!node && rendered !== prev) {
								ascended = true;
								prev = node = prev.parentElement;
							}
						} while (ascended && node && node !== topNode);
						if (!node || node == topNode) {
							return [null, false];
						}
					} else {
						// Node is causing the overflow via padding and margin or text content.
						done = true;
					}
				} else {
					// childNode is null. Overflowing children have been ignored and no other
					// overflowing children were found. Check the node's next sibling or one of
					// an ancestor.
					do {
						while (!node.nextElementSibling) {
							if (node == rendered) {
								return [null, false];
							}
							node = node.parentElement;
						}
						do {
							node = node.nextElementSibling;
						} while (node.nextElementSibling && node.dataset.overflowTagged);
					} while (node.dataset.overflowTagged);
				}
			} while (node && !childNode && !done);

			if (node) {
				node = childNode;
			}
		} while (node && !done);

		return [prev, anyOverflowFound];
	}

	tagAndCreateOverflowRange(startOfOverflow, rangeStart, rangeEnd, bounds, rendered) {
		let offset = 0;
		let start = bounds.left;
		let end = bounds.right;
		let vStart = bounds.top;
		let vEnd = bounds.bottom;
		let range;

		if (isText(rangeStart) && rangeStart.textContent.trim().length) {
			offset = this.textBreak(rangeStart, start, end, vStart, vEnd);
			if (offset === undefined) {
				// Adding split-to changed the CSS and meant we don't need to
				// split this node.
				let next = rangeStart;
				while (!next.nextElementSibling) {
					next = next.parentElement;
					if (next == rendered) {
						return;
					}
				}
				startOfOverflow = rangeStart = next.nextElementSibling;
			}
		}

		let previousElement = nodeBefore(rangeStart, rendered, true);
		let shouldContinue = true;
		let newRangeStart = rangeStart;
		while (!offset && previousElement && shouldContinue && (
			(isText(newRangeStart) && (
				newRangeStart.parentElement.dataset.previousBreakAfter == 'avoid' ||
				newRangeStart.parentElement.dataset.breakBefore == 'avoid'
			)) ||
			(!isText(newRangeStart) && (
				newRangeStart.dataset.previousBreakAfter == 'avoid' ||
				newRangeStart.dataset.breakBefore == 'avoid'
			)))) {
			// We are trying to avoid putting a break at newRangeStart.
			// See if we can move some of the content above into the overflow.
			let newPreviousElement = nodeBefore(previousElement, rendered, true);
			// Don't go back into stuff already rendered.
			if (!newPreviousElement || newPreviousElement.dataset.splitFrom) {
				shouldContinue = false;
			}
			else {
				newRangeStart = previousElement;
				previousElement = newPreviousElement;
			}
		}

		if (shouldContinue) {
			// We found earlier content that doesn't want to avoid having a break after it.
			// newRangeStart is the next node (new overflow start).
			rangeStart = newRangeStart;
		}

		// Set the start of the range and record on node or the previous element
		// that overflow was moved.
		let position = rangeStart;
		range = this.getRange(rangeStart, offset, rangeEnd);
		if (isText(rangeStart)) {
			rangeStart.parentElement.dataset.splitTo = rangeStart.parentElement.dataset.ref;
			rangeStart.parentElement.dataset.rangeStartOverflow = true;
			rangeStart.parentElement.dataset.overflowTagged = true;
			position = rangeStart.parentElement;
		} else {
			rangeStart.dataset.rangeStartOverflow = true;
		}

		rangeEnd = rangeEnd || rangeStart;
		if (isElement(rangeEnd)) {
			if (rangeStart.parentElement.closest(`[data-ref='${rangeEnd.dataset.ref}']`)) {
				let nextNode = nodeAfter(rangeEnd);
				if (nextNode) {
					nextNode.dataset.rangeEndOverflow = true;
					nextNode.dataset.overflowTagged = true;
				}
			}
			else {
				rangeEnd.dataset.rangeEndOverflow = true;
				rangeEnd.dataset.overflowTagged = true;
			}
		}
		else {
			rangeEnd.parentElement.dataset.rangeEndOverflow = true;
		}

		// Add splitTo
		while (position !== rendered) {
			if (position.previousSibling) {
				position.parentElement.dataset.splitTo = position.parentElement.dataset.ref;
			}
			position = position.parentElement;
		}

		// Tag ancestors in the range so we don't generate additional ranges
		// that then cause problems when removing the ranges.
		position = rangeStart;
		while (position.parentElement !== range.commonAncestorContainer) {
			position = position.parentElement;
			position.dataset.overflowTagged = true;
		}

		if (isElement(position)) {
			let stopAt = rangeEnd;
			while (stopAt.parentElement !== range.commonAncestorContainer) {
				stopAt = stopAt.parentElement;
			}

			while (position !== stopAt) {
				position = position.nextSibling;
				if (isElement(position)) {
					position.dataset.overflowTagged = true;
				}
			}
		}
		else {
			position = position.parentElement;
		}
		while (!position.nextElementSibling && position !== rendered) {
			position = position.parentElement;
			position.dataset.overflowTagged = true;
		}

		return range;
	}

	rowspanNeedsBreakAt(tableRow, rendered) {
		if (tableRow.nodeName !== 'TR') {
			return;
		}

		const table = parentOf(tableRow, "TABLE", rendered);
		if (!table) {
			return;
		}

		const rowspan = table.querySelector("[colspan]");
		if (!rowspan) {
			return;
		}

		let columnCount = 0;
		for (const cell of Array.from(table.rows[0].cells)) {
			columnCount += parseInt(cell.getAttribute("colspan") || "1");
		}
		if (tableRow.cells.length !== columnCount) {
			let previousRow = tableRow;
			let previousRowColumnCount;
			while (previousRow !== null) {
				previousRowColumnCount = 0;
				for (const cell of Array.from(previousRow.cells)) {
					previousRowColumnCount += parseInt(cell.getAttribute("colspan") || "1");
				}
				if (previousRowColumnCount === columnCount) {
					break;
				}
				previousRow = previousRow.previousElementSibling;
			}
			if (previousRowColumnCount === columnCount) {
				return previousRow;
			}
		}
	}

	findOverflow(rendered, bounds, source) {

		if (!this.hasOverflow(rendered, bounds) || rendered.dataset.overflowTagged) {
			return;
		}

		// The pattern here is:
		// Round the bounds towards the smaller rectangle (round up top & left and
		// round down bottom and right) and round the content towards the larger
		// rectangle (round down top and left and round up bottom and right). Then
		// use > and < to check if bounds are exceeded. That way portions of pixels
		// will be correctly handled - you can't render a fraction of a pixel so
		// bounds should have any fraction treated like that pixel isn't available
		// and content should have any fraction of a pixel treated like the whole
		// pixel is required.
		let end = bounds.right;
		let vEnd = bounds.bottom;
		let anyOverflowFound;

		// Find the deepest element that is the first in set of siblings with
		// overflow. There may be others. We just take the first we find and
		// are called again to check for additional instances.
		let node = rendered, startOfOverflow, check;

		while (isText(node)) {
			node = node.nextElementSibling;
		}

		[startOfOverflow, anyOverflowFound] = this.startOfNewOverflow(node, rendered, bounds);

		if (!anyOverflowFound) {
			return;
		}

		let startOfOverflowIsText = isText(startOfOverflow);
		if (startOfOverflowIsText && startOfOverflow.parentElement.dataset.overflowTagged ||
			(!startOfOverflowIsText && startOfOverflow.dataset.overflowTagged)) {
			return;
		}

		// The node we finished on may be within something asking not to have its
		// contents split. It - or a parent - may also have to be split because
		// the content is just too big for the page.
		// Resolve those requirements, deciding on a node that will be split in
		// the following way:
		// 1) Prefer the smallest node we can (start with the one we ended on).
		//    While going back up the ancestors, check that subsequent children
		//    of the ancestor are all entirely in overflow too. If they are, we
		//    can take a range starting at our initial node and going to the end
		//    of the ancestor's children.

		let rangeStart = check = node = startOfOverflow;
		let visibleSiblings = false;
		let rangeEnd = rendered.lastElementChild;

		do {
			let checkBounds = getBoundingClientRect(check);
			let hasOverflow = (checkBounds.bottom > vEnd || checkBounds.right > end);

			let rowspanNeedsBreakAt;

			if (hasOverflow && this.avoidBreakInside(check, rendered)) {
				rowspanNeedsBreakAt = this.rowspanNeedsBreakAt(check, rendered);
				if (rowspanNeedsBreakAt) {
					// No question - break earlier.
					rangeStart = rowspanNeedsBreakAt;
					rangeEnd = rendered.lastChild;
					break;
				}
				else {
					// If there is an element with overflow and it is within a
					// break-inside: avoid, we take the whole container, provided that it
					// will fit on a page by itself. But calculating whether it will fit
					// by itself is non-trivial. If it is within a dom structure, the
					// space available will be reduced by the containers. We can use the
					// current container (that will get duplicated) but there might be
					// subtle differences in styling due to the split-from class being
					// added. We therefore temporary add the split-from to the current
					// structure and find out how much space we need for the whole thing.
					//
					// To calculate whether we must split the element, we need to know its
					// unconstrained height. If it has been wrapped into another column
					// by .pagedjs_pagebox's display:grid, we need to temporarily lengthen
					// the current column to get the maximum width it would take. Go from
					// check's parent to simplify handling where check is a text node.
					let unconstrainedHeight;
					if (checkBounds.width > bounds.width) {
						unconstrainedHeight = this.getUnconstrainedElementHeight(check);
					}
					else {
						unconstrainedHeight = checkBounds.height;
					}

					let mustSplit = (unconstrainedHeight > bounds.height);

					if (!mustSplit) {
						// Move the whole thing.
						rangeStart = check;
					}
				}
			}

			let sibling = check, siblingBounds;
			do {
				sibling = sibling.nextSibling;
				siblingBounds = sibling ? getBoundingClientRect(sibling) : undefined;
			} while (sibling && !siblingBounds?.height);

			if (sibling && siblingBounds?.height && !rowspanNeedsBreakAt) {

				// Is the sibling entirely in overflow? If yes, so must all following
				// siblings be - add them to this range; they can't have anything we
				// want to keep on this page.
				if ((siblingBounds.left > end || siblingBounds.top > vEnd) && !visibleSiblings) {
					if (!rowspanNeedsBreakAt) {
						rangeEnd = check.parentElement.lastChild;
					}
				} else {
					visibleSiblings = true;
					rangeEnd = undefined;
				}
			}

			// Get the columns widths and make them attributes so removal of
			// overflow doesn't do strange things - they may be affecting
			// widths on this page.
			Array.from(check.parentElement.children).forEach((childNode) => {
				let style = getComputedStyle(childNode);
				childNode.width = style.width;
			});

			if (isElement(check) && Array.from(check.classList).filter(value => ['region-content', 'pagedjs_page_content'].includes(value)).length) {
				break;
			}
			check = check.parentElement;
		} while (check && check !== rendered);

		return this.tagAndCreateOverflowRange(startOfOverflow, rangeStart, rangeEnd, bounds, rendered);
	}

	findEndToken(rendered, source) {
		if (rendered.childNodes.length === 0) {
			return;
		}

		let lastChild = rendered.lastChild;

		let lastNodeIndex;
		while (lastChild && lastChild.lastChild) {
			if (!validNode(lastChild)) {
				// Only get elements with refs
				lastChild = lastChild.previousSibling;
			} else if (!validNode(lastChild.lastChild)) {
				// Deal with invalid dom items
				lastChild = prevValidNode(lastChild.lastChild);
				break;
			} else {
				lastChild = lastChild.lastChild;
			}
		}

		if (isText(lastChild)) {

			if (lastChild.parentNode.dataset.ref) {
				lastNodeIndex = indexOf(lastChild);
				lastChild = lastChild.parentNode;
			} else {
				lastChild = lastChild.previousSibling;
			}
		}

		let original = findElement(lastChild, source);

		if (lastNodeIndex) {
			original = original.childNodes[lastNodeIndex];
		}

		let after = nodeAfter(original);

		return this.breakAt(after);
	}

	textBreak(node, start, end, vStart, vEnd) {
		let wordwalker = words(node);
		let left = 0;
		let right = 0;
		let top = 0;
		let bottom = 0;
		let word, next, done, pos;
		let offset;

		// Margin bottom is needed when the node is in a block level element
		// such as a table, grid or flex, where margins don't collapse.
		// Temporarily add data-split-to as this may change margins too
		// (It always does in current code but let's not assume that).
		// With the split-to set, margin might be removed, resulting in us
		// not actually needing to split this text. In that case, the return
		// result will be undefined and the split should be done at the next
		// node. In this case we also keep the data-split-to=foo so the
		// styling that removes the need for the overflow remains active.
		// "Margin" includes bottom padding and border in this calculation.

		this.addTemporarySplit(node.parentElement);

		let parentAdditions = this.getAncestorPaddingBorderAndMarginSums(node.parentElement);
		parentAdditions = parentAdditions['padding-bottom'] +
			parentAdditions['border-bottom-width'] + parentAdditions['margin-bottom'];

		while (!done) {
			next = wordwalker.next();
			word = next.value;
			done = next.done;

			if (!word) {
				break;
			}

			pos = getBoundingClientRect(word);

			left = Math.floor(pos.left);
			right = Math.floor(pos.right);
			top = pos.top;
			bottom = pos.bottom;

			if (left > end || top > (vEnd - parentAdditions)) {
				offset = word.startOffset;
				break;
			}

			// The bounds won't be exceeded so we need >= rather than >.
			// Also below for the letters.
			if (right > end || bottom > (vEnd - parentAdditions)) {
				let letterwalker = letters(word);
				let letter, nextLetter, doneLetter;

				while (!doneLetter) {
					// Note that the letter walker continues to walk beyond the end of the word, until the end of the
					// text node.
					nextLetter = letterwalker.next();
					letter = nextLetter.value;
					doneLetter = nextLetter.done;

					if (!letter) {
						break;
					}

					pos = getBoundingClientRect(letter);
					right = pos.right;
					bottom = pos.bottom;

					if (right > end || bottom > (vEnd - parentAdditions)) {
						offset = letter.startOffset;
						done = true;

						break;
					}
				}
			}
		}

		// See comment above the addTemporarySplit call above for the offset ==
		// undefined part of why we may leave the temporary split-to attribute in
		// place. This should be overridden though if a break is to be avoided.
		// In that case,
		if (offset != undefined) {
			this.deleteTemporarySplit(node.parentElement);
		}

		// Don't get tricked into doing a split by whitespace at the start of a string.
		if (node.textContent.substring(0, offset).trim() == '') {
			return 0;
		}

		return offset;
	}

	removeOverflow(overflow, breakLetter) {
		let { startContainer } = overflow;
		let extracted = overflow.extractContents();

		this.hyphenateAtBreak(startContainer, breakLetter);

		return extracted;
	}

	hyphenateAtBreak(startContainer, breakLetter) {
		if (isText(startContainer)) {
			let startText = startContainer.textContent;
			let prevLetter = startText[startText.length - 1];

			// Add a hyphen if previous character is a letter or soft hyphen
			if (
				(breakLetter && /^\w|\u00AD$/.test(prevLetter) && /^\w|\u00AD$/.test(breakLetter)) ||
				(!breakLetter && prevLetter && /^\w|\u00AD$/.test(prevLetter))
			) {
				startContainer.parentNode.classList.add("pagedjs_hyphen");
				startContainer.textContent += this.settings.hyphenGlyph || "\u2011";
			}
		}
	}

	equalTokens(a, b) {
		if (!a || !b) {
			return false;
		}
		if (a["node"] && b["node"] && a["node"] !== b["node"]) {
			return false;
		}
		if (a["offset"] && b["offset"] && a["offset"] !== b["offset"]) {
			return false;
		}
		return true;
	}
}

EventEmitter(Layout.prototype);

export default Layout;