Commit f6aa5e6e authored by Fred Chasen's avatar Fred Chasen

Add filter hooks and modules

parent fa80584d
Pipeline #491 failed with stage
in 5 minutes and 15 seconds
......@@ -117,6 +117,7 @@ afterPreview(pages)
// Chunker
beforeParsed(content)
filter(content)
afterParsed(parsed)
beforePageLayout(page)
afterPageLayout(pageElement, page, breakToken)
......
......@@ -2536,6 +2536,11 @@
}
}
},
"clear-cut": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/clear-cut/-/clear-cut-2.0.2.tgz",
"integrity": "sha1-CC2zLsqkSjWKewhoUv4dVIC77tE="
},
"cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
......@@ -3873,8 +3878,7 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"aproba": {
"version": "1.2.0",
......@@ -3895,14 +3899,12 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
......@@ -3917,20 +3919,17 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"core-util-is": {
"version": "1.0.2",
......@@ -4047,8 +4046,7 @@
"inherits": {
"version": "2.0.4",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"ini": {
"version": "1.3.5",
......@@ -4060,7 +4058,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
......@@ -4075,7 +4072,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
......@@ -4083,14 +4079,12 @@
"minimist": {
"version": "1.2.5",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"minipass": {
"version": "2.9.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
......@@ -4109,7 +4103,6 @@
"version": "0.5.3",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "^1.2.5"
}
......@@ -4171,8 +4164,7 @@
"npm-normalize-package-bin": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"npm-packlist": {
"version": "1.4.8",
......@@ -4200,8 +4192,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"object-assign": {
"version": "4.1.1",
......@@ -4213,7 +4204,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
......@@ -4291,8 +4281,7 @@
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"safer-buffer": {
"version": "2.1.2",
......@@ -4328,7 +4317,6 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
......@@ -4348,7 +4336,6 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
......@@ -4392,14 +4379,12 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"yallist": {
"version": "3.1.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
}
}
},
......
......@@ -11,6 +11,7 @@
"dependencies": {
"@babel/polyfill": "^7.8.7",
"@babel/runtime": "^7.9.2",
"clear-cut": "^2.0.2",
"css-tree": "1.0.0-alpha.39",
"event-emitter": "^0.3.5"
},
......
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>Script elements should be ignored</title>
<style>
#cover {
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
break-after: page;
align-items: flex-end;
}
</style>
</head>
<body>
<div id="cover" class="title-page">
<h1>Title</h1>
</div>
<script id="pagedScript" src="../../../dist/paged.polyfill.js"></script>
</body>
</html>
const TIMEOUT = 10000; // Some book might take longer than this to renderer
describe("undisplayed", () => {
let page;
beforeAll(async () => {
page = await loadPage("filters/script-elements/script-elements.html");
return page.rendered;
}, TIMEOUT);
afterAll(async () => {
if (!DEBUG) {
await page.close();
}
});
it("script elements should not be appended in the layout", async () => {
let el = await page.$("#pagedScript");
expect(el).toBe(null);
});
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>display: none elements should be ignored</title>
<style>
#cover {
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
break-after: page;
align-items: flex-end;
}
#displayNoneStyle {
display: flex;
}
div#displayNoneStyle {
display: none;
}
#displayNoneWithPageBreak {
display: none;
break-after: page;
}
</style>
</head>
<body>
<div id="cover" class="title-page">
<h1>Title</h1>
</div>
<div id="displayNoneInlineStyle" style="display: none;"></div>
<div id="displayNoneStyle"></div>
<div id="displayNoneWithPageBreak"></div>
<div>Content</div>
<script src="../../../dist/paged.polyfill.js"></script>
</body>
</html>
const TIMEOUT = 10000; // Some book might take longer than this to renderer
describe("undisplayed", () => {
let page;
beforeAll(async () => {
page = await loadPage("filters/undisplayed/undisplayed.html");
return page.rendered;
}, TIMEOUT);
afterAll(async () => {
if (!DEBUG) {
await page.close();
}
});
it("display: none elements should not be appended in the layout", async () => {
let el = await page.$("#displayNoneInlineStyle");
expect(el).toBe(null);
el = await page.$("#displayNoneStyle");
expect(el).toBe(null);
el = await page.$("#displayNoneWithPageBreak");
expect(el).not.toBe(null);
});
});
......@@ -90,6 +90,7 @@ class Chunker {
this.hooks = {};
this.hooks.beforeParsed = new Hook(this);
this.hooks.filter = new Hook(this);
this.hooks.afterParsed = new Hook(this);
this.hooks.beforePageLayout = new Hook(this);
this.hooks.layout = new Hook(this);
......@@ -139,6 +140,8 @@ class Chunker {
parsed = new ContentParser(content);
this.hooks.filter.triggerSync(content);
this.source = parsed;
this.breakToken = undefined;
......@@ -162,7 +165,7 @@ class Chunker {
}
this.rendered = true;
this.pagesArea.style.setProperty("--pagedjs-page-count", this.total);
this.pagesArea.style.setProperty("--pagedjs-page-count", this.total);
await this.hooks.afterRendered.trigger(this.pages, this);
......
import {UUID} from "../utils/utils";
import {isElement, isIgnorable, nextSignificantNode, previousSignificantNode} from "../utils/dom";
/**
* Render a flow of text offscreen
......@@ -23,7 +22,6 @@ class ContentParser {
let fragment = range.createContextualFragment(markup);
this.addRefs(fragment);
this.removeEmpty(fragment);
return fragment;
}
......@@ -38,7 +36,6 @@ class ContentParser {
// }
this.addRefs(contents);
this.removeEmpty(contents);
return contents;
}
......@@ -47,7 +44,7 @@ class ContentParser {
var treeWalker = document.createTreeWalker(
content,
NodeFilter.SHOW_ELEMENT,
{ acceptNode: function(node) { return NodeFilter.FILTER_ACCEPT; } },
null,
false
);
......@@ -70,96 +67,10 @@ class ContentParser {
}
}
removeEmpty(content) {
const treeWalker = document.createTreeWalker(
content,
NodeFilter.SHOW_TEXT,
{ acceptNode: function(node) {
if (node.textContent.length > 1 && isIgnorable(node)) {
// Do not touch the content if text is pre-formatted
let parent = node.parentNode;
let pre = isElement(parent) && parent.closest("pre");
if (pre) {
return NodeFilter.FILTER_REJECT;
}
const nextSibling = previousSignificantNode(node);
const previousSibling = nextSignificantNode(node);
if (nextSibling === null && previousSibling === null) {
// we should not remove a Node that does not have any siblings.
node.textContent = " ";
return NodeFilter.FILTER_REJECT;
}
if (nextSibling === null) {
// we can safely remove this node
return NodeFilter.FILTER_ACCEPT;
}
if (previousSibling === null) {
// we can safely remove this node
return NodeFilter.FILTER_ACCEPT;
}
// replace the content with a single space
node.textContent = " ";
// TODO: we also need to preserve sequences of white spaces when the parent has "white-space" rule:
// pre
// Sequences of white space are preserved. Lines are only broken at newline characters in the source and at <br> elements.
//
// pre-wrap
// Sequences of white space are preserved. Lines are broken at newline characters, at <br>, and as necessary to fill line boxes.
//
// pre-line
// Sequences of white space are collapsed. Lines are broken at newline characters, at <br>, and as necessary to fill line boxes.
//
// break-spaces
// The behavior is identical to that of pre-wrap, except that:
// - Any sequence of preserved white space always takes up space, including at the end of the line.
// - A line breaking opportunity exists after every preserved white space character, including between white space characters.
// - Such preserved spaces take up space and do not hang, and thus affect the box’s intrinsic sizes (min-content size and max-content size).
//
// See: https://developer.mozilla.org/en-US/docs/Web/CSS/white-space#Values
return NodeFilter.FILTER_REJECT;
} else {
return NodeFilter.FILTER_REJECT;
}
} },
false
);
let node;
let current;
node = treeWalker.nextNode();
while(node) {
current = node;
node = treeWalker.nextNode();
current.parentNode.removeChild(current);
}
}
find(ref) {
return this.refs[ref];
}
// isWrapper(element) {
// return wrappersRegex.test(element.nodeName);
// }
isText(node) {
return node.tagName === "TAG";
}
isElement(node) {
return node.nodeType === 1;
}
hasChildren(node) {
return node.childNodes && node.childNodes.length;
}
destroy() {
this.refs = undefined;
this.dom = undefined;
......
import Handler from "../handler";
import {filterTree} from "../../utils/dom";
class CommentsFilter extends Handler {
constructor(chunker, polisher, caller) {
super(chunker, polisher, caller);
}
filter(content) {
filterTree(content, null, NodeFilter.SHOW_COMMENT);
}
}
export default CommentsFilter;
import WhiteSpaceFilter from "./whitespace";
import CommentsFilter from "./comments";
import ScriptsFilter from "./scripts";
import UndisplayedFilter from "./undisplayed";
export default [
WhiteSpaceFilter,
CommentsFilter,
ScriptsFilter,
UndisplayedFilter
];
import Handler from "../handler";
class ScriptsFilter extends Handler {
constructor(chunker, polisher, caller) {
super(chunker, polisher, caller);
}
filter(content) {
content.querySelectorAll("script").forEach( script => { script.remove(); });
}
}
export default ScriptsFilter;
import Handler from "../handler";
import csstree from "css-tree";
import { calculateSpecificity } from "clear-cut";
const pageBreakValuesWithNoEffectOnLayout = [
"auto",
"avoid",
"avoid-page",
"avoid-column",
"avoid-region"
];
class UndisplayedFilter extends Handler {
constructor(chunker, polisher, caller) {
super(chunker, polisher, caller);
this.displayRules = {};
}
onDeclaration(declaration, dItem, dList, rule) {
if (declaration.property === "display") {
let selector = csstree.generate(rule.ruleNode.prelude);
let value = declaration.value.children.first().name;
let display = {
value: value,
selector: selector,
specificity: calculateSpecificity(selector),
important: declaration.important,
matches: []
};
selector.split(",").forEach((s) => {
this.displayRules[s] = display;
});
}
}
afterParsed(content) {
let { matches, selectors } = this.sortDisplayedSelectors(content, this.displayRules);
// Find matching elements that have display styles
for (let i = 0; i < matches.length; i++) {
let element = matches[i];
let selector = selectors[i];
let displayValue = selector[selector.length-1].value;
if(this.removable(element) && displayValue === "none") {
element.remove();
}
}
// Find elements that have inline styles
let styledElements = content.querySelectorAll("[style]");
for (let i = 0; i < styledElements.length; i++) {
let element = styledElements[i];
if (this.removable(element)) {
element.remove();
}
}
}
sorter(a, b) {
if (a.important && !b.important) {
return 1;
}
if (b.important && !a.important) {
return -1;
}
return a.specificity - b.specificity;
}
sortDisplayedSelectors(content, displayRules=[]) {
let matches = [];
let selectors = [];
for (let d in displayRules) {
let displayItem = displayRules[d];
let elements = Array.from(content.querySelectorAll(displayItem.selector));
for (let e of elements) {
if (matches.includes(e)) {
let index = matches.indexOf(e);
selectors[index].push(displayItem);
selectors[index] = selectors[index].sort(this.sorter);
} else {
matches.push(e);
selectors.push([displayItem]);
}
}
}
return { matches, selectors };
}
removable(element) {
let breakBefore = element.getAttribute("data-break-before");
let breakAfter = element.getAttribute("data-break-after");
if(breakBefore && !pageBreakValuesWithNoEffectOnLayout.includes(breakBefore)) {
return false;
}
if (breakAfter && !pageBreakValuesWithNoEffectOnLayout.includes(breakAfter)) {
return false;
}
if (element.style &&
element.style.display !== "" &&
element.style.display !== "none") {
return false;
}
return true;
}
}
export default UndisplayedFilter;
import Handler from "../handler";
import {isElement, isIgnorable, nextSignificantNode, previousSignificantNode, filterTree} from "../../utils/dom";
class WhiteSpaceFilter extends Handler {
constructor(chunker, polisher, caller) {
super(chunker, polisher, caller);
}
filter(content) {
filterTree(content, (node) => {
return this.filterEmpty(node);
}, NodeFilter.SHOW_TEXT);
}
filterEmpty(node) {
if (node.textContent.length > 1 && isIgnorable(node)) {
// Do not touch the content if text is pre-formatted
let parent = node.parentNode;
let pre = isElement(parent) && parent.closest("pre");
if (pre) {
return NodeFilter.FILTER_REJECT;
}
const nextSibling = previousSignificantNode(node);
const previousSibling = nextSignificantNode(node);
if (nextSibling === null && previousSibling === null) {
// we should not remove a Node that does not have any siblings.
node.textContent = " ";
return NodeFilter.FILTER_REJECT;
}
if (nextSibling === null) {
// we can safely remove this node
return NodeFilter.FILTER_ACCEPT;
}
if (previousSibling === null) {
// we can safely remove this node
return NodeFilter.FILTER_ACCEPT;
}
// replace the content with a single space
node.textContent = " ";
// TODO: we also need to preserve sequences of white spaces when the parent has "white-space" rule:
// pre
// Sequences of white space are preserved. Lines are only broken at newline characters in the source and at <br> elements.
//
// pre-wrap
// Sequences of white space are preserved. Lines are broken at newline characters, at <br>, and as necessary to fill line boxes.
//
// pre-line
// Sequences of white space are collapsed. Lines are broken at newline characters, at <br>, and as necessary to fill line boxes.
//
// break-spaces
// The behavior is identical to that of pre-wrap, except that:
// - Any sequence of preserved white space always takes up space, including at the end of the line.
// - A line breaking opportunity exists after every preserved white space character, including between white space characters.
// - Such preserved spaces take up space and do not hang, and thus affect the box’s intrinsic sizes (min-content size and max-content size).
//
// See: https://developer.mozilla.org/en-US/docs/Web/CSS/white-space#Values
return NodeFilter.FILTER_REJECT;
} else {
return NodeFilter.FILTER_REJECT;
}
}
}
export default WhiteSpaceFilter;
......@@ -593,3 +593,21 @@ export function nextSignificantNode(sib) {
if (!isIgnorable(sib)) return sib;
}
}
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);
}
}
import pagedMediaHandlers from "../modules/paged-media/index";
import generatedContentHandlers from "../modules/generated-content/index";
import filters from "../modules/filters/index";
import EventEmitter from "event-emitter";
import pipe from "event-emitter/pipe";
export let registeredHandlers = [...pagedMediaHandlers, ...generatedContentHandlers];
export let registeredHandlers = [...pagedMediaHandlers, ...generatedContentHandlers, ...filters];
export class Handlers {
constructor(chunker, polisher, caller) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment