diff --git a/specs/breaks/unforced-breaks/long-table.html b/specs/breaks/unforced-breaks/long-table.html new file mode 100644 index 0000000000000000000000000000000000000000..129f1815d6d53421e70fec8baef4c1e8560f0552 --- /dev/null +++ b/specs/breaks/unforced-breaks/long-table.html @@ -0,0 +1,145 @@ + + + + + + Long table + + + + + +
+

Table

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#Name
1a
1b
1c
1d
1e
1f
1g
1h
1i
1j
1k
1l
1m
1n
1o
1p
1q
1r
1s
1t
1u
1v
+
+
+ + diff --git a/specs/breaks/unforced-breaks/unforced-breaks.js b/specs/breaks/unforced-breaks/unforced-breaks.js new file mode 100644 index 0000000000000000000000000000000000000000..af8bbe9615b35d14dd926642c6737218f1c58a19 --- /dev/null +++ b/specs/breaks/unforced-breaks/unforced-breaks.js @@ -0,0 +1,32 @@ +const TIMEOUT = 10000; // Some book might take longer than this to renderer + +describe("unforced-breaks-drop-break-after-then-break-inside", () => { + let page; + beforeAll(async () => { + page = await loadPage("breaks/unforced-breaks/long-table.html"); + return page.rendered; + }, TIMEOUT); + + afterAll(async () => { + if (!DEBUG) { + await page.close(); + } + }); + + it("should render 2 pages", async () => { + let pages = await page.$$eval(".pagedjs_page", (r) => { + return r.length; + }); + + expect(pages).toEqual(2); + }); + + if (!DEBUG) { + it("should create a pdf", async () => { + let pdf = await page.pdf(PDF_SETTINGS); + + expect(pdf).toMatchPDFSnapshot(1); + expect(pdf).toMatchPDFSnapshot(2); + }); + } +}); diff --git a/src/chunker/breaktoken.js b/src/chunker/breaktoken.js index a128f2a0f3bf8655e2c5b86835c01032a5747ffa..ec8661ec9d37fc7400441b6c87135b069593c450 100644 --- a/src/chunker/breaktoken.js +++ b/src/chunker/breaktoken.js @@ -26,4 +26,4 @@ class BreakToken { } -export default BreakToken; \ No newline at end of file +export default BreakToken; diff --git a/src/chunker/chunker.js b/src/chunker/chunker.js index 7758a63e3f9dda0303e320104ac11bf9f2a50e58..276dc3850054c9c576f68f1c8e8b3e7bb922c135 100644 --- a/src/chunker/chunker.js +++ b/src/chunker/chunker.js @@ -4,7 +4,7 @@ import EventEmitter from "event-emitter"; import Hook from "../utils/hook"; import Queue from "../utils/queue"; import {requestIdleCallback} from "../utils/utils"; -import {breakInsideAvoidNode, breakInsideAvoidParentNode} from "../utils/dom"; +import { breakInsideAvoidNode, breakAfterAvoidNode, breakInsideAvoidParentNode, breakAfterAvoidParentNode, nextSignificantNode } from "../utils/dom"; const MAX_PAGES = false; const MAX_LAYOUTS = false; @@ -265,6 +265,31 @@ class Chunker { } } + async handleOverruledBreakAfter(node) { + const breakAfterNode = breakAfterAvoidNode(node); + if (breakAfterNode) { + const nextNode = nextSignificantNode(node); + if (nextNode) { + const page = this.addPage(); + // QUESTION: should we emit the temporary page? + //this.emit("page", page); + const newBreakToken = await page.layout(node.parentNode, null, this.maxChars); + let overruled = false; + // indicates that content does not fit into the page + if (newBreakToken && typeof newBreakToken.node === "undefined") { + // we need to overrule this definition, remove "break-after: avoid" directive and reflow the previous page + delete breakAfterNode.dataset.breakAfter; + breakAfterNode.style = breakAfterNode.style + ";break-after: auto;"; + delete nextNode.dataset.previousBreakAfter; + overruled = true; + } + // remove the temporary page + this.removePages(this.pages.length - 1); + return overruled; + } + } + } + async handleOverruledBreakInside(node) { const breakInsideNode = breakInsideAvoidNode(node); if (breakInsideNode) { @@ -272,7 +297,7 @@ class Chunker { // QUESTION: should we emit the temporary page? //this.emit("page", page); const newBreakToken = await page.layout(breakInsideNode, null, this.maxChars); - const breakInsideAvoidParent = newBreakToken && breakInsideAvoidParentNode(newBreakToken.node); + const breakInsideAvoidParent = newBreakToken && newBreakToken.node && breakInsideAvoidParentNode(newBreakToken.node); // we are breaking inside a "break-inside: avoid" node which mean that we cannot comply with this rule! const overruleBreakInside = breakInsideAvoidParent && breakInsideAvoidParent.dataset && breakInsideNode.dataset && breakInsideNode.dataset.ref === breakInsideAvoidParent.dataset.ref; if (overruleBreakInside) { @@ -347,8 +372,10 @@ class Chunker { let reflowCurrentPage = false; if (breakToken && breakToken.node) { await this.handleBreaks(breakToken.node); - const overruleBreakInside = await this.handleOverruledBreakInside(breakToken.node); - if (overruleBreakInside) { + if (await this.handleOverruledBreakAfter(breakToken.node)) { + reflowCurrentPage = typeof page !== "undefined"; + breakToken = page && page.startToken; + } else if (await this.handleOverruledBreakInside(breakToken.node)) { reflowCurrentPage = typeof page !== "undefined"; breakToken = page && page.startToken; } diff --git a/src/chunker/layout.js b/src/chunker/layout.js index e1dca0fe6631be65f55c33713ce416c991b46deb..c220dd925a0830b833ac293f59951aca40cde3fc 100644 --- a/src/chunker/layout.js +++ b/src/chunker/layout.js @@ -67,13 +67,13 @@ class Layout { let next; let hasRenderedContent = false; - let newBreakToken; + let newBreakToken = {node: undefined}; let length = 0; let prevBreakToken = breakToken || new BreakToken(start); - while (!done && !newBreakToken) { + while (!done && typeof newBreakToken.node !== undefined) { next = walker.next(); prevNode = node; node = next.value; @@ -90,8 +90,10 @@ class Layout { newBreakToken = this.findBreakToken(wrapper, source, bounds, prevBreakToken); if (newBreakToken && newBreakToken.equals(prevBreakToken)) { + // TODO: we should probably return an objet (BreakToken?) with additional info console.warn("Unable to layout item: ", prevNode); - return undefined; + delete newBreakToken.node; + return newBreakToken; } return newBreakToken; } @@ -115,7 +117,8 @@ class Layout { if (newBreakToken && newBreakToken.equals(prevBreakToken)) { console.warn("Unable to layout item: ", node); - return undefined; + delete newBreakToken.node; + return newBreakToken; } length = 0; @@ -169,7 +172,8 @@ class Layout { if (newBreakToken && newBreakToken.equals(prevBreakToken)) { console.warn("Unable to layout item: ", node); - return undefined; + delete newBreakToken.node; + return newBreakToken; } if (newBreakToken) { diff --git a/src/utils/dom.js b/src/utils/dom.js index 0c1b3f58dd15b26583b252b439fe2be6d5431fd0..6c8fc73fc4c36bef7759d23ac7ad228190b67ea9 100644 --- a/src/utils/dom.js +++ b/src/utils/dom.js @@ -610,6 +610,15 @@ export function breakInsideAvoidParentNode(node) { return null; } +export function breakAfterAvoidParentNode(node) { + while ((node = node.parentNode)) { + if (node && node.dataset && node.dataset.breakAfter === "avoid") { + return node; + } + } + return null; +} + export function breakInsideAvoidNode(node) { if (node && node.dataset && node.dataset.breakInside === "avoid") { return node; @@ -620,7 +629,23 @@ export function breakInsideAvoidNode(node) { if (child && child.dataset && child.dataset.breakInside === "avoid") { return child; } - return breakInsideAvoidNode(child); + breakInsideAvoidNode(child); + } + } + return null; +} + +export function breakAfterAvoidNode(node) { + if (node && node.dataset && node.dataset.breakAfter === "avoid") { + return node; + } + let children = node.children; + if (children) { + for (const child of children) { + if (child && child.dataset && child.dataset.breakAfter === "avoid") { + return child; + } + breakAfterAvoidNode(child); } } return null;