|
@@ -18,6 +18,8 @@
|
|
|
* along with SingleFile. If not, see <http://www.gnu.org/licenses/>.
|
|
* along with SingleFile. If not, see <http://www.gnu.org/licenses/>.
|
|
|
*/
|
|
*/
|
|
|
|
|
|
|
|
|
|
+/* global Blob */
|
|
|
|
|
+
|
|
|
this.SingleFileCore = this.SingleFileCore || (() => {
|
|
this.SingleFileCore = this.SingleFileCore || (() => {
|
|
|
|
|
|
|
|
const SELECTED_CONTENT_ATTRIBUTE_NAME = "data-single-file-selected-content";
|
|
const SELECTED_CONTENT_ATTRIBUTE_NAME = "data-single-file-selected-content";
|
|
@@ -214,6 +216,38 @@ this.SingleFileCore = this.SingleFileCore || (() => {
|
|
|
class DOMProcessor {
|
|
class DOMProcessor {
|
|
|
constructor(options) {
|
|
constructor(options) {
|
|
|
this.options = options;
|
|
this.options = options;
|
|
|
|
|
+ if (this.options.displayStats) {
|
|
|
|
|
+ this.stats = {
|
|
|
|
|
+ discarded: {
|
|
|
|
|
+ htmlBytes: 0,
|
|
|
|
|
+ hiddenElements: 0,
|
|
|
|
|
+ imports: 0,
|
|
|
|
|
+ scripts: 0,
|
|
|
|
|
+ objects: 0,
|
|
|
|
|
+ audioSource: 0,
|
|
|
|
|
+ videoSource: 0,
|
|
|
|
|
+ frames: 0,
|
|
|
|
|
+ cssRules: 0,
|
|
|
|
|
+ canvas: 0,
|
|
|
|
|
+ styleSheets: 0,
|
|
|
|
|
+ resources: 0
|
|
|
|
|
+ },
|
|
|
|
|
+ processed: {
|
|
|
|
|
+ htmlBytes: 0,
|
|
|
|
|
+ hiddenElements: 0,
|
|
|
|
|
+ imports: 0,
|
|
|
|
|
+ scripts: 0,
|
|
|
|
|
+ objects: 0,
|
|
|
|
|
+ audioSource: 0,
|
|
|
|
|
+ videoSource: 0,
|
|
|
|
|
+ frames: 0,
|
|
|
|
|
+ cssRules: 0,
|
|
|
|
|
+ canvas: 0,
|
|
|
|
|
+ styleSheets: 0,
|
|
|
|
|
+ resources: 0
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
this.baseURI = options.url;
|
|
this.baseURI = options.url;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -240,6 +274,9 @@ this.SingleFileCore = this.SingleFileCore || (() => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async retrieveResources(onloadListener) {
|
|
async retrieveResources(onloadListener) {
|
|
|
|
|
+ if (this.options.displayStats) {
|
|
|
|
|
+ this.stats.processed.resources = batchRequest.getMaxResources();
|
|
|
|
|
+ }
|
|
|
await batchRequest.run(onloadListener, this.options);
|
|
await batchRequest.run(onloadListener, this.options);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -258,9 +295,19 @@ this.SingleFileCore = this.SingleFileCore || (() => {
|
|
|
title = titleElement.textContent.trim();
|
|
title = titleElement.textContent.trim();
|
|
|
}
|
|
}
|
|
|
const matchTitle = this.baseURI.match(/([^/]*)\/?(\.html?.*)$/) || this.baseURI.match(/\/\/([^/]*)\/?$/);
|
|
const matchTitle = this.baseURI.match(/([^/]*)\/?(\.html?.*)$/) || this.baseURI.match(/\/\/([^/]*)\/?$/);
|
|
|
|
|
+ let size;
|
|
|
|
|
+ if (this.options.displayStats) {
|
|
|
|
|
+ size = new Blob([this.doc.documentElement.outerHTML]).size;
|
|
|
|
|
+ }
|
|
|
|
|
+ const content = this.dom.serialize(this.options);
|
|
|
|
|
+ if (this.options.displayStats) {
|
|
|
|
|
+ this.stats.processed.htmlBytes = new Blob([content]).size;
|
|
|
|
|
+ this.stats.discarded.htmlBytes += size - this.stats.processed.htmlBytes;
|
|
|
|
|
+ }
|
|
|
return {
|
|
return {
|
|
|
|
|
+ stats: this.stats,
|
|
|
title: title || (this.baseURI && matchTitle ? matchTitle[1] : ""),
|
|
title: title || (this.baseURI && matchTitle ? matchTitle[1] : ""),
|
|
|
- content: this.dom.serialize(this.options)
|
|
|
|
|
|
|
+ content
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -285,14 +332,26 @@ this.SingleFileCore = this.SingleFileCore || (() => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
removeDiscardedResources() {
|
|
removeDiscardedResources() {
|
|
|
- this.doc.querySelectorAll("applet, meta[http-equiv=refresh], object:not([type=\"image/svg+xml\"]):not([type=\"image/svg-xml\"]):not([type=\"text/html\"]), embed:not([src*=\".svg\"]), link[rel*=preload], link[rel*=prefetch]").forEach(element => element.remove());
|
|
|
|
|
|
|
+ const objectElements = this.doc.querySelectorAll("applet, meta[http-equiv=refresh], object:not([type=\"image/svg+xml\"]):not([type=\"image/svg-xml\"]):not([type=\"text/html\"]), embed:not([src*=\".svg\"]), link[rel*=preload], link[rel*=prefetch]");
|
|
|
|
|
+ if (this.options.displayStats) {
|
|
|
|
|
+ this.stats.discarded.objects = objectElements.length;
|
|
|
|
|
+ }
|
|
|
|
|
+ objectElements.forEach(element => element.remove());
|
|
|
this.doc.querySelectorAll("[onload]").forEach(element => element.removeAttribute("onload"));
|
|
this.doc.querySelectorAll("[onload]").forEach(element => element.removeAttribute("onload"));
|
|
|
this.doc.querySelectorAll("[onerror]").forEach(element => element.removeAttribute("onerror"));
|
|
this.doc.querySelectorAll("[onerror]").forEach(element => element.removeAttribute("onerror"));
|
|
|
if (this.options.removeAudioSrc) {
|
|
if (this.options.removeAudioSrc) {
|
|
|
- this.doc.querySelectorAll("audio[src], audio > source[src]").forEach(element => element.removeAttribute("src"));
|
|
|
|
|
|
|
+ const audioSourceElements = this.doc.querySelectorAll("audio[src], audio > source[src]");
|
|
|
|
|
+ if (this.options.displayStats) {
|
|
|
|
|
+ this.stats.discarded.audioSource = objectElements.length;
|
|
|
|
|
+ }
|
|
|
|
|
+ audioSourceElements.forEach(element => element.removeAttribute("src"));
|
|
|
}
|
|
}
|
|
|
if (this.options.removeVideoSrc) {
|
|
if (this.options.removeVideoSrc) {
|
|
|
- this.doc.querySelectorAll("video[src], video > source[src]").forEach(element => element.removeAttribute("src"));
|
|
|
|
|
|
|
+ const videoSourceElements = this.doc.querySelectorAll("video[src], video > source[src]");
|
|
|
|
|
+ if (this.options.displayStats) {
|
|
|
|
|
+ this.stats.discarded.videoSource = objectElements.length;
|
|
|
|
|
+ }
|
|
|
|
|
+ videoSourceElements.forEach(element => element.removeAttribute("src"));
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -305,15 +364,27 @@ this.SingleFileCore = this.SingleFileCore || (() => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
removeScripts() {
|
|
removeScripts() {
|
|
|
- this.doc.querySelectorAll("script:not([type=\"application/ld+json\"])").forEach(element => element.remove());
|
|
|
|
|
|
|
+ const scriptElements = this.doc.querySelectorAll("script:not([type=\"application/ld+json\"])");
|
|
|
|
|
+ if (this.options.displayStats) {
|
|
|
|
|
+ this.stats.discarded.scripts = scriptElements.length;
|
|
|
|
|
+ }
|
|
|
|
|
+ scriptElements.forEach(element => element.remove());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
removeFrames() {
|
|
removeFrames() {
|
|
|
|
|
+ const frameElements = this.doc.querySelectorAll("iframe, frame, object[type=\"text/html\"][data]");
|
|
|
|
|
+ if (this.options.displayStats) {
|
|
|
|
|
+ this.stats.discarded.frames = frameElements.length;
|
|
|
|
|
+ }
|
|
|
this.doc.querySelectorAll("iframe, frame, object[type=\"text/html\"][data]").forEach(element => element.remove());
|
|
this.doc.querySelectorAll("iframe, frame, object[type=\"text/html\"][data]").forEach(element => element.remove());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
removeImports() {
|
|
removeImports() {
|
|
|
- this.doc.querySelectorAll("link[rel=import]").forEach(element => element.remove());
|
|
|
|
|
|
|
+ const importElements = this.doc.querySelectorAll("link[rel=import]");
|
|
|
|
|
+ if (this.options.displayStats) {
|
|
|
|
|
+ this.stats.discarded.imports = importElements.length;
|
|
|
|
|
+ }
|
|
|
|
|
+ importElements.forEach(element => element.remove());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
resetCharsetMeta() {
|
|
resetCharsetMeta() {
|
|
@@ -344,18 +415,40 @@ this.SingleFileCore = this.SingleFileCore || (() => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
removeUnusedCSSRules() {
|
|
removeUnusedCSSRules() {
|
|
|
- this.dom.rulesMinifier(this.doc);
|
|
|
|
|
|
|
+ const stats = this.dom.rulesMinifier(this.doc);
|
|
|
|
|
+ if (this.options.displayStats) {
|
|
|
|
|
+ this.stats.processed.cssRules = stats.processed;
|
|
|
|
|
+ this.stats.discarded.cssRules = stats.discarded;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
removeHiddenElements() {
|
|
removeHiddenElements() {
|
|
|
- this.doc.querySelectorAll("[" + REMOVED_CONTENT_ATTRIBUTE_NAME + "]").forEach(element => element.remove());
|
|
|
|
|
|
|
+ const hiddenElements = this.doc.querySelectorAll("[" + REMOVED_CONTENT_ATTRIBUTE_NAME + "]");
|
|
|
|
|
+ if (this.options.displayStats) {
|
|
|
|
|
+ this.stats.discarded.hiddenElements = hiddenElements.length;
|
|
|
|
|
+ }
|
|
|
|
|
+ hiddenElements.forEach(element => element.remove());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
compressHTML(postProcess) {
|
|
compressHTML(postProcess) {
|
|
|
if (postProcess) {
|
|
if (postProcess) {
|
|
|
|
|
+ let size;
|
|
|
|
|
+ if (this.options.displayStats) {
|
|
|
|
|
+ size = new Blob([this.doc.documentElement.outerHTML]).size;
|
|
|
|
|
+ }
|
|
|
this.dom.htmlmini.postProcess(this.doc);
|
|
this.dom.htmlmini.postProcess(this.doc);
|
|
|
|
|
+ if (this.options.displayStats) {
|
|
|
|
|
+ this.stats.discarded.htmlBytes += size - (new Blob([this.doc.documentElement.outerHTML]).size);
|
|
|
|
|
+ }
|
|
|
} else {
|
|
} else {
|
|
|
|
|
+ let size;
|
|
|
|
|
+ if (this.options.displayStats) {
|
|
|
|
|
+ size = new Blob([this.doc.documentElement.outerHTML]).size;
|
|
|
|
|
+ }
|
|
|
this.dom.htmlmini.process(this.doc, { preservedSpaceAttributeName: PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME });
|
|
this.dom.htmlmini.process(this.doc, { preservedSpaceAttributeName: PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME });
|
|
|
|
|
+ if (this.options.displayStats) {
|
|
|
|
|
+ this.stats.discarded.htmlBytes += size - (new Blob([this.doc.documentElement.outerHTML]).size);
|
|
|
|
|
+ }
|
|
|
this.doc.querySelectorAll("[" + PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME + "]").forEach(element => element.removeAttribute(PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME));
|
|
this.doc.querySelectorAll("[" + PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME + "]").forEach(element => element.removeAttribute(PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME));
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -384,6 +477,9 @@ this.SingleFileCore = this.SingleFileCore || (() => {
|
|
|
imgElement.style.pixelHeight = canvasData.height;
|
|
imgElement.style.pixelHeight = canvasData.height;
|
|
|
}
|
|
}
|
|
|
canvasElement.parentElement.replaceChild(imgElement, canvasElement);
|
|
canvasElement.parentElement.replaceChild(imgElement, canvasElement);
|
|
|
|
|
+ if (this.options.displayStats) {
|
|
|
|
|
+ this.stats.processed.canvas++;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
@@ -414,6 +510,9 @@ this.SingleFileCore = this.SingleFileCore || (() => {
|
|
|
|
|
|
|
|
async inlineStylesheets(initialization) {
|
|
async inlineStylesheets(initialization) {
|
|
|
await Promise.all(Array.from(this.doc.querySelectorAll("style")).map(async styleElement => {
|
|
await Promise.all(Array.from(this.doc.querySelectorAll("style")).map(async styleElement => {
|
|
|
|
|
+ if (!initialization && this.options.displayStats) {
|
|
|
|
|
+ this.stats.processed.styleSheets++;
|
|
|
|
|
+ }
|
|
|
const stylesheetContent = initialization ? await DomProcessorHelper.resolveImportURLs(styleElement.textContent, this.baseURI, { maxResourceSize: this.options.maxResourceSize, maxResourceSizeEnabled: this.options.maxResourceSizeEnabled }) : await DomProcessorHelper.processStylesheet(styleElement.textContent, this.baseURI);
|
|
const stylesheetContent = initialization ? await DomProcessorHelper.resolveImportURLs(styleElement.textContent, this.baseURI, { maxResourceSize: this.options.maxResourceSize, maxResourceSizeEnabled: this.options.maxResourceSizeEnabled }) : await DomProcessorHelper.processStylesheet(styleElement.textContent, this.baseURI);
|
|
|
styleElement.textContent = !initialization && this.options.compressCSS ? this.dom.uglifycss(stylesheetContent) : stylesheetContent;
|
|
styleElement.textContent = !initialization && this.options.compressCSS ? this.dom.uglifycss(stylesheetContent) : stylesheetContent;
|
|
|
}));
|
|
}));
|
|
@@ -422,6 +521,9 @@ this.SingleFileCore = this.SingleFileCore || (() => {
|
|
|
async scripts() {
|
|
async scripts() {
|
|
|
await Promise.all(Array.from(this.doc.querySelectorAll("script[src]")).map(async scriptElement => {
|
|
await Promise.all(Array.from(this.doc.querySelectorAll("script[src]")).map(async scriptElement => {
|
|
|
if (scriptElement.src) {
|
|
if (scriptElement.src) {
|
|
|
|
|
+ if (this.options.displayStats) {
|
|
|
|
|
+ this.stats.processed.scripts++;
|
|
|
|
|
+ }
|
|
|
const scriptContent = await Download.getContent(scriptElement.src, { asDataURI: false, maxResourceSize: this.options.maxResourceSize, maxResourceSizeEnabled: this.options.maxResourceSizeEnabled });
|
|
const scriptContent = await Download.getContent(scriptElement.src, { asDataURI: false, maxResourceSize: this.options.maxResourceSize, maxResourceSizeEnabled: this.options.maxResourceSizeEnabled });
|
|
|
scriptElement.textContent = scriptContent.replace(/<\/script>/gi, "<\\/script>");
|
|
scriptElement.textContent = scriptContent.replace(/<\/script>/gi, "<\\/script>");
|
|
|
}
|
|
}
|
|
@@ -453,10 +555,19 @@ this.SingleFileCore = this.SingleFileCore || (() => {
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
if (frameData.processor) {
|
|
if (frameData.processor) {
|
|
|
|
|
+ if (this.options.displayStats) {
|
|
|
|
|
+ this.stats.processed.frames++;
|
|
|
|
|
+ }
|
|
|
await frameData.processor.preparePageData();
|
|
await frameData.processor.preparePageData();
|
|
|
const pageData = await frameData.processor.getPageData();
|
|
const pageData = await frameData.processor.getPageData();
|
|
|
frameElement.removeAttribute(WIN_ID_ATTRIBUTE_NAME);
|
|
frameElement.removeAttribute(WIN_ID_ATTRIBUTE_NAME);
|
|
|
DomProcessorHelper.setFrameContent(frameElement, pageData.content);
|
|
DomProcessorHelper.setFrameContent(frameElement, pageData.content);
|
|
|
|
|
+ if (this.options.displayStats) {
|
|
|
|
|
+ Object.keys(this.stats.discarded).forEach(key => this.stats.discarded[key] += (pageData.stats.discarded[key] || 0));
|
|
|
|
|
+ Object.keys(this.stats.processed).forEach(key => this.stats.processed[key] += (pageData.stats.processed[key] || 0));
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (this.options.displayStats) {
|
|
|
|
|
+ this.stats.discarded.frames++;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -485,13 +596,21 @@ this.SingleFileCore = this.SingleFileCore || (() => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
|
|
+ linkElement.setAttribute("href", EMPTY_DATA_URI);
|
|
|
const processor = this.relImportProcessors.get(linkElement);
|
|
const processor = this.relImportProcessors.get(linkElement);
|
|
|
if (processor) {
|
|
if (processor) {
|
|
|
|
|
+ if (this.options.displayStats) {
|
|
|
|
|
+ this.stats.processed.imports++;
|
|
|
|
|
+ }
|
|
|
this.relImportProcessors.delete(linkElement);
|
|
this.relImportProcessors.delete(linkElement);
|
|
|
const pageData = await processor.getPageData();
|
|
const pageData = await processor.getPageData();
|
|
|
linkElement.setAttribute("href", "data:text/html," + pageData.content);
|
|
linkElement.setAttribute("href", "data:text/html," + pageData.content);
|
|
|
- } else {
|
|
|
|
|
- linkElement.setAttribute("href", EMPTY_DATA_URI);
|
|
|
|
|
|
|
+ if (this.options.displayStats) {
|
|
|
|
|
+ Object.keys(this.stats.discarded).forEach(key => this.stats.discarded[key] += (pageData.stats.discarded[key] || 0));
|
|
|
|
|
+ Object.keys(this.stats.processed).forEach(key => this.stats.processed[key] += (pageData.stats.processed[key] || 0));
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (this.options.displayStats) {
|
|
|
|
|
+ this.stats.discarded.imports++;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}));
|
|
}));
|