UNPKG

pagedjs

Version:

Chunks up a document into paged media flows and applies print styles

750 lines (652 loc) 17.7 kB
export function isElement(node) { return node && node.nodeType === 1; } export function isText(node) { return node && node.nodeType === 3; } export function* walk(start, limiter) { let node = start; while (node) { yield node; if (node.childNodes.length) { node = node.firstChild; } else if (node.nextSibling) { if (limiter && node === limiter) { node = undefined; break; } node = node.nextSibling; } else { while (node) { node = node.parentNode; if (limiter && node === limiter) { node = undefined; break; } if (node && node.nextSibling) { node = node.nextSibling; break; } } } } } export function nodeAfter(node, limiter) { if (limiter && node === limiter) { return; } let significantNode = nextSignificantNode(node); if (significantNode) { return significantNode; } if (node.parentNode) { while ((node = node.parentNode)) { if (limiter && node === limiter) { return; } significantNode = nextSignificantNode(node); if (significantNode) { return significantNode; } } } } export function nodeBefore(node, limiter) { if (limiter && node === limiter) { return; } let significantNode = previousSignificantNode(node); if (significantNode) { return significantNode; } if (node.parentNode) { while ((node = node.parentNode)) { if (limiter && node === limiter) { return; } significantNode = previousSignificantNode(node); if (significantNode) { return significantNode; } } } } export function elementAfter(node, limiter) { let after = nodeAfter(node, limiter); while (after && after.nodeType !== 1) { after = nodeAfter(after, limiter); } return after; } export function elementBefore(node, limiter) { let before = nodeBefore(node, limiter); while (before && before.nodeType !== 1) { before = nodeBefore(before, limiter); } return before; } export function displayedElementAfter(node, limiter) { let after = elementAfter(node, limiter); while (after && after.dataset.undisplayed) { after = elementAfter(after, limiter); } return after; } export function displayedElementBefore(node, limiter) { let before = elementBefore(node, limiter); while (before && before.dataset.undisplayed) { before = elementBefore(before, limiter); } return before; } export function stackChildren(currentNode, stacked) { let stack = stacked || []; stack.unshift(currentNode); let children = currentNode.children; for (var i = 0, length = children.length; i < length; i++) { stackChildren(children[i], stack); } return stack; } export function rebuildAncestors(node) { let parent, ancestor; let ancestors = []; let added = []; let fragment = document.createDocumentFragment(); // Handle rowspan on table if (node.nodeName === "TR") { let previousRow = node.previousElementSibling; let previousRowDistance = 1; while (previousRow) { // previous row has more columns, might indicate a rowspan. if (previousRow.childElementCount > node.childElementCount) { const initialColumns = Array.from(node.children); while (node.firstChild) { node.firstChild.remove(); } let k = 0; for (let j = 0; j < previousRow.children.length; j++) { let column = previousRow.children[j]; if (column.rowSpan && column.rowSpan > previousRowDistance) { const duplicatedColumn = column.cloneNode(true); // Adjust rowspan value duplicatedColumn.rowSpan = column.rowSpan - previousRowDistance; // Add the column to the row node.appendChild(duplicatedColumn); } else { // Fill the gap with the initial columns (if exists) const initialColumn = initialColumns[k++]; // The initial column can be undefined if the newly created table has less columns than the original table if (initialColumn) { node.appendChild(initialColumn); } } } } previousRow = previousRow.previousElementSibling; previousRowDistance++; } } // Gather all ancestors let element = node; while(element.parentNode && element.parentNode.nodeType === 1) { ancestors.unshift(element.parentNode); element = element.parentNode; } for (var i = 0; i < ancestors.length; i++) { ancestor = ancestors[i]; parent = ancestor.cloneNode(false); parent.setAttribute("data-split-from", parent.getAttribute("data-ref")); // ancestor.setAttribute("data-split-to", parent.getAttribute("data-ref")); if (parent.hasAttribute("id")) { let dataID = parent.getAttribute("id"); parent.setAttribute("data-id", dataID); parent.removeAttribute("id"); } // This is handled by css :not, but also tidied up here if (parent.hasAttribute("data-break-before")) { parent.removeAttribute("data-break-before"); } if (parent.hasAttribute("data-previous-break-after")) { parent.removeAttribute("data-previous-break-after"); } if (added.length) { let container = added[added.length-1]; container.appendChild(parent); } else { fragment.appendChild(parent); } added.push(parent); // rebuild table rows if (parent.nodeName === "TD" && ancestor.parentElement.contains(ancestor)) { let td = ancestor; let prev = parent; while ((td = td.previousElementSibling)) { let sib = td.cloneNode(false); parent.parentElement.insertBefore(sib, prev); prev = sib; } } } added = undefined; return fragment; } /* export function split(bound, cutElement, breakAfter) { let needsRemoval = []; let index = indexOf(cutElement); if (!breakAfter && index === 0) { return; } if (breakAfter && index === (cutElement.parentNode.children.length - 1)) { return; } // Create a fragment with rebuilt ancestors let fragment = rebuildAncestors(cutElement); // Clone cut if (!breakAfter) { let clone = cutElement.cloneNode(true); let ref = cutElement.parentNode.getAttribute('data-ref'); let parent = fragment.querySelector("[data-ref='" + ref + "']"); parent.appendChild(clone); needsRemoval.push(cutElement); } // Remove all after cut let next = nodeAfter(cutElement, bound); while (next) { let clone = next.cloneNode(true); let ref = next.parentNode.getAttribute('data-ref'); let parent = fragment.querySelector("[data-ref='" + ref + "']"); parent.appendChild(clone); needsRemoval.push(next); next = nodeAfter(next, bound); } // Remove originals needsRemoval.forEach((node) => { if (node) { node.remove(); } }); // Insert after bounds bound.parentNode.insertBefore(fragment, bound.nextSibling); return [bound, bound.nextSibling]; } */ export function needsBreakBefore(node) { if( typeof node !== "undefined" && typeof node.dataset !== "undefined" && typeof node.dataset.breakBefore !== "undefined" && (node.dataset.breakBefore === "always" || node.dataset.breakBefore === "page" || node.dataset.breakBefore === "left" || node.dataset.breakBefore === "right" || node.dataset.breakBefore === "recto" || node.dataset.breakBefore === "verso") ) { return true; } return false; } export function needsBreakAfter(node) { if( typeof node !== "undefined" && typeof node.dataset !== "undefined" && typeof node.dataset.breakAfter !== "undefined" && (node.dataset.breakAfter === "always" || node.dataset.breakAfter === "page" || node.dataset.breakAfter === "left" || node.dataset.breakAfter === "right" || node.dataset.breakAfter === "recto" || node.dataset.breakAfter === "verso") ) { return true; } return false; } export function needsPreviousBreakAfter(node) { if( typeof node !== "undefined" && typeof node.dataset !== "undefined" && typeof node.dataset.previousBreakAfter !== "undefined" && (node.dataset.previousBreakAfter === "always" || node.dataset.previousBreakAfter === "page" || node.dataset.previousBreakAfter === "left" || node.dataset.previousBreakAfter === "right" || node.dataset.previousBreakAfter === "recto" || node.dataset.previousBreakAfter === "verso") ) { return true; } return false; } export function needsPageBreak(node, previousSignificantNode) { if (typeof node === "undefined" || !previousSignificantNode || isIgnorable(node)) { return false; } if (node.dataset && node.dataset.undisplayed) { return false; } let previousSignificantNodePage = previousSignificantNode.dataset ? previousSignificantNode.dataset.page : undefined; if (typeof previousSignificantNodePage === "undefined") { const nodeWithNamedPage = getNodeWithNamedPage(previousSignificantNode); if (nodeWithNamedPage) { previousSignificantNodePage = nodeWithNamedPage.dataset.page; } } let currentNodePage = node.dataset ? node.dataset.page : undefined; if (typeof currentNodePage === "undefined") { const nodeWithNamedPage = getNodeWithNamedPage(node, previousSignificantNode); if (nodeWithNamedPage) { currentNodePage = nodeWithNamedPage.dataset.page; } } return currentNodePage !== previousSignificantNodePage; } export function *words(node) { let currentText = node.nodeValue; let max = currentText.length; let currentOffset = 0; let currentLetter; let range; const significantWhitespaces = node.parentElement && node.parentElement.nodeName === "PRE"; while (currentOffset < max) { currentLetter = currentText[currentOffset]; if (/^[\S\u202F\u00A0]$/.test(currentLetter) || significantWhitespaces) { if (!range) { range = document.createRange(); range.setStart(node, currentOffset); } } else { if (range) { range.setEnd(node, currentOffset); yield range; range = undefined; } } currentOffset += 1; } if (range) { range.setEnd(node, currentOffset); yield range; } } export function *letters(wordRange) { let currentText = wordRange.startContainer; let max = currentText.length; let currentOffset = wordRange.startOffset; // let currentLetter; let range; while(currentOffset < max) { // currentLetter = currentText[currentOffset]; range = document.createRange(); range.setStart(currentText, currentOffset); range.setEnd(currentText, currentOffset+1); yield range; currentOffset += 1; } } export function isContainer(node) { let container; if (typeof node.tagName === "undefined") { return true; } if (node.style && node.style.display === "none") { return false; } switch (node.tagName) { // Inline case "A": case "ABBR": case "ACRONYM": case "B": case "BDO": case "BIG": case "BR": case "BUTTON": case "CITE": case "CODE": case "DFN": case "EM": case "I": case "IMG": case "INPUT": case "KBD": case "LABEL": case "MAP": case "OBJECT": case "Q": case "SAMP": case "SCRIPT": case "SELECT": case "SMALL": case "SPAN": case "STRONG": case "SUB": case "SUP": case "TEXTAREA": case "TIME": case "TT": case "VAR": case "P": case "H1": case "H2": case "H3": case "H4": case "H5": case "H6": case "FIGCAPTION": case "BLOCKQUOTE": case "PRE": case "LI": case "TD": case "DT": case "DD": case "VIDEO": case "CANVAS": container = false; break; default: container = true; } return container; } export function cloneNode(n, deep=false) { return n.cloneNode(deep); } export function findElement(node, doc, forceQuery) { const ref = node.getAttribute("data-ref"); return findRef(ref, doc, forceQuery); } export function findRef(ref, doc, forceQuery) { if (!forceQuery && doc.indexOfRefs && doc.indexOfRefs[ref]) { return doc.indexOfRefs[ref]; } else { return doc.querySelector(`[data-ref='${ref}']`); } } export function validNode(node) { if (isText(node)) { return true; } if (isElement(node) && node.dataset.ref) { return true; } return false; } export function prevValidNode(node) { while (!validNode(node)) { if (node.previousSibling) { node = node.previousSibling; } else { node = node.parentNode; } if (!node) { break; } } return node; } export function nextValidNode(node) { while (!validNode(node)) { if (node.nextSibling) { node = node.nextSibling; } else { node = node.parentNode.nextSibling; } if (!node) { break; } } return node; } export function indexOf(node) { let parent = node.parentNode; if (!parent) { return 0; } return Array.prototype.indexOf.call(parent.childNodes, node); } export function child(node, index) { return node.childNodes[index]; } export function isVisible(node) { if (isElement(node) && window.getComputedStyle(node).display !== "none") { return true; } else if (isText(node) && hasTextContent(node) && window.getComputedStyle(node.parentNode).display !== "none") { return true; } return false; } export function hasContent(node) { if (isElement(node)) { return true; } else if (isText(node) && node.textContent.trim().length) { return true; } return false; } export function hasTextContent(node) { if (isElement(node)) { let child; for (var i = 0; i < node.childNodes.length; i++) { child = node.childNodes[i]; if (child && isText(child) && child.textContent.trim().length) { return true; } } } else if (isText(node) && node.textContent.trim().length) { return true; } return false; } export function indexOfTextNode(node, parent) { if (!isText(node)) { return -1; } let nodeTextContent = node.textContent; let child; let index = -1; for (var i = 0; i < parent.childNodes.length; i++) { child = parent.childNodes[i]; if (child.nodeType === 3) { let text = parent.childNodes[i].textContent; if (text.includes(nodeTextContent)) { index = i; break; } } } return index; } /** * Throughout, whitespace is defined as one of the characters * "\t" TAB \u0009 * "\n" LF \u000A * "\r" CR \u000D * " " SPC \u0020 * * This does not use Javascript's "\s" because that includes non-breaking * spaces (and also some other characters). */ /** * Determine if a node should be ignored by the iterator functions. * taken from https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace#Whitespace_helper_functions * * @param {Node} node An object implementing the DOM1 |Node| interface. * @return {boolean} true if the node is: * 1) A |Text| node that is all whitespace * 2) A |Comment| node * and otherwise false. */ export function isIgnorable(node) { return (node.nodeType === 8) || // A comment node ((node.nodeType === 3) && isAllWhitespace(node)); // a text node, all whitespace } /** * Determine whether a node's text content is entirely whitespace. * * @param {Node} node A node implementing the |CharacterData| interface (i.e., a |Text|, |Comment|, or |CDATASection| node * @return {boolean} true if all of the text content of |nod| is whitespace, otherwise false. */ export function isAllWhitespace(node) { return !(/[^\t\n\r ]/.test(node.textContent)); } /** * Version of |previousSibling| that skips nodes that are entirely * whitespace or comments. (Normally |previousSibling| is a property * of all DOM nodes that gives the sibling node, the node that is * a child of the same parent, that occurs immediately before the * reference node.) * * @param {ChildNode} sib The reference node. * @return {Node|null} Either: * 1) The closest previous sibling to |sib| that is not ignorable according to |is_ignorable|, or * 2) null if no such node exists. */ export function previousSignificantNode(sib) { while ((sib = sib.previousSibling)) { if (!isIgnorable(sib)) return sib; } return null; } function getNodeWithNamedPage(node, limiter) { if (node && node.dataset && node.dataset.page) { return node; } if (node.parentNode) { while ((node = node.parentNode)) { if (limiter && node === limiter) { return; } if (node.dataset && node.dataset.page) { return node; } } } return null; } export function breakInsideAvoidParentNode(node) { while ((node = node.parentNode)) { if (node && node.dataset && node.dataset.breakInside === "avoid") { return node; } } return null; } /** * Find a parent with a given node name. * @param {Node} node - initial Node * @param {string} nodeName - node name (eg. "TD", "TABLE", "STRONG"...) * @param {Node} limiter - go up to the parent until there's no more parent or the current node is equals to the limiter * @returns {Node|undefined} - Either: * 1) The closest parent for a the given node name, or * 2) undefined if no such node exists. */ export function parentOf(node, nodeName, limiter) { if (limiter && node === limiter) { return; } if (node.parentNode) { while ((node = node.parentNode)) { if (limiter && node === limiter) { return; } if (node.nodeName === nodeName) { return node; } } } } /** * Version of |nextSibling| that skips nodes that are entirely * whitespace or comments. * * @param {ChildNode} sib The reference node. * @return {Node|null} Either: * 1) The closest next sibling to |sib| that is not ignorable according to |is_ignorable|, or * 2) null if no such node exists. */ export function nextSignificantNode(sib) { while ((sib = sib.nextSibling)) { if (!isIgnorable(sib)) return sib; } return null; } export function filterTree(content, func, what) { const treeWalker = document.createTreeWalker( content || this.dom, what || NodeFilter.SHOW_ALL, func ? { acceptNode: func } : null, false ); let node; let current; node = treeWalker.nextNode(); while(node) { current = node; node = treeWalker.nextNode(); current.parentNode.removeChild(current); } }