single-file-core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,262 @@
1
+ /*
2
+ * Copyright 2010-2020 Gildas Lormeau
3
+ * contact : gildas.lormeau <at> gmail.com
4
+ *
5
+ * This file is part of SingleFile.
6
+ *
7
+ * The code in this file is free software: you can redistribute it and/or
8
+ * modify it under the terms of the GNU Affero General Public License
9
+ * (GNU AGPL) as published by the Free Software Foundation, either version 3
10
+ * of the License, or (at your option) any later version.
11
+ *
12
+ * The code in this file is distributed in the hope that it will be useful,
13
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
15
+ * General Public License for more details.
16
+ *
17
+ * As additional permission under GNU AGPL version 3 section 7, you may
18
+ * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU
19
+ * AGPL normally required by section 4, provided you include this license
20
+ * notice and a URL through which recipients can access the Corresponding
21
+ * Source.
22
+ */
23
+
24
+ // Derived from the work of Kirill Maltsev - https://github.com/posthtml/htmlnano
25
+
26
+ // Source: https://github.com/kangax/html-minifier/issues/63
27
+ const booleanAttributes = [
28
+ "allowfullscreen",
29
+ "async",
30
+ "autofocus",
31
+ "autoplay",
32
+ "checked",
33
+ "compact",
34
+ "controls",
35
+ "declare",
36
+ "default",
37
+ "defaultchecked",
38
+ "defaultmuted",
39
+ "defaultselected",
40
+ "defer",
41
+ "disabled",
42
+ "enabled",
43
+ "formnovalidate",
44
+ "hidden",
45
+ "indeterminate",
46
+ "inert",
47
+ "ismap",
48
+ "itemscope",
49
+ "loop",
50
+ "multiple",
51
+ "muted",
52
+ "nohref",
53
+ "noresize",
54
+ "noshade",
55
+ "novalidate",
56
+ "nowrap",
57
+ "open",
58
+ "pauseonexit",
59
+ "readonly",
60
+ "required",
61
+ "reversed",
62
+ "scoped",
63
+ "seamless",
64
+ "selected",
65
+ "sortable",
66
+ "truespeed",
67
+ "typemustmatch",
68
+ "visible"
69
+ ];
70
+
71
+ const noWhitespaceCollapseElements = ["script", "style", "pre", "textarea"];
72
+
73
+ // Source: https://www.w3.org/TR/html4/sgml/dtd.html#events (Generic Attributes)
74
+ const safeToRemoveAttrs = [
75
+ "id",
76
+ "class",
77
+ "style",
78
+ "lang",
79
+ "dir",
80
+ "onclick",
81
+ "ondblclick",
82
+ "onmousedown",
83
+ "onmouseup",
84
+ "onmouseover",
85
+ "onmousemove",
86
+ "onmouseout",
87
+ "onkeypress",
88
+ "onkeydown",
89
+ "onkeyup"
90
+ ];
91
+
92
+ const redundantAttributes = {
93
+ "form": {
94
+ "method": "get"
95
+ },
96
+ "script": {
97
+ "language": "javascript",
98
+ "type": "text/javascript",
99
+ // Remove attribute if the function returns false
100
+ "charset": node => {
101
+ // The charset attribute only really makes sense on “external” SCRIPT elements:
102
+ // http://perfectionkills.com/optimizing-html/#8_script_charset
103
+ return !node.getAttribute("src");
104
+ }
105
+ },
106
+ "style": {
107
+ "media": "all",
108
+ "type": "text/css"
109
+ },
110
+ "link": {
111
+ "media": "all"
112
+ }
113
+ };
114
+
115
+ const REGEXP_WHITESPACE = /[ \t\f\r]+/g;
116
+ const REGEXP_NEWLINE = /[\n]+/g;
117
+ const REGEXP_ENDS_WHITESPACE = /^\s+$/;
118
+ const NodeFilter_SHOW_ALL = 4294967295;
119
+ const Node_ELEMENT_NODE = 1;
120
+ const Node_TEXT_NODE = 3;
121
+ const Node_COMMENT_NODE = 8;
122
+
123
+ const modules = [
124
+ collapseBooleanAttributes,
125
+ mergeTextNodes,
126
+ collapseWhitespace,
127
+ removeComments,
128
+ removeEmptyAttributes,
129
+ removeRedundantAttributes,
130
+ compressJSONLD,
131
+ node => mergeElements(node, "style", (node, previousSibling) => node.parentElement && node.parentElement.tagName == "HEAD" && node.media == previousSibling.media && node.title == previousSibling.title)
132
+ ];
133
+
134
+ export {
135
+ process
136
+ };
137
+
138
+ function process(doc, options) {
139
+ removeEmptyInlineElements(doc);
140
+ const nodesWalker = doc.createTreeWalker(doc.documentElement, NodeFilter_SHOW_ALL, null, false);
141
+ let node = nodesWalker.nextNode();
142
+ while (node) {
143
+ const deletedNode = modules.find(module => module(node, options));
144
+ const previousNode = node;
145
+ node = nodesWalker.nextNode();
146
+ if (deletedNode) {
147
+ previousNode.remove();
148
+ }
149
+ }
150
+ }
151
+
152
+ function collapseBooleanAttributes(node) {
153
+ if (node.nodeType == Node_ELEMENT_NODE) {
154
+ Array.from(node.attributes).forEach(attribute => {
155
+ if (booleanAttributes.includes(attribute.name)) {
156
+ node.setAttribute(attribute.name, "");
157
+ }
158
+ });
159
+ }
160
+ }
161
+
162
+ function mergeTextNodes(node) {
163
+ if (node.nodeType == Node_TEXT_NODE) {
164
+ if (node.previousSibling && node.previousSibling.nodeType == Node_TEXT_NODE) {
165
+ node.textContent = node.previousSibling.textContent + node.textContent;
166
+ node.previousSibling.remove();
167
+ }
168
+ }
169
+ }
170
+
171
+ function mergeElements(node, tagName, acceptMerge) {
172
+ if (node.nodeType == Node_ELEMENT_NODE && node.tagName.toLowerCase() == tagName.toLowerCase()) {
173
+ let previousSibling = node.previousSibling;
174
+ const previousSiblings = [];
175
+ while (previousSibling && previousSibling.nodeType == Node_TEXT_NODE && !previousSibling.textContent.trim()) {
176
+ previousSiblings.push(previousSibling);
177
+ previousSibling = previousSibling.previousSibling;
178
+ }
179
+ if (previousSibling && previousSibling.nodeType == Node_ELEMENT_NODE && previousSibling.tagName == node.tagName && acceptMerge(node, previousSibling)) {
180
+ node.textContent = previousSibling.textContent + node.textContent;
181
+ previousSiblings.forEach(node => node.remove());
182
+ previousSibling.remove();
183
+ }
184
+ }
185
+ }
186
+
187
+ function collapseWhitespace(node, options) {
188
+ if (node.nodeType == Node_TEXT_NODE) {
189
+ let element = node.parentElement;
190
+ const spacePreserved = element.getAttribute(options.PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME) == "";
191
+ if (!spacePreserved) {
192
+ const textContent = node.textContent;
193
+ let noWhitespace = noWhitespaceCollapse(element);
194
+ while (noWhitespace) {
195
+ element = element.parentElement;
196
+ noWhitespace = element && noWhitespaceCollapse(element);
197
+ }
198
+ if ((!element || noWhitespace) && textContent.length > 1) {
199
+ node.textContent = textContent.replace(REGEXP_WHITESPACE, getWhiteSpace(node)).replace(REGEXP_NEWLINE, "\n");
200
+ }
201
+ }
202
+ }
203
+ }
204
+
205
+ function getWhiteSpace(node) {
206
+ return node.parentElement && node.parentElement.tagName == "HEAD" ? "\n" : " ";
207
+ }
208
+
209
+ function noWhitespaceCollapse(element) {
210
+ return element && !noWhitespaceCollapseElements.includes(element.tagName.toLowerCase());
211
+ }
212
+
213
+ function removeComments(node) {
214
+ if (node.nodeType == Node_COMMENT_NODE && node.parentElement.tagName != "HTML") {
215
+ return !node.textContent.toLowerCase().trim().startsWith("[if");
216
+ }
217
+ }
218
+
219
+ function removeEmptyAttributes(node) {
220
+ if (node.nodeType == Node_ELEMENT_NODE) {
221
+ Array.from(node.attributes).forEach(attribute => {
222
+ if (safeToRemoveAttrs.includes(attribute.name.toLowerCase())) {
223
+ const attributeValue = node.getAttribute(attribute.name);
224
+ if (attributeValue == "" || (attributeValue || "").match(REGEXP_ENDS_WHITESPACE)) {
225
+ node.removeAttribute(attribute.name);
226
+ }
227
+ }
228
+ });
229
+ }
230
+ }
231
+
232
+ function removeRedundantAttributes(node) {
233
+ if (node.nodeType == Node_ELEMENT_NODE) {
234
+ const tagRedundantAttributes = redundantAttributes[node.tagName.toLowerCase()];
235
+ if (tagRedundantAttributes) {
236
+ Object.keys(tagRedundantAttributes).forEach(redundantAttributeName => {
237
+ const tagRedundantAttributeValue = tagRedundantAttributes[redundantAttributeName];
238
+ if (typeof tagRedundantAttributeValue == "function" ? tagRedundantAttributeValue(node) : node.getAttribute(redundantAttributeName) == tagRedundantAttributeValue) {
239
+ node.removeAttribute(redundantAttributeName);
240
+ }
241
+ });
242
+ }
243
+ }
244
+ }
245
+
246
+ function compressJSONLD(node) {
247
+ if (node.nodeType == Node_ELEMENT_NODE && node.tagName == "SCRIPT" && node.type == "application/ld+json" && node.textContent.trim()) {
248
+ try {
249
+ node.textContent = JSON.stringify(JSON.parse(node.textContent));
250
+ } catch (error) {
251
+ // ignored
252
+ }
253
+ }
254
+ }
255
+
256
+ function removeEmptyInlineElements(doc) {
257
+ doc.querySelectorAll("style, script:not([src])").forEach(element => {
258
+ if (!element.textContent.trim()) {
259
+ element.remove();
260
+ }
261
+ });
262
+ }
@@ -0,0 +1,180 @@
1
+ /*
2
+ * Copyright 2010-2020 Gildas Lormeau
3
+ * contact : gildas.lormeau <at> gmail.com
4
+ *
5
+ * This file is part of SingleFile.
6
+ *
7
+ * The code in this file is free software: you can redistribute it and/or
8
+ * modify it under the terms of the GNU Affero General Public License
9
+ * (GNU AGPL) as published by the Free Software Foundation, either version 3
10
+ * of the License, or (at your option) any later version.
11
+ *
12
+ * The code in this file is distributed in the hope that it will be useful,
13
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
15
+ * General Public License for more details.
16
+ *
17
+ * As additional permission under GNU AGPL version 3 section 7, you may
18
+ * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU
19
+ * AGPL normally required by section 4, provided you include this license
20
+ * notice and a URL through which recipients can access the Corresponding
21
+ * Source.
22
+ */
23
+
24
+ const SELF_CLOSED_TAG_NAMES = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"];
25
+
26
+ const Node_ELEMENT_NODE = 1;
27
+ const Node_TEXT_NODE = 3;
28
+ const Node_COMMENT_NODE = 8;
29
+
30
+ // see https://www.w3.org/TR/html5/syntax.html#optional-tags
31
+ const OMITTED_START_TAGS = [
32
+ { tagName: "head", accept: element => !element.childNodes.length || element.childNodes[0].nodeType == Node_ELEMENT_NODE },
33
+ { tagName: "body", accept: element => !element.childNodes.length }
34
+ ];
35
+ const OMITTED_END_TAGS = [
36
+ { tagName: "html", accept: next => !next || next.nodeType != Node_COMMENT_NODE },
37
+ { tagName: "head", accept: next => !next || (next.nodeType != Node_COMMENT_NODE && (next.nodeType != Node_TEXT_NODE || !startsWithSpaceChar(next.textContent))) },
38
+ { tagName: "body", accept: next => !next || next.nodeType != Node_COMMENT_NODE },
39
+ { tagName: "li", accept: (next, element) => (!next && element.parentElement && (element.parentElement.tagName == "UL" || element.parentElement.tagName == "OL")) || (next && ["LI"].includes(next.tagName)) },
40
+ { tagName: "dt", accept: next => !next || ["DT", "DD"].includes(next.tagName) },
41
+ { tagName: "p", accept: next => next && ["ADDRESS", "ARTICLE", "ASIDE", "BLOCKQUOTE", "DETAILS", "DIV", "DL", "FIELDSET", "FIGCAPTION", "FIGURE", "FOOTER", "FORM", "H1", "H2", "H3", "H4", "H5", "H6", "HEADER", "HR", "MAIN", "NAV", "OL", "P", "PRE", "SECTION", "TABLE", "UL"].includes(next.tagName) },
42
+ { tagName: "dd", accept: next => !next || ["DT", "DD"].includes(next.tagName) },
43
+ { tagName: "rt", accept: next => !next || ["RT", "RP"].includes(next.tagName) },
44
+ { tagName: "rp", accept: next => !next || ["RT", "RP"].includes(next.tagName) },
45
+ { tagName: "optgroup", accept: next => !next || ["OPTGROUP"].includes(next.tagName) },
46
+ { tagName: "option", accept: next => !next || ["OPTION", "OPTGROUP"].includes(next.tagName) },
47
+ { tagName: "colgroup", accept: next => !next || (next.nodeType != Node_COMMENT_NODE && (next.nodeType != Node_TEXT_NODE || !startsWithSpaceChar(next.textContent))) },
48
+ { tagName: "caption", accept: next => !next || (next.nodeType != Node_COMMENT_NODE && (next.nodeType != Node_TEXT_NODE || !startsWithSpaceChar(next.textContent))) },
49
+ { tagName: "thead", accept: next => !next || ["TBODY", "TFOOT"].includes(next.tagName) },
50
+ { tagName: "tbody", accept: next => !next || ["TBODY", "TFOOT"].includes(next.tagName) },
51
+ { tagName: "tfoot", accept: next => !next },
52
+ { tagName: "tr", accept: next => !next || ["TR"].includes(next.tagName) },
53
+ { tagName: "td", accept: next => !next || ["TD", "TH"].includes(next.tagName) },
54
+ { tagName: "th", accept: next => !next || ["TD", "TH"].includes(next.tagName) }
55
+ ];
56
+ const TEXT_NODE_TAGS = ["style", "script", "xmp", "iframe", "noembed", "noframes", "plaintext", "noscript"];
57
+
58
+ export {
59
+ process
60
+ };
61
+
62
+ function process(doc, compressHTML) {
63
+ const docType = doc.doctype;
64
+ let docTypeString = "";
65
+ if (docType) {
66
+ docTypeString = "<!DOCTYPE " + docType.nodeName;
67
+ if (docType.publicId) {
68
+ docTypeString += " PUBLIC \"" + docType.publicId + "\"";
69
+ if (docType.systemId)
70
+ docTypeString += " \"" + docType.systemId + "\"";
71
+ } else if (docType.systemId)
72
+ docTypeString += " SYSTEM \"" + docType.systemId + "\"";
73
+ if (docType.internalSubset)
74
+ docTypeString += " [" + docType.internalSubset + "]";
75
+ docTypeString += "> ";
76
+ }
77
+ return docTypeString + serialize(doc.documentElement, compressHTML);
78
+ }
79
+
80
+ function serialize(node, compressHTML, isSVG) {
81
+ if (node.nodeType == Node_TEXT_NODE) {
82
+ return serializeTextNode(node);
83
+ } else if (node.nodeType == Node_COMMENT_NODE) {
84
+ return serializeCommentNode(node);
85
+ } else if (node.nodeType == Node_ELEMENT_NODE) {
86
+ return serializeElement(node, compressHTML, isSVG);
87
+ }
88
+ }
89
+
90
+ function serializeTextNode(textNode) {
91
+ const parentNode = textNode.parentNode;
92
+ let parentTagName;
93
+ if (parentNode && parentNode.nodeType == Node_ELEMENT_NODE) {
94
+ parentTagName = parentNode.tagName.toLowerCase();
95
+ }
96
+ if (!parentTagName || TEXT_NODE_TAGS.includes(parentTagName)) {
97
+ if (parentTagName == "script") {
98
+ return textNode.textContent.replace(/<\//gi, "<\\/").replace(/\/>/gi, "\\/>");
99
+ }
100
+ return textNode.textContent;
101
+ } else {
102
+ return textNode.textContent.replace(/&/g, "&amp;").replace(/\u00a0/g, "&nbsp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
103
+ }
104
+ }
105
+
106
+ function serializeCommentNode(commentNode) {
107
+ return "<!--" + commentNode.textContent + "-->";
108
+ }
109
+
110
+ function serializeElement(element, compressHTML, isSVG) {
111
+ const tagName = element.tagName.toLowerCase();
112
+ const omittedStartTag = compressHTML && OMITTED_START_TAGS.find(omittedStartTag => tagName == omittedStartTag.tagName && omittedStartTag.accept(element));
113
+ let content = "";
114
+ if (!omittedStartTag || element.attributes.length) {
115
+ content = "<" + tagName;
116
+ Array.from(element.attributes).forEach(attribute => content += serializeAttribute(attribute, element, compressHTML));
117
+ content += ">";
118
+ }
119
+ if (element.tagName == "TEMPLATE" && !element.childNodes.length) {
120
+ content += element.innerHTML;
121
+ } else {
122
+ Array.from(element.childNodes).forEach(childNode => content += serialize(childNode, compressHTML, isSVG || tagName == "svg"));
123
+ }
124
+ const omittedEndTag = compressHTML && OMITTED_END_TAGS.find(omittedEndTag => tagName == omittedEndTag.tagName && omittedEndTag.accept(element.nextSibling, element));
125
+ if (isSVG || (!omittedEndTag && !SELF_CLOSED_TAG_NAMES.includes(tagName))) {
126
+ content += "</" + tagName + ">";
127
+ }
128
+ return content;
129
+ }
130
+
131
+ function serializeAttribute(attribute, element, compressHTML) {
132
+ const name = attribute.name;
133
+ let content = "";
134
+ if (!name.match(/["'>/=]/)) {
135
+ let value = attribute.value;
136
+ if (compressHTML && name == "class") {
137
+ value = Array.from(element.classList).map(className => className.trim()).join(" ");
138
+ }
139
+ let simpleQuotesValue;
140
+ value = value.replace(/&/g, "&amp;").replace(/\u00a0/g, "&nbsp;");
141
+ if (value.includes("\"")) {
142
+ if (value.includes("'") || !compressHTML) {
143
+ value = value.replace(/"/g, "&quot;");
144
+ } else {
145
+ simpleQuotesValue = true;
146
+ }
147
+ }
148
+ const invalidUnquotedValue = !compressHTML || !value.match(/^[^ \t\n\f\r'"`=<>]+$/);
149
+ content += " ";
150
+ if (!attribute.namespace) {
151
+ content += name;
152
+ } else if (attribute.namespaceURI == "http://www.w3.org/XML/1998/namespace") {
153
+ content += "xml:" + name;
154
+ } else if (attribute.namespaceURI == "http://www.w3.org/2000/xmlns/") {
155
+ if (name !== "xmlns") {
156
+ content += "xmlns:";
157
+ }
158
+ content += name;
159
+ } else if (attribute.namespaceURI == "http://www.w3.org/1999/xlink") {
160
+ content += "xlink:" + name;
161
+ } else {
162
+ content += name;
163
+ }
164
+ if (value != "") {
165
+ content += "=";
166
+ if (invalidUnquotedValue) {
167
+ content += simpleQuotesValue ? "'" : "\"";
168
+ }
169
+ content += value;
170
+ if (invalidUnquotedValue) {
171
+ content += simpleQuotesValue ? "'" : "\"";
172
+ }
173
+ }
174
+ }
175
+ return content;
176
+ }
177
+
178
+ function startsWithSpaceChar(textContent) {
179
+ return Boolean(textContent.match(/^[ \t\n\f\r]/));
180
+ }
@@ -0,0 +1,42 @@
1
+ /*
2
+ * Copyright 2010-2020 Gildas Lormeau
3
+ * contact : gildas.lormeau <at> gmail.com
4
+ *
5
+ * This file is part of SingleFile.
6
+ *
7
+ * The code in this file is free software: you can redistribute it and/or
8
+ * modify it under the terms of the GNU Affero General Public License
9
+ * (GNU AGPL) as published by the Free Software Foundation, either version 3
10
+ * of the License, or (at your option) any later version.
11
+ *
12
+ * The code in this file is distributed in the hope that it will be useful,
13
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
15
+ * General Public License for more details.
16
+ *
17
+ * As additional permission under GNU AGPL version 3 section 7, you may
18
+ * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU
19
+ * AGPL normally required by section 4, provided you include this license
20
+ * notice and a URL through which recipients can access the Corresponding
21
+ * Source.
22
+ */
23
+
24
+ import * as fontsAltMinifier from "./css-fonts-alt-minifier.js";
25
+ import * as fontsMinifier from "./css-fonts-minifier";
26
+ import * as matchedRules from "./css-matched-rules.js";
27
+ import * as mediasAltMinifier from "./css-medias-alt-minifier.js";
28
+ import * as cssRulesMinifier from "./css-rules-minifier.js";
29
+ import * as imagesAltMinifier from "./html-images-alt-minifier.js";
30
+ import * as htmlMinifier from "./html-minifier.js";
31
+ import * as serializer from "./html-serializer.js";
32
+
33
+ export {
34
+ fontsAltMinifier,
35
+ fontsMinifier,
36
+ matchedRules,
37
+ mediasAltMinifier,
38
+ cssRulesMinifier,
39
+ imagesAltMinifier,
40
+ htmlMinifier,
41
+ serializer
42
+ };
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "single-file-core",
3
+ "version": "1.0.0",
4
+ "description": "SingleFile Core",
5
+ "author": "Gildas Lormeau",
6
+ "license": "AGPL-3.0-or-later",
7
+ "scripts": {
8
+ "test": "echo \"Error: no test specified\" && exit 1"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/gildas-lormeau/single-file-core.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/gildas-lormeau/single-file-core/issues"
16
+ },
17
+ "homepage": "https://github.com/gildas-lormeau/single-file-core#readme"
18
+ }