Просмотр исходного кода

replaced "append the save date to the file name" with "template" option

Gildas 7 лет назад
Родитель
Сommit
10cdd8ab82

+ 3 - 3
_locales/en/messages.json

@@ -103,9 +103,9 @@
         "message": "File name",
         "description": "Options sub-title: 'File name'"
     },
-    "optionAppendSaveDate": {
-        "message": "append the save date to the file name",
-        "description": "Options page label: 'append the save date to the file name'"
+    "optionFilenameTemplate": {
+        "message": "template",
+        "description": "Options page label: 'template'"
     },
     "optionConfirmFilename": {
         "message": "open the \"Save as\" dialog to confirm the file name",

+ 2 - 2
_locales/fr/messages.json

@@ -104,8 +104,8 @@
         "description": "Options sub-title: 'File name'"
     },
     "optionAppendSaveDate": {
-        "message": "ajouter la date de sauvegarde à la fin du nom de fichier",
-        "description": "Options page label: 'append the save date to the file name'"
+        "message": "modèle",
+        "description": "Options page label: 'template'"
     },
     "optionConfirmFilename": {
         "message": "ouvrir la boite de dialogue \"Sauver sous\" pour confimer le nom de fichier",

+ 8 - 3
extension/core/bg/config.js

@@ -32,7 +32,7 @@ singlefile.config = (() => {
 		compressHTML: true,
 		compressCSS: true,
 		lazyLoadImages: true,
-		appendSaveDate: true,
+		filenameTemplate: "{title} ({iso-date} {locale-time}).html",
 		confirmFilename: false,
 		contextMenuEnabled: true,
 		shadowEnabled: true,
@@ -90,8 +90,13 @@ singlefile.config = (() => {
 		if (config.contextMenuEnabled === undefined) {
 			config.contextMenuEnabled = true;
 		}
-		if (config.appendSaveDate === undefined) {
-			config.appendSaveDate = true;
+		if (config.filenameTemplate === undefined) {
+			if (config.appendSaveDate || config.appendSaveDate === undefined) {
+				config.filenameTemplate = "{title} ({iso-date} {locale-time}).html";
+			} else {
+				config.filenameTemplate = "{title}.html";
+			}
+			delete config.appendSaveDate;
 		}
 		if (config.removeImports === undefined) {
 			config.removeImports = true;

+ 1 - 5
extension/core/bg/download.js

@@ -45,14 +45,10 @@ singlefile.download = (() => {
 	return { downloadPage };
 
 	async function downloadPage(page, options) {
-		let filename = page.filename.replace(/[~/\\?%*:|"<>\x00-\x1f\x7F]+/g, "_");
-		if (filename.length > 128) {
-			filename = filename.replace(/\.html?$/, "").substring(0, 122) + "….html";
-		}
 		const downloadInfo = {
 			url: page.url,
 			saveAs: options.confirmFilename,
-			filename
+			filename: page.filename
 		};
 		if (options.incognito) {
 			downloadInfo.incognito = true;

+ 2 - 3
extension/core/bg/processor.js

@@ -42,6 +42,7 @@ singlefile.processor = (() => {
 		options.backgroundTab = true;
 		options.autoSave = true;
 		options.incognito = incognito;
+		options.tabId = tabId;
 		let index = 0, maxIndex = 0;
 		options.onprogress = async event => {
 			if (event.type == event.RESOURCES_INITIALIZED) {
@@ -57,9 +58,7 @@ singlefile.processor = (() => {
 		const processor = new (SingleFile.getClass())(options);
 		await processor.initialize();
 		await processor.preparePageData();
-		const page = processor.getPageData();
-		const date = new Date();
-		page.filename = page.title + (options.appendSaveDate ? " (" + date.toISOString().split("T")[0] + " " + date.toLocaleTimeString() + ")" : "") + ".html";
+		const page = await processor.getPageData();
 		page.url = URL.createObjectURL(new Blob([page.content], { type: "text/html" }));
 		return singlefile.download.downloadPage(page, options);
 	}

+ 1 - 3
extension/core/content/content.js

@@ -93,12 +93,10 @@ this.singlefile.top = this.singlefile.top || (() => {
 		}
 		await processor.initialize();
 		await processor.preparePageData();
-		const page = processor.getPageData();
+		const page = await processor.getPageData();
 		if (options.selected) {
 			unmarkSelectedContent(processor.SELECTED_CONTENT_ATTRIBUTE_NAME, processor.SELECTED_CONTENT_ROOT_ATTRIBUTE_NAME);
 		}
-		const date = new Date();
-		page.filename = page.title + (options.appendSaveDate ? " (" + date.toISOString().split("T")[0] + " " + date.toLocaleTimeString() + ")" : "") + ".html";
 		page.url = URL.createObjectURL(new Blob([page.content], { type: "text/html" }));
 		if (options.shadowEnabled) {
 			singlefile.ui.end();

+ 1 - 0
extension/ui/bg/bg-ui.js

@@ -25,6 +25,7 @@ singlefile.ui = (() => {
 	return {
 		async saveTab(tab, options = {}) {
 			const tabId = tab.id;
+			options.tabId = tabId;
 			try {
 				singlefile.ui.button.onInitialize(tabId, options, 1);
 				if (options.autoSave) {

+ 6 - 6
extension/ui/bg/options.js

@@ -33,7 +33,7 @@
 	const compressCSSLabel = document.getElementById("compressCSSLabel");
 	const lazyLoadImagesLabel = document.getElementById("lazyLoadImagesLabel");
 	const addMenuEntryLabel = document.getElementById("addMenuEntryLabel");
-	const appendSaveDateLabel = document.getElementById("appendSaveDateLabel");
+	const filenameTemplateLabel = document.getElementById("filenameTemplateLabel");
 	const shadowEnabledLabel = document.getElementById("shadowEnabledLabel");
 	const setMaxResourceSizeLabel = document.getElementById("setMaxResourceSizeLabel");
 	const maxResourceSizeLabel = document.getElementById("maxResourceSizeLabel");
@@ -72,7 +72,7 @@
 	const compressCSSInput = document.getElementById("compressCSSInput");
 	const lazyLoadImagesInput = document.getElementById("lazyLoadImagesInput");
 	const contextMenuEnabledInput = document.getElementById("contextMenuEnabledInput");
-	const appendSaveDateInput = document.getElementById("appendSaveDateInput");
+	const filenameTemplateInput = document.getElementById("filenameTemplateInput");
 	const shadowEnabledInput = document.getElementById("shadowEnabledInput");
 	const maxResourceSizeInput = document.getElementById("maxResourceSizeInput");
 	const maxResourceSizeEnabledInput = document.getElementById("maxResourceSizeEnabledInput");
@@ -130,7 +130,7 @@
 	compressCSSLabel.textContent = browser.i18n.getMessage("optionCompressCSS");
 	lazyLoadImagesLabel.textContent = browser.i18n.getMessage("optionLazyLoadImages");
 	addMenuEntryLabel.textContent = browser.i18n.getMessage("optionAddMenuEntry");
-	appendSaveDateLabel.textContent = browser.i18n.getMessage("optionAppendSaveDate");
+	filenameTemplateLabel.textContent = browser.i18n.getMessage("optionFilenameTemplate");
 	shadowEnabledLabel.textContent = browser.i18n.getMessage("optionDisplayShadow");
 	setMaxResourceSizeLabel.textContent = browser.i18n.getMessage("optionSetMaxResourceSize");
 	maxResourceSizeLabel.textContent = browser.i18n.getMessage("optionMaxResourceSize");
@@ -157,7 +157,7 @@
 	autoSaveLabel.textContent = browser.i18n.getMessage("optionsAutoSaveSubTitle");
 	miscLabel.textContent = browser.i18n.getMessage("optionsMiscSubTitle");
 	helpLabel.textContent = browser.i18n.getMessage("optionsHelpLink");
-	resetButton.textContent = browser.i18n.getMessage("optionsResetButton");	
+	resetButton.textContent = browser.i18n.getMessage("optionsResetButton");
 	resetButton.title = browser.i18n.getMessage("optionsResetTooltip");
 
 	refresh();
@@ -174,7 +174,7 @@
 		compressCSSInput.checked = config.compressCSS;
 		lazyLoadImagesInput.checked = config.lazyLoadImages;
 		contextMenuEnabledInput.checked = config.contextMenuEnabled;
-		appendSaveDateInput.checked = config.appendSaveDate;
+		filenameTemplateInput.value = config.filenameTemplate;
 		shadowEnabledInput.checked = config.shadowEnabled;
 		maxResourceSizeEnabledInput.checked = config.maxResourceSizeEnabled;
 		maxResourceSizeInput.value = config.maxResourceSize;
@@ -210,7 +210,7 @@
 			compressCSS: compressCSSInput.checked,
 			lazyLoadImages: lazyLoadImagesInput.checked,
 			contextMenuEnabled: contextMenuEnabledInput.checked,
-			appendSaveDate: appendSaveDateInput.checked,
+			filenameTemplate: filenameTemplateInput.value,
 			shadowEnabled: shadowEnabledInput.checked,
 			maxResourceSizeEnabled: maxResourceSizeEnabledInput.checked,
 			maxResourceSize: maxResourceSizeInput.value,

+ 51 - 6
extension/ui/pages/help.html

@@ -118,13 +118,58 @@
 				<p>File name</p>
 				<ul>
 					<li>
-						<span class="option">append the save date to the file name</span>
-						<p>Check this option to append the save date of the webpage to the file name.
-						</p>
-						<p class="notice">It is recommended to
-							<u>check</u> this option</p>
+						<span class="option">template</span>
+						<p>The template allows you to customize the file name of saved pages. You can use any valid character and "/" to
+							create sub-folders. You can also use the variables in the list below anywhere in the template.</p>
+						<ul>
+							<li><code>{title}</code>: the title of the page or the last segment if not found</li>
+							<li><code>{iso-datetime}</code>: the date and time in the ISO format (e.g. "2018-09-15T22_38_26_317Z")</li>
+							<li><code>{iso-date}</code>: the date in the ISO format (e.g. "2018-09-15")</li>
+							<li><code>{iso-time}</code>: the time in the ISO format (e.g. "22_38_26_317")</li>
+							<li><code>{utc-datetime}</code>: the date and time in UTC format (e.g. "Sat, 15 Sep 2018 22_38_26 GMT")</li>
+							<li><code>{utc-day}</code>: the day in UTC format (e.g. "15")</li>
+							<li><code>{utc-month}</code>: the the month in UTC format (e.g. "9")</li>
+							<li><code>{utc-year}</code>: the year in UTC format (e.g. "2018")</li>
+							<li><code>{locale-date}</code>: the localized value of the date (e.g. "16_09_2018")</li>
+							<li><code>{locale-time}</code>: the localized value of the time (e.g. "00_38_26")</li>
+							<li><code>{locale-day}</code>: the localized value of the day (e.g. "15")</li>
+							<li><code>{locale-month}</code>: the localized value of the month (e.g. "9")</li>
+							<li><code>{locale-year}</code>: the localized value of the year (e.g. "2018")</li>
+							<li><code>{locale-datetime}</code>: the localized value of the date and time (e.g. "9_16_2018, 12_54_31 AM")</li>
+							<li><code>{url-href}</code>: the URL of the page (e.g. "http_example.com")</li>
+							<li><code>{url-pathname}</code>: the path name of the URL (e.g. "category_index.html")</li>
+							<li><code>{url-last-segment}</code>: the last part of the pathname without the extension or the host if not
+								found
+								(e.g. "index")</li>
+							<li><code>{url-protocol}</code>: the protocol of the URL (e.g. "https")</li>
+							<li><code>{url-host}</code>: the host name + the port of the URL (e.g. "example.com_8080")</li>
+							<li><code>{url-hostname}</code>: the host name of the URL (e.g. "example.com")</li>
+							<li><code>{url-port}</code>: the port of the URL (e.g. "8080")</li>
+							<li><code>{url-username}</code>: the user name of the URL (e.g. "john_doe")</li>
+							<li><code>{url-password}</code>: the password of the URL (e.g. "qwerty123")</li>
+							<li><code>{url-search}</code>: the search string of the URL (e.g. "filter-date=today")</li>
+							<li><code>{url-hash}</code>: the hash of the URL (e.g. "chapter-2")</li>
+							<li><code>{tab-id}</code>: the id of the tab (e.g. "326")</li>
+							<li><code>{digest-sha-256}</code>: the SHA-256 hash value of the entire page content (e.g.
+								e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855)</li>
+							<li><code>{digest-sha-384}</code>: the SHA-384 hash value of the entire page content</li>
+							<li><code>{digest-sha-512}</code>: the SHA-512 hash value of the entire page content</li>
+						</ul>
+						<p>Note that invalid characters are automatically transformed and collapsed to underscores. File names cannot
+							exceed 192 characters and are truncated if longer. The invalid characters are: [, ~, \, ?, %, *, :, |, ", &lt;,
+							&gt;, and control characters from 0 to 31 in the ASCII table.</p>
+						<p>Template Examples</p>
+						<ul>
+							<li><code>{title} ({iso-date} {locale-time}).html</code> will produce filenames like "Introduction to SingleFile
+								(2018-09-15
+								11_06_03 PM).html" for a page having "Introduction to SingleFile" as title.</li>
+							<li><code>{url-last-segment} - {iso-date}</code> will produce filenames like "welcome - " for a page hosted on
+								https://example.com/welcome.html.</li>
+							<li><code>archives/{locale-year}/{locale-month}/{locale-day}/{title}</code> will produce filenames like
+								"Introduction to SingleFile" stored into 3 sub-directories in the "archives" folder, one for each part of the
+								date.</li>
+						</ul>
 					</li>
-
 					<li>
 						<span class="option">open the "Save as" dialog to confirm the file name</span>
 						<p>Check this option to display the "Save as" dialog in order to confirm the file name before saving the page. If

+ 17 - 12
extension/ui/pages/options.css

@@ -1,11 +1,7 @@
 body {
-    box-sizing: border-box;
-    padding: 0px;
     background-color: transparent;
     font-family: sans-serif;
-    text-align: justify;
     max-width: 800px;
-    height: auto;
 }
 
 button {
@@ -24,18 +20,23 @@ button:active {
     border-color: rgb(237, 237, 237);
 }
 
-input[type="checkbox"] {
-    margin-left: 30px;
-    position: relative;
-    align-self: flex-start;
+input {
+    margin-left: 5px;
+}
+
+label,
+input {
+    align-self: center;
+}
+
+input[type="text"] {
+    min-width: 70%;
+    text-align: right;
 }
 
 input[type="number"] {
-    margin-left: 30px;
     max-width: 40px;
     text-align: right;
-    position: relative;
-    top: -2px;
 }
 
 h3 {
@@ -57,7 +58,6 @@ details:last-of-type {
 details>summary {
     margin-bottom: 10px;
     margin-top: 10px;
-    min-width: 340px;
     cursor: pointer;
 }
 
@@ -79,6 +79,7 @@ a {
     justify-content: space-between;
     margin-top: 5px;
     padding-left: 12px;
+    min-height: 24px;
 }
 
 .option:last-of-type {
@@ -120,4 +121,8 @@ a {
     body {
         font-size: 11px;
     }
+    body,
+    input {
+        font-size: 12px;
+    }
 }

+ 2 - 2
extension/ui/pages/options.html

@@ -27,8 +27,8 @@
 	<details>
 		<summary id="filenameLabel"></summary>
 		<div class="option">
-			<label for="appendSaveDateInput" id="appendSaveDateLabel"></label>
-			<input type="checkbox" id="appendSaveDateInput">
+			<label for="filenameTemplateInput" id="filenameTemplateLabel"></label>
+			<input type="text" id="filenameTemplateInput">
 		</div>
 		<div class="option">
 			<label for="confirmFilenameInput" id="confirmFilenameLabel"></label>

+ 20 - 1
lib/single-file/single-file-browser.js

@@ -18,7 +18,7 @@
  *   along with SingleFile.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-/* global SingleFileCore, DOMParser, TextDecoder, Blob, fetch, base64, superFetch, parseSrcset, uglifycss, htmlmini, cssMinifier, fontsMinifier, lazyLoader, serializer, docHelper, mediasMinifier */
+/* global SingleFileCore, DOMParser, TextDecoder, Blob, fetch, base64, superFetch, parseSrcset, uglifycss, htmlmini, cssMinifier, fontsMinifier, lazyLoader, serializer, docHelper, mediasMinifier, TextEncoder, crypto */
 
 this.SingleFile = this.SingleFile || (() => {
 
@@ -105,6 +105,20 @@ this.SingleFile = this.SingleFile || (() => {
 		}
 	}
 
+	// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
+	function hex(buffer) {
+		var hexCodes = [];
+		var view = new DataView(buffer);
+		for (var i = 0; i < view.byteLength; i += 4) {
+			var value = view.getUint32(i);
+			var stringValue = value.toString(16);
+			var padding = "00000000";
+			var paddedValue = (padding + stringValue).slice(-padding.length);
+			hexCodes.push(paddedValue);
+		}
+		return hexCodes.join("");
+	}
+
 	// ---
 	// DOM
 	// ---
@@ -138,6 +152,11 @@ this.SingleFile = this.SingleFile || (() => {
 			return DOMParser;
 		}
 
+		static async digest(algo, text) {
+			const hash = await crypto.subtle.digest(algo, new TextEncoder("utf-8").encode(text));
+			return (hex(hash));
+		}
+
 		static getContentSize(content) {
 			return new Blob([content]).size;
 		}

+ 73 - 6
lib/single-file/single-file-core.js

@@ -43,7 +43,7 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 			async preparePageData() {
 				await this.processor.preparePageData();
 			}
-			getPageData() {
+			async getPageData() {
 				return this.processor.getPageData();
 			}
 		};
@@ -171,7 +171,7 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 			await this.executeStage(3);
 		}
 
-		getPageData() {
+		async getPageData() {
 			if (!this.options.windowId) {
 				this.onprogress(new ProgressEvent(PAGE_ENDED, { pageURL: this.options.url }));
 			}
@@ -289,7 +289,7 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 			await this.loadPage();
 		}
 
-		getPageData() {
+		async getPageData() {
 			DOM.postProcessDoc(this.doc, this.options);
 			if (this.options.selected) {
 				const rootElement = this.doc.querySelector("[" + SELECTED_CONTENT_ROOT_ATTRIBUTE_NAME + "]");
@@ -300,9 +300,10 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 				}
 			}
 			const titleElement = this.doc.querySelector("title");
-			let title;
 			if (titleElement) {
-				title = titleElement.textContent.trim();
+				this.options.title = titleElement.textContent.trim();
+			} else {
+				this.options.title = "";
 			}
 			const matchTitle = this.baseURI.match(/([^/]*)\/?(\.html?.*)$/) || this.baseURI.match(/\/\/([^/]*)\/?$/);
 			const url = new URL(this.baseURI);
@@ -316,9 +317,11 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 				this.stats.set("processed", "HTML bytes", contentSize);
 				this.stats.add("discarded", "HTML bytes", size - contentSize);
 			}
+			const filename = await DomProcessorHelper.getFilename(this.options, content);
 			return {
 				stats: this.stats.data,
-				title: title || (this.baseURI && matchTitle ? matchTitle[1] : (url.hostname ? url.hostname : "Untitled page")),
+				title: this.options.title || (this.baseURI && matchTitle ? matchTitle[1] : (url.hostname ? url.hostname : "")),
+				filename,
 				content
 			};
 		}
@@ -780,6 +783,70 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 	const PREFIX_DATA_URI_IMAGE_SVG = "data:image/svg+xml";
 
 	class DomProcessorHelper {
+		static async getFilename(options, content) {
+			let filename = options.filenameTemplate;
+			const date = new Date();
+			const url = new URL(options.url);
+			filename = filename.replace(/{\s*title\s*}/g, options.title.replace(/\/+/g, "_"));
+			filename = filename.replace(/{\s*iso-datetime\s*}/g, date.toISOString().replace(/\/+/g, "_"));
+			filename = filename.replace(/{\s*iso-date\s*}/g, date.toISOString().split("T")[0].replace(/\/+/g, "_"));
+			filename = filename.replace(/{\s*iso-time\s*}/g, date.toISOString().split("T")[1].split("Z")[0].replace(/\/+/g, "_"));
+			filename = filename.replace(/{\s*locale-date\s*}/g, date.toLocaleDateString().replace(/\/+/g, "_"));
+			filename = filename.replace(/{\s*locale-time\s*}/g, date.toLocaleTimeString().replace(/\/+/g, "_"));
+			filename = filename.replace(/{\s*locale-day\s*}/g, String(date.getDate()).replace(/\/+/g, "_"));
+			filename = filename.replace(/{\s*locale-month\s*}/g, String(date.getMonth() + 1).replace(/\/+/g, "_"));
+			filename = filename.replace(/{\s*locale-year\s*}/g, String(date.getFullYear()).replace(/\/+/g, "_"));
+			filename = filename.replace(/{\s*locale-datetime\s*}/g, date.toLocaleString().replace(/\/+/g, "_"));
+			filename = filename.replace(/{\s*utc-datetime\s*}/g, date.toUTCString().replace(/\/+/g, "_"));
+			filename = filename.replace(/{\s*utc-day\s*}/g, String(date.getUTCDate()).replace(/\/+/g, "_"));
+			filename = filename.replace(/{\s*utc-month\s*}/g, String(date.getUTCMonth() + 1).replace(/\/+/g, "_"));
+			filename = filename.replace(/{\s*utc-year\s*}/g, String(date.getUTCFullYear()).replace(/\/+/g, "_"));
+			filename = filename.replace(/{\s*url-hash\s*}/g, url.hash.substring(1).replace(/\/+/g, "_"));
+			filename = filename.replace(/{\s*url-host\s*}/g, url.host.replace(/\/+/g, "_").replace(/\/$/, ""));
+			filename = filename.replace(/{\s*url-hostname\s*}/g, url.hostname.replace(/\/+/g, "_").replace(/\/$/, ""));
+			filename = filename.replace(/{\s*url-href\s*}/g, url.href.replace(/\/+/g, "_"));
+			filename = filename.replace(/{\s*url-password\s*}/g, url.password.replace(/\/+/g, "_"));
+			filename = filename.replace(/{\s*url-pathname\s*}/g, url.pathname.replace(/\/+/g, "_").replace(/\/$/, "").replace(/^\//, ""));
+			filename = filename.replace(/{\s*url-port\s*}/g, url.port.replace(/\/+/g, "_"));
+			filename = filename.replace(/{\s*url-protocol\s*}/g, url.protocol.replace(/\/+/g, "_").replace(/\/$/, ""));
+			filename = filename.replace(/{\s*url-search\s*}/g, url.search.substring(1).replace(/\/+/g, "_"));
+			filename = filename.replace(/{\s*url-username\s*}/g, url.username.replace(/\/+/g, "_"));
+			filename = filename.replace(/{\s*tab-id\s*}/g, String(options.tabId || "unknown"));
+			let lastSegmentMatch = url.pathname.match(/\/([^/]+)$/);
+			let lastSegment = lastSegmentMatch && lastSegmentMatch[0];
+			if (!lastSegment) {
+				lastSegmentMatch = url.href.match(/([^/]+)\/?$/);
+				lastSegment = lastSegmentMatch && lastSegmentMatch[0];
+			}
+			if (!lastSegment) {
+				lastSegmentMatch = lastSegment.match(/(.*)<\.[^.]+$/);
+				lastSegment = lastSegmentMatch && lastSegmentMatch[0];
+			}
+			if (!lastSegment) {
+				lastSegment = url.hostname.replace(/\/+/g, "_").replace(/\/$/, "");
+			}
+			filename = filename.replace(/{\s*url-last-segment\s*}/g, lastSegment.replace(/\/+/g, "").replace(/\/$/, "").replace(/^\//, ""));
+			if (filename.match(/{\s*digest-sha-256\s*}/g)) {
+				filename = filename.replace(/{\s*digest-sha-256\s*}/g, await DOM.digest("SHA-256", content));
+			}
+			if (filename.match(/{\s*digest-sha-384\s*}/g)) {
+				filename = filename.replace(/{\s*digest-sha-384\s*}/g, await DOM.digest("SHA-384", content));
+			}
+			if (filename.match(/{\s*digest-sha-512\s*}/g)) {
+				filename = filename.replace(/{\s*digest-sha-512\s*}/g, await DOM.digest("SHA-512", content));
+			}
+			filename = filename.replace(/[~\\?%*:|"<>\x00-\x1f\x7F]+/g, "_"); // eslint-disable-line no-control-regex
+			if (filename.length > 192) {
+				const extensionMatch = filename.match(/(\.[^.]{3,4})$/);
+				const extension = extensionMatch && extensionMatch[0] && extensionMatch[0].length > 1 ? extensionMatch[0] : "";
+				filename = filename.substring(0, 192 - extension.length) + "…" + extension;
+			}
+			if (!filename) {
+				filename = "Untitled page";
+			}
+			return filename;
+		}
+
 		static setFrameEmptySrc(frameElement) {
 			if (frameElement.tagName == "OBJECT") {
 				frameElement.setAttribute("data", "data:text/html,");