/
index.js
131 lines (112 loc) · 3.76 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
const cssEsc = require("cssesc");
const CSSOM = require("cssom");
const fp = require("fastify-plugin");
const { JSDOM } = require("jsdom");
/**
* @author Frazer Smith
* @description Decorator plugin that adds function that parses
* and tidies CSS passed.
* @param {Function} server - Fastify instance.
*/
async function plugin(server) {
/**
* @param {string} html - Valid HTML.
* @param {object} options - Function config values.
* @param {string=} options.backgroundColor - Color to replace document's original
* `background-color` CSS property for `<div>` elements with.
* @param {string=} options.fonts - Font to replace document's original font(s), can be
* single font or comma separated list i.e `Arial, Sans Serif`.
* @returns {string} HTML with tidied CSS.
*/
function tidyCss(html, options = {}) {
const dom = new JSDOM(html);
let styles = dom.window.document.querySelectorAll("style");
let newBackgroundColor;
if (options.backgroundColor) {
newBackgroundColor = String(options.backgroundColor);
}
let newFonts;
if (options.fonts) {
newFonts = String(options.fonts);
}
// Create style element inside head if none already exist
if (styles.length === 0 && (newFonts || newBackgroundColor)) {
const element = dom.window.document.createElement("style");
element.innerHTML = "div {}";
dom.window.document.head.appendChild(element);
styles = dom.window.document.querySelectorAll("style");
}
styles.forEach((style) => {
const element = style;
// Remove optional type attribute
if (element.hasAttribute("type")) {
element.removeAttribute("type");
}
let styleObj = CSSOM.parse(element.innerHTML);
styleObj.cssRules.forEach((styleRule) => {
// Replace default font
if (
newFonts &&
(styleRule.style["font-family"] || styles.length === 1)
) {
styleRule.style.setProperty("font-family", newFonts);
}
/**
* Font family names containing any non-alphabetical characters
* other than hyphens should be quoted.
* See https://developer.mozilla.org/en-US/docs/Web/CSS/font-family#valid_family_names
*/
if (styleRule.style["font-family"]) {
const fonts = styleRule.style["font-family"].split(",");
const parsedFonts = [];
fonts.forEach((font) => {
if (/[^a-zA-Z-]+/.test(font.trim())) {
parsedFonts.push(
// Stop escaping of <style> elements and code injection
cssEsc(font.replace(/<\/style>/gm, "").trim(), {
quotes: "double",
wrap: true,
})
);
} else {
parsedFonts.push(font.trim());
}
});
styleRule.style.setProperty(
"font-family",
parsedFonts.join(", ")
);
}
/**
* Stop pages overrunning the next page, leading to overlapping text.
* "page-break-inside" is a legacy property, replaced by "break-inside".
* "page-break-inside" should be treated by browsers as an alias of "break-inside"
*/
if (styleRule.selectorText.substring(0, 3) === "div") {
styleRule.style.setProperty("page-break-inside", "avoid");
// Replace default color
if (newBackgroundColor) {
styleRule.style.setProperty(
"background-color",
newBackgroundColor
);
}
}
});
/**
* Remove HTML comment tags wrapping CSS and redundant semi-colons
* generated by Poppler.
* `while` loop is needed to prevent incomplete multi-character
* sanitization (CWE-20 and CWE-116).
*/
styleObj = styleObj.toString().replace(/;}/gm, "}");
while (/<!--|-->/m.test(styleObj)) {
styleObj = styleObj.replace(/<!--|-->/gm, "");
}
element.innerHTML = styleObj;
});
return dom.serialize();
}
server.decorate("tidyCss", tidyCss);
}
module.exports = fp(plugin, { fastify: "3.x", name: "tidy-css" });