Przeglądaj źródła

merge SingleFileZ code

Gildas 2 lat temu
rodzic
commit
6a927a3716

+ 1 - 1
.eslintrc.js

@@ -11,7 +11,7 @@ module.exports = {
 	},
 	},
 	"extends": "eslint:recommended",
 	"extends": "eslint:recommended",
 	"parserOptions": {
 	"parserOptions": {
-		"ecmaVersion": 2017,
+		"ecmaVersion": 2018,
 		"sourceType": "module"
 		"sourceType": "module"
 	},
 	},
 	"ignorePatterns": [
 	"ignorePatterns": [

+ 37 - 1
_locales/de/messages.json

@@ -255,6 +255,42 @@
 		"message": "Verborgene Elemente entfernen",
 		"message": "Verborgene Elemente entfernen",
 		"description": "Options page label: 'remove hidden elements'"
 		"description": "Options page label: 'remove hidden elements'"
 	},
 	},
+	"optionsFileFormatSubTitle": {
+		"message": "Dateiformat",
+		"description": "Options page label: 'File format'"
+	},
+	"optionFileFormat": {
+		"message": "format",
+		"description": "Options page label: 'format'"
+	},
+	"optionFileFormatSelectHTML": {
+		"message": "HTML",
+		"description": "Options page label: 'HTML'"
+	},
+	"optionFileFormatSelectSelfExtractingUniversal": {
+		"message": "selbstextrahierendes ZIP (universal)",
+		"description": "Options page label: 'self-extracting ZIP (universal)'"
+	},
+	"optionFileFormatSelectSelfExtracting": {
+		"message": "selbstextrahierendes ZIP",
+		"description": "Options page label: 'self-extracting ZIP'"
+	},
+	"optionFileFormatSelectZIP": {
+		"message": "ZIP",
+		"description": "Options page label: 'ZIP'"
+	},
+	"optionPassword": {
+		"message": "Passwort",
+		"description": "Options page label: 'password'"
+	},
+	"optionCreateRootDirectory": {
+		"message": "Stammverzeichnis erstellen",
+		"description": "Options page label: 'create a root directory'"
+	},
+	"optionInsertTextBody": {
+		"message": "Text durchsuchbar machen",
+		"description": "Options page label: 'make text searchable'"
+	},
 	"optionsInfobarSubTitle": {
 	"optionsInfobarSubTitle": {
 		"message": "Infoknopf",
 		"message": "Infoknopf",
 		"description": "Options sub-title: 'Infobar'"
 		"description": "Options sub-title: 'Infobar'"
@@ -891,4 +927,4 @@
 		"message": "URL",
 		"message": "URL",
 		"description": "Title of the column in the table of the URLs"
 		"description": "Title of the column in the table of the URLs"
 	}
 	}
-}
+}

+ 38 - 2
_locales/en/messages.json

@@ -211,7 +211,7 @@
 		"message": "replacement character",
 		"message": "replacement character",
 		"description": "Options page label: 'replacement character'"
 		"description": "Options page label: 'replacement character'"
 	},
 	},
-	"optionReplaceEmojisInFilename" : {
+	"optionReplaceEmojisInFilename": {
 		"message": "replace emojis with text",
 		"message": "replace emojis with text",
 		"description": "Options page label: 'replace emojis with text'"
 		"description": "Options page label: 'replace emojis with text'"
 	},
 	},
@@ -255,6 +255,42 @@
 		"message": "remove hidden elements",
 		"message": "remove hidden elements",
 		"description": "Options page label: 'remove hidden elements'"
 		"description": "Options page label: 'remove hidden elements'"
 	},
 	},
+	"optionsFileFormatSubTitle": {
+		"message": "File format",
+		"description": "Options page label: 'File format'"
+	},
+	"optionFileFormat": {
+		"message": "format",
+		"description": "Options page label: 'format'"
+	},
+	"optionFileFormatSelectHTML": {
+		"message": "HTML",
+		"description": "Options page label: 'HTML'"
+	},
+	"optionFileFormatSelectSelfExtractingUniversal": {
+		"message": "self-extracting ZIP (universal)",
+		"description": "Options page label: 'self-extracting ZIP (universal)'"
+	},
+	"optionFileFormatSelectSelfExtracting": {
+		"message": "self-extracting ZIP",
+		"description": "Options page label: 'self-extracting ZIP'"
+	},
+	"optionFileFormatSelectZIP": {
+		"message": "ZIP",
+		"description": "Options page label: 'ZIP'"
+	},
+	"optionPassword": {
+		"message": "password",
+		"description": "Options page label: 'password'"
+	},
+	"optionCreateRootDirectory": {
+		"message": "create a root directory",
+		"description": "Options page label: 'create a root directory'"
+	},
+	"optionInsertTextBody": {
+		"message": "make text searchable",
+		"description": "Options page label: 'make text searchable'"
+	},
 	"optionsInfobarSubTitle": {
 	"optionsInfobarSubTitle": {
 		"message": "Infobar",
 		"message": "Infobar",
 		"description": "Options sub-title: 'Infobar'"
 		"description": "Options sub-title: 'Infobar'"
@@ -891,4 +927,4 @@
 		"message": "URL",
 		"message": "URL",
 		"description": "Title of the column in the table of the URLs"
 		"description": "Title of the column in the table of the URLs"
 	}
 	}
-}
+}

+ 38 - 2
_locales/es/messages.json

@@ -211,7 +211,7 @@
 		"message": "carácter de reemplazo",
 		"message": "carácter de reemplazo",
 		"description": "Options page label: 'replacement character'"
 		"description": "Options page label: 'replacement character'"
 	},
 	},
-	"optionReplaceEmojisInFilename" : {
+	"optionReplaceEmojisInFilename": {
 		"message": "reemplazar emojis con texto",
 		"message": "reemplazar emojis con texto",
 		"description": "Options page label: 'replace emojis with text'"
 		"description": "Options page label: 'replace emojis with text'"
 	},
 	},
@@ -255,6 +255,42 @@
 		"message": "eliminar elementos ocultos (hidden)",
 		"message": "eliminar elementos ocultos (hidden)",
 		"description": "Options page label: 'remove hidden elements'"
 		"description": "Options page label: 'remove hidden elements'"
 	},
 	},
+	"optionsFileFormatSubTitle": {
+		"message": "Formato de archivo",
+		"description": "Options page label: 'File format'"
+	},
+	"optionFileFormat": {
+		"message": "formato",
+		"description": "Options page label: 'format'"
+	},
+	"optionFileFormatSelectHTML": {
+		"message": "HTML",
+		"description": "Options page label: 'HTML'"
+	},
+	"optionFileFormatSelectSelfExtractingUniversal": {
+		"message": "ZIP autoextraíble (universal)",
+		"description": "Options page label: 'self-extracting ZIP (universal)'"
+	},
+	"optionFileFormatSelectSelfExtracting": {
+		"message": "ZIP autoextraíble",
+		"description": "Options page label: 'self-extracting ZIP'"
+	},
+	"optionFileFormatSelectZIP": {
+		"message": "ZIP",
+		"description": "Options page label: 'ZIP'"
+	},
+	"optionPassword": {
+		"message": "contraseña",
+		"description": "Options page label: 'password'"
+	},
+	"optionCreateRootDirectory": {
+		"message": "crear un directorio raíz",
+		"description": "Options page label: 'create a root directory'"
+	},
+	"optionInsertTextBody": {
+		"message": "hacer buscable el texto",
+		"description": "Options page label: 'make text searchable'"
+	},
 	"optionsInfobarSubTitle": {
 	"optionsInfobarSubTitle": {
 		"message": "Barra informativa",
 		"message": "Barra informativa",
 		"description": "Options sub-title: 'Infobar'"
 		"description": "Options sub-title: 'Infobar'"
@@ -891,4 +927,4 @@
 		"message": "URL",
 		"message": "URL",
 		"description": "Title of the column in the table of the URLs"
 		"description": "Title of the column in the table of the URLs"
 	}
 	}
-}
+}

+ 38 - 2
_locales/fr/messages.json

@@ -211,7 +211,7 @@
 		"message": "caractère de remplacement",
 		"message": "caractère de remplacement",
 		"description": "Options page label: 'replacement character'"
 		"description": "Options page label: 'replacement character'"
 	},
 	},
-	"optionReplaceEmojisInFilename" : {
+	"optionReplaceEmojisInFilename": {
 		"message": "remplacer les emojis par du texte",
 		"message": "remplacer les emojis par du texte",
 		"description": "Options page label: 'replace emojis with text'"
 		"description": "Options page label: 'replace emojis with text'"
 	},
 	},
@@ -255,6 +255,42 @@
 		"message": "supprimer les élements cachés",
 		"message": "supprimer les élements cachés",
 		"description": "Options page label: 'remove hidden elements'"
 		"description": "Options page label: 'remove hidden elements'"
 	},
 	},
+	"optionsFileFormatSubTitle": {
+		"message": "Format de fichier",
+		"description": "Options page label: 'File format'"
+	},
+	"optionFileFormat": {
+		"message": "format",
+		"description": "Options page label: 'format'"
+	},
+	"optionFileFormatSelectHTML": {
+		"message": "HTML",
+		"description": "Options page label: 'HTML'"
+	},
+	"optionFileFormatSelectSelfExtractingUniversal": {
+		"message": "auto-extractibles ZIP (universel)",
+		"description": "Options page label: 'self-extracting ZIP (universal)'"
+	},
+	"optionFileFormatSelectSelfExtracting": {
+		"message": "auto-extractibles ZIP",
+		"description": "Options page label: 'self-extracting ZIP'"
+	},
+	"optionFileFormatSelectZIP": {
+		"message": "ZIP",
+		"description": "Options page label: 'ZIP'"
+	},
+	"optionPassword": {
+		"message": "mot de passe",
+		"description": "Options page label: 'password'"
+	},
+	"optionCreateRootDirectory": {
+		"message": "créer un répertoire racine",
+		"description": "Options page label: 'create a root directory'"
+	},
+	"optionInsertTextBody": {
+		"message": "rendre le texte indexable",
+		"description": "Options page label: 'make text searchable'"
+	},
 	"optionsInfobarSubTitle": {
 	"optionsInfobarSubTitle": {
 		"message": "Barre d'information",
 		"message": "Barre d'information",
 		"description": "Options sub-title: 'Infobar'"
 		"description": "Options sub-title: 'Infobar'"
@@ -891,4 +927,4 @@
 		"message": "URL",
 		"message": "URL",
 		"description": "Title of the column in the table of the URLs"
 		"description": "Title of the column in the table of the URLs"
 	}
 	}
-}
+}

+ 38 - 2
_locales/it/messages.json

@@ -211,7 +211,7 @@
 		"message": "carattere di sostituzione",
 		"message": "carattere di sostituzione",
 		"description": "Options page label: 'replacement character'"
 		"description": "Options page label: 'replacement character'"
 	},
 	},
-	"optionReplaceEmojisInFilename" : {
+	"optionReplaceEmojisInFilename": {
 		"message": "sostituisci le emoji con il testo",
 		"message": "sostituisci le emoji con il testo",
 		"description": "Options page label: 'replace emojis with text'"
 		"description": "Options page label: 'replace emojis with text'"
 	},
 	},
@@ -255,6 +255,42 @@
 		"message": "rimuovi elementi nascosti",
 		"message": "rimuovi elementi nascosti",
 		"description": "Options page label: 'remove hidden elements'"
 		"description": "Options page label: 'remove hidden elements'"
 	},
 	},
+	"optionsFileFormatSubTitle": {
+		"message": "Formato del file",
+		"description": "Options page label: 'File format'"
+	},
+	"optionFileFormat": {
+		"message": "formato",
+		"description": "Options page label: 'format'"
+	},
+	"optionFileFormatSelectHTML": {
+		"message": "HTML",
+		"description": "Options page label: 'HTML'"
+	},
+	"optionFileFormatSelectSelfExtractingUniversal": {
+		"message": "ZIP autoestraente (universale)",
+		"description": "Options page label: 'self-extracting ZIP (universal)'"
+	},
+	"optionFileFormatSelectSelfExtracting": {
+		"message": "ZIP autoestraente",
+		"description": "Options page label: 'self-extracting ZIP'"
+	},
+	"optionFileFormatSelectZIP": {
+		"message": "ZIP",
+		"description": "Options page label: 'ZIP'"
+	},
+	"optionPassword": {
+		"message": "password",
+		"description": "Options page label: 'password'"
+	},
+	"optionCreateRootDirectory": {
+		"message": "crea una cartella principale",
+		"description": "Options page label: 'create a root directory'"
+	},
+	"optionInsertTextBody": {
+		"message": "rendi il testo ricercabile",
+		"description": "Options page label: 'make text searchable'"
+	},
 	"optionsInfobarSubTitle": {
 	"optionsInfobarSubTitle": {
 		"message": "Barra informativa",
 		"message": "Barra informativa",
 		"description": "Options sub-title: 'Infobar'"
 		"description": "Options sub-title: 'Infobar'"
@@ -891,4 +927,4 @@
 		"message": "URL",
 		"message": "URL",
 		"description": "Title of the column in the table of the URLs"
 		"description": "Title of the column in the table of the URLs"
 	}
 	}
-}
+}

+ 38 - 2
_locales/ja/messages.json

@@ -211,7 +211,7 @@
 		"message": "置換文字",
 		"message": "置換文字",
 		"description": "Options page label: 'replacement character'"
 		"description": "Options page label: 'replacement character'"
 	},
 	},
-	"optionReplaceEmojisInFilename" : {
+	"optionReplaceEmojisInFilename": {
 		"message": "絵文字をテキストに置き換える",
 		"message": "絵文字をテキストに置き換える",
 		"description": "Options page label: 'replace emojis with text'"
 		"description": "Options page label: 'replace emojis with text'"
 	},
 	},
@@ -255,6 +255,42 @@
 		"message": "隠された要素を削除する",
 		"message": "隠された要素を削除する",
 		"description": "Options page label: 'remove hidden elements'"
 		"description": "Options page label: 'remove hidden elements'"
 	},
 	},
+	"optionsFileFormatSubTitle": {
+		"message": "ファイル形式",
+		"description": "Options page label: 'File format'"
+	},
+	"optionFileFormat": {
+		"message": "形式",
+		"description": "Options page label: 'format'"
+	},
+	"optionFileFormatSelectHTML": {
+		"message": "HTML",
+		"description": "Options page label: 'HTML'"
+	},
+	"optionFileFormatSelectSelfExtractingUniversal": {
+		"message": "自己解凍型ZIP(ユニバーサル)",
+		"description": "Options page label: 'self-extracting ZIP (universal)'"
+	},
+	"optionFileFormatSelectSelfExtracting": {
+		"message": "自己解凍型ZIP",
+		"description": "Options page label: 'self-extracting ZIP'"
+	},
+	"optionFileFormatSelectZIP": {
+		"message": "ZIP",
+		"description": "Options page label: 'ZIP'"
+	},
+	"optionPassword": {
+		"message": "パスワード",
+		"description": "Options page label: 'password'"
+	},
+	"optionCreateRootDirectory": {
+		"message": "ルートディレクトリを作成する",
+		"description": "Options page label: 'create a root directory'"
+	},
+	"optionInsertTextBody": {
+		"message": "テキストを検索可能にする",
+		"description": "Options page label: 'make text searchable'"
+	},
 	"optionsInfobarSubTitle": {
 	"optionsInfobarSubTitle": {
 		"message": "インフォバー",
 		"message": "インフォバー",
 		"description": "Options sub-title: 'Infobar'"
 		"description": "Options sub-title: 'Infobar'"
@@ -891,4 +927,4 @@
 		"message": "URL",
 		"message": "URL",
 		"description": "Title of the column in the table of the URLs"
 		"description": "Title of the column in the table of the URLs"
 	}
 	}
-}
+}

+ 37 - 1
_locales/pl/messages.json

@@ -211,7 +211,7 @@
 		"message": "znak zastępczy",
 		"message": "znak zastępczy",
 		"description": "Options page label: 'replacement character'"
 		"description": "Options page label: 'replacement character'"
 	},
 	},
-	"optionReplaceEmojisInFilename" : {
+	"optionReplaceEmojisInFilename": {
 		"message": "zamień emotikony na tekst",
 		"message": "zamień emotikony na tekst",
 		"description": "Options page label: 'replace emojis with text'"
 		"description": "Options page label: 'replace emojis with text'"
 	},
 	},
@@ -255,6 +255,42 @@
 		"message": "usuwaj ukryte elementy",
 		"message": "usuwaj ukryte elementy",
 		"description": "Options page label: 'remove hidden elements'"
 		"description": "Options page label: 'remove hidden elements'"
 	},
 	},
+	"optionsFileFormatSubTitle": {
+		"message": "File format",
+		"description": "Options page label: 'File format'"
+	},
+	"optionFileFormat": {
+		"message": "format",
+		"description": "Options page label: 'format'"
+	},
+	"optionFileFormatSelectHTML": {
+		"message": "HTML",
+		"description": "Options page label: 'HTML'"
+	},
+	"optionFileFormatSelectSelfExtractingUniversal": {
+		"message": "self-extracting ZIP (universal)",
+		"description": "Options page label: 'self-extracting ZIP (universal)'"
+	},
+	"optionFileFormatSelectSelfExtracting": {
+		"message": "self-extracting ZIP",
+		"description": "Options page label: 'self-extracting ZIP'"
+	},
+	"optionFileFormatSelectZIP": {
+		"message": "ZIP",
+		"description": "Options page label: 'ZIP'"
+	},
+	"optionPassword": {
+		"message": "password",
+		"description": "Options page label: 'password'"
+	},
+	"optionCreateRootDirectory": {
+		"message": "create a root directory",
+		"description": "Options page label: 'create a root directory'"
+	},
+	"optionInsertTextBody": {
+		"message": "make text searchable",
+		"description": "Options page label: 'make text searchable'"
+	},
 	"optionsInfobarSubTitle": {
 	"optionsInfobarSubTitle": {
 		"message": "Pasek informacyjny",
 		"message": "Pasek informacyjny",
 		"description": "Options sub-title: 'Infobar'"
 		"description": "Options sub-title: 'Infobar'"

+ 38 - 2
_locales/pt_br/messages.json

@@ -211,7 +211,7 @@
 		"message": "caractere de substituição",
 		"message": "caractere de substituição",
 		"description": "Options page label: 'replacement character'"
 		"description": "Options page label: 'replacement character'"
 	},
 	},
-	"optionReplaceEmojisInFilename" : {
+	"optionReplaceEmojisInFilename": {
 		"message": "substituir emojis por texto",
 		"message": "substituir emojis por texto",
 		"description": "Options page label: 'replace emojis with text'"
 		"description": "Options page label: 'replace emojis with text'"
 	},
 	},
@@ -255,6 +255,42 @@
 		"message": "remover elementos escondidos",
 		"message": "remover elementos escondidos",
 		"description": "Options page label: 'remove hidden elements'"
 		"description": "Options page label: 'remove hidden elements'"
 	},
 	},
+	"optionsFileFormatSubTitle": {
+		"message": "Formato do arquivo",
+		"description": "Options page label: 'File format'"
+	},
+	"optionFileFormat": {
+		"message": "formato",
+		"description": "Options page label: 'format'"
+	},
+	"optionFileFormatSelectHTML": {
+		"message": "HTML",
+		"description": "Options page label: 'HTML'"
+	},
+	"optionFileFormatSelectSelfExtractingUniversal": {
+		"message": "ZIP autoextraível (universal)",
+		"description": "Options page label: 'self-extracting ZIP (universal)'"
+	},
+	"optionFileFormatSelectSelfExtracting": {
+		"message": "ZIP autoextraível",
+		"description": "Options page label: 'self-extracting ZIP'"
+	},
+	"optionFileFormatSelectZIP": {
+		"message": "ZIP",
+		"description": "Options page label: 'ZIP'"
+	},
+	"optionPassword": {
+		"message": "senha",
+		"description": "Options page label: 'password'"
+	},
+	"optionCreateRootDirectory": {
+		"message": "criar um diretório raiz",
+		"description": "Options page label: 'create a root directory'"
+	},
+	"optionInsertTextBody": {
+		"message": "tornar texto pesquisável",
+		"description": "Options page label: 'make text searchable'"
+	},
 	"optionsInfobarSubTitle": {
 	"optionsInfobarSubTitle": {
 		"message": "Infobar",
 		"message": "Infobar",
 		"description": "Options sub-title: 'Infobar'"
 		"description": "Options sub-title: 'Infobar'"
@@ -891,4 +927,4 @@
 		"message": "URL",
 		"message": "URL",
 		"description": "Title of the column in the table of the URLs"
 		"description": "Title of the column in the table of the URLs"
 	}
 	}
-}
+}

+ 38 - 2
_locales/ru/messages.json

@@ -211,7 +211,7 @@
 		"message": "заменяющий символ",
 		"message": "заменяющий символ",
 		"description": "Options page label: 'replacement character'"
 		"description": "Options page label: 'replacement character'"
 	},
 	},
-	"optionReplaceEmojisInFilename" : {
+	"optionReplaceEmojisInFilename": {
 		"message": "заменять эмодзи текстом",
 		"message": "заменять эмодзи текстом",
 		"description": "Options page label: 'replace emojis with text'"
 		"description": "Options page label: 'replace emojis with text'"
 	},
 	},
@@ -255,6 +255,42 @@
 		"message": "удалить скрытые элементы",
 		"message": "удалить скрытые элементы",
 		"description": "Options page label: 'remove hidden elements'"
 		"description": "Options page label: 'remove hidden elements'"
 	},
 	},
+	"optionsFileFormatSubTitle": {
+		"message": "File format",
+		"description": "Options page label: 'File format'"
+	},
+	"optionFileFormat": {
+		"message": "format",
+		"description": "Options page label: 'format'"
+	},
+	"optionFileFormatSelectHTML": {
+		"message": "HTML",
+		"description": "Options page label: 'HTML'"
+	},
+	"optionFileFormatSelectSelfExtractingUniversal": {
+		"message": "self-extracting ZIP (universal)",
+		"description": "Options page label: 'self-extracting ZIP (universal)'"
+	},
+	"optionFileFormatSelectSelfExtracting": {
+		"message": "self-extracting ZIP",
+		"description": "Options page label: 'self-extracting ZIP'"
+	},
+	"optionFileFormatSelectZIP": {
+		"message": "ZIP",
+		"description": "Options page label: 'ZIP'"
+	},
+	"optionPassword": {
+		"message": "password",
+		"description": "Options page label: 'password'"
+	},
+	"optionCreateRootDirectory": {
+		"message": "create a root directory",
+		"description": "Options page label: 'create a root directory'"
+	},
+	"optionInsertTextBody": {
+		"message": "make text searchable",
+		"description": "Options page label: 'make text searchable'"
+	},
 	"optionsInfobarSubTitle": {
 	"optionsInfobarSubTitle": {
 		"message": "Информационная панель",
 		"message": "Информационная панель",
 		"description": "Options sub-title: 'Infobar'"
 		"description": "Options sub-title: 'Infobar'"
@@ -891,4 +927,4 @@
 		"message": "URL",
 		"message": "URL",
 		"description": "Title of the column in the table of the URLs"
 		"description": "Title of the column in the table of the URLs"
 	}
 	}
-}
+}

+ 38 - 2
_locales/tr/messages.json

@@ -211,7 +211,7 @@
 		"message": "yerine geçen karakter",
 		"message": "yerine geçen karakter",
 		"description": "Options page label: 'replacement character'"
 		"description": "Options page label: 'replacement character'"
 	},
 	},
-	"optionReplaceEmojisInFilename" : {
+	"optionReplaceEmojisInFilename": {
 		"message": "emoji'leri metinle değiştir",
 		"message": "emoji'leri metinle değiştir",
 		"description": "Options page label: 'replace emojis with text'"
 		"description": "Options page label: 'replace emojis with text'"
 	},
 	},
@@ -255,6 +255,42 @@
 		"message": "gizli öğeleri kaldır",
 		"message": "gizli öğeleri kaldır",
 		"description": "Options page label: 'remove hidden elements'"
 		"description": "Options page label: 'remove hidden elements'"
 	},
 	},
+	"optionsFileFormatSubTitle": {
+		"message": "Dosya biçimi",
+		"description": "Options page label: 'File format'"
+	},
+	"optionFileFormat": {
+		"message": "biçim",
+		"description": "Options page label: 'format'"
+	},
+	"optionFileFormatSelectHTML": {
+		"message": "HTML",
+		"description": "Options page label: 'HTML'"
+	},
+	"optionFileFormatSelectSelfExtractingUniversal": {
+		"message": "kendinden çıkartılabilen ZIP (evrensel)",
+		"description": "Options page label: 'self-extracting ZIP (universal)'"
+	},
+	"optionFileFormatSelectSelfExtracting": {
+		"message": "kendinden çıkartılabilen ZIP",
+		"description": "Options page label: 'self-extracting ZIP'"
+	},
+	"optionFileFormatSelectZIP": {
+		"message": "ZIP",
+		"description": "Options page label: 'ZIP'"
+	},
+	"optionPassword": {
+		"message": "şifre",
+		"description": "Options page label: 'password'"
+	},
+	"optionCreateRootDirectory": {
+		"message": "kök dizin oluştur",
+		"description": "Options page label: 'create a root directory'"
+	},
+	"optionInsertTextBody": {
+		"message": "metni aranabilir yap",
+		"description": "Options page label: 'make text searchable'"
+	},
 	"optionsInfobarSubTitle": {
 	"optionsInfobarSubTitle": {
 		"message": "Bilgi çubuğu",
 		"message": "Bilgi çubuğu",
 		"description": "Options sub-title: 'Infobar'"
 		"description": "Options sub-title: 'Infobar'"
@@ -891,4 +927,4 @@
 		"message": "URL",
 		"message": "URL",
 		"description": "Title of the column in the table of the URLs"
 		"description": "Title of the column in the table of the URLs"
 	}
 	}
-}
+}

+ 38 - 2
_locales/uk/messages.json

@@ -211,7 +211,7 @@
 		"message": "замінний символ",
 		"message": "замінний символ",
 		"description": "Options page label: 'replacement character'"
 		"description": "Options page label: 'replacement character'"
 	},
 	},
-	"optionReplaceEmojisInFilename" : {
+	"optionReplaceEmojisInFilename": {
 		"message": "Замініть смайлики на текст",
 		"message": "Замініть смайлики на текст",
 		"description": "Options page label: 'replace emojis with text'"
 		"description": "Options page label: 'replace emojis with text'"
 	},
 	},
@@ -255,6 +255,42 @@
 		"message": "видалити приховані елементи",
 		"message": "видалити приховані елементи",
 		"description": "Options page label: 'remove hidden elements'"
 		"description": "Options page label: 'remove hidden elements'"
 	},
 	},
+	"optionsFileFormatSubTitle": {
+		"message": "Формат файлу",
+		"description": "Options page label: 'File format'"
+	},
+	"optionFileFormat": {
+		"message": "формату",
+		"description": "Options page label: 'format'"
+	},
+	"optionFileFormatSelectHTML": {
+		"message": "HTML",
+		"description": "Options page label: 'HTML'"
+	},
+	"optionFileFormatSelectSelfExtractingUniversal": {
+		"message": "саморозпаковується ZIP (універсальний)",
+		"description": "Options page label: 'self-extracting ZIP (universal)'"
+	},
+	"optionFileFormatSelectSelfExtracting": {
+		"message": "саморозпаковується ZIP",
+		"description": "Options page label: 'self-extracting ZIP'"
+	},
+	"optionFileFormatSelectZIP": {
+		"message": "ZIP",
+		"description": "Options page label: 'ZIP'"
+	},
+	"optionPassword": {
+		"message": "пароль",
+		"description": "Options page label: 'password'"
+	},
+	"optionCreateRootDirectory": {
+		"message": "створити кореневий каталог",
+		"description": "Options page label: 'create a root directory'"
+	},
+	"optionInsertTextBody": {
+		"message": "зробити текст доступним для пошуку",
+		"description": "Options page label: 'make text searchable'"
+	},
 	"optionsInfobarSubTitle": {
 	"optionsInfobarSubTitle": {
 		"message": "Інфобар",
 		"message": "Інфобар",
 		"description": "Options sub-title: 'Infobar'"
 		"description": "Options sub-title: 'Infobar'"
@@ -891,4 +927,4 @@
 		"message": "URL",
 		"message": "URL",
 		"description": "Title of the column in the table of the URLs"
 		"description": "Title of the column in the table of the URLs"
 	}
 	}
-}
+}

+ 38 - 2
_locales/zh_CN/messages.json

@@ -211,7 +211,7 @@
 		"message": "replacement character",
 		"message": "replacement character",
 		"description": "Options page label: 'replacement character'"
 		"description": "Options page label: 'replacement character'"
 	},
 	},
-	"optionReplaceEmojisInFilename" : {
+	"optionReplaceEmojisInFilename": {
 		"message": "用文字替换表情符号",
 		"message": "用文字替换表情符号",
 		"description": "Options page label: 'replace emojis with text'"
 		"description": "Options page label: 'replace emojis with text'"
 	},
 	},
@@ -255,6 +255,42 @@
 		"message": "移除隐藏元素",
 		"message": "移除隐藏元素",
 		"description": "Options page label: 'remove hidden elements'"
 		"description": "Options page label: 'remove hidden elements'"
 	},
 	},
+	"optionsFileFormatSubTitle": {
+		"message": "文件格式",
+		"description": "Options page label: 'File format'"
+	},
+	"optionFileFormat": {
+		"message": "格式",
+		"description": "Options page label: 'format'"
+	},
+	"optionFileFormatSelectHTML": {
+		"message": "HTML",
+		"description": "Options page label: 'HTML'"
+	},
+	"optionFileFormatSelectSelfExtractingUniversal": {
+		"message": "自解压缩 ZIP(通用)",
+		"description": "Options page label: 'self-extracting ZIP (universal)'"
+	},
+	"optionFileFormatSelectSelfExtracting": {
+		"message": "自解压缩 ZIP",
+		"description": "Options page label: 'self-extracting ZIP'"
+	},
+	"optionFileFormatSelectZIP": {
+		"message": "ZIP",
+		"description": "Options page label: 'ZIP'"
+	},
+	"optionPassword": {
+		"message": "密码",
+		"description": "Options page label: 'password'"
+	},
+	"optionCreateRootDirectory": {
+		"message": "创建根目录",
+		"description": "Options page label: 'create a root directory'"
+	},
+	"optionInsertTextBody": {
+		"message": "使文本可被搜索",
+		"description": "Options page label: 'make text searchable'"
+	},
 	"optionsInfobarSubTitle": {
 	"optionsInfobarSubTitle": {
 		"message": "信息栏",
 		"message": "信息栏",
 		"description": "Options sub-title: 'Infobar'"
 		"description": "Options sub-title: 'Infobar'"
@@ -891,4 +927,4 @@
 		"message": "网址",
 		"message": "网址",
 		"description": "Title of the column in the table of the URLs"
 		"description": "Title of the column in the table of the URLs"
 	}
 	}
-}
+}

+ 38 - 2
_locales/zh_TW/messages.json

@@ -211,7 +211,7 @@
 		"message": "replacement character",
 		"message": "replacement character",
 		"description": "Options page label: 'replacement character'"
 		"description": "Options page label: 'replacement character'"
 	},
 	},
-	"optionReplaceEmojisInFilename" : {
+	"optionReplaceEmojisInFilename": {
 		"message": "用文字替換表情符號",
 		"message": "用文字替換表情符號",
 		"description": "Options page label: 'replace emojis with text'"
 		"description": "Options page label: 'replace emojis with text'"
 	},
 	},
@@ -255,6 +255,42 @@
 		"message": "移除隱藏元素",
 		"message": "移除隱藏元素",
 		"description": "Options page label: 'remove hidden elements'"
 		"description": "Options page label: 'remove hidden elements'"
 	},
 	},
+	"optionsFileFormatSubTitle": {
+		"message": "檔案格式",
+		"description": "Options page label: 'File format'"
+	},
+	"optionFileFormat": {
+		"message": "格式",
+		"description": "Options page label: 'format'"
+	},
+	"optionFileFormatSelectHTML": {
+		"message": "HTML",
+		"description": "Options page label: 'HTML'"
+	},
+	"optionFileFormatSelectSelfExtractingUniversal": {
+		"message": "自解壓縮 ZIP(通用)",
+		"description": "Options page label: 'self-extracting ZIP (universal)'"
+	},
+	"optionFileFormatSelectSelfExtracting": {
+		"message": "自解壓縮 ZIP",
+		"description": "Options page label: 'self-extracting ZIP'"
+	},
+	"optionFileFormatSelectZIP": {
+		"message": "ZIP",
+		"description": "Options page label: 'ZIP'"
+	},
+	"optionPassword": {
+		"message": "密碼",
+		"description": "Options page label: 'password'"
+	},
+	"optionCreateRootDirectory": {
+		"message": "創建根目錄",
+		"description": "Options page label: 'create a root directory'"
+	},
+	"optionInsertTextBody": {
+		"message": "使文本可被搜索",
+		"description": "Options page label: 'make text searchable'"
+	},
 	"optionsInfobarSubTitle": {
 	"optionsInfobarSubTitle": {
 		"message": "信息欄",
 		"message": "信息欄",
 		"description": "Options sub-title: 'Infobar'"
 		"description": "Options sub-title: 'Infobar'"
@@ -891,4 +927,4 @@
 		"message": "網址",
 		"message": "網址",
 		"description": "Title of the column in the table of the URLs"
 		"description": "Title of the column in the table of the URLs"
 	}
 	}
-}
+}

+ 2 - 0
manifest.json

@@ -83,6 +83,8 @@
 		"lib/single-file-extension-editor-init.js",
 		"lib/single-file-extension-editor-init.js",
 		"lib/single-file-extension-editor.js",
 		"lib/single-file-extension-editor.js",
 		"lib/single-file-extension-editor-helper.js",
 		"lib/single-file-extension-editor-helper.js",
+		"lib/single-file-zip.min.js",
+		"lib/single-file-z-worker.js",
 		"src/lib/readability/Readability.js",
 		"src/lib/readability/Readability.js",
 		"src/lib/readability/Readability-readerable.js",
 		"src/lib/readability/Readability-readerable.js",
 		"src/ui/pages/editor-note-web.css",
 		"src/ui/pages/editor-note-web.css",

+ 2 - 2
package.json

@@ -12,8 +12,8 @@
 		"single-file": "./cli/single-file"
 		"single-file": "./cli/single-file"
 	},
 	},
 	"dependencies": {
 	"dependencies": {
-		"single-file-core": "1.1.79",
-		"single-file-cli": "1.0.69"
+		"single-file-core": "1.2.0",
+		"single-file-cli": "1.1.0"
 	},
 	},
 	"devDependencies": {
 	"devDependencies": {
 		"@rollup/plugin-node-resolve": "15.0.1",
 		"@rollup/plugin-node-resolve": "15.0.1",

+ 35 - 2
rollup.config.dev.js

@@ -54,6 +54,35 @@ export default [{
 	}],
 	}],
 	plugins: PLUGINS,
 	plugins: PLUGINS,
 	external: EXTERNAL
 	external: EXTERNAL
+}, {
+	input: ["single-file-core/vendor/zip/z-worker.js"],
+	output: [{
+		file: "lib/single-file-z-worker.js",
+		format: "es",
+		plugins: []
+	}],
+	plugins: PLUGINS,
+	external: EXTERNAL
+}, {
+	input: ["single-file-core/vendor/zip/zip.js"],
+	output: [{
+		file: "lib/single-file-zip.js",
+		format: "es",
+		plugins: []
+	}],
+	context: "this",
+	plugins: PLUGINS,
+	external: EXTERNAL
+}, {
+	input: ["single-file-core/vendor/zip/zip.min.js"],
+	output: [{
+		file: "lib/single-file-zip.min.js",
+		format: "es",
+		plugins: []
+	}],
+	context: "this",
+	plugins: PLUGINS,
+	external: EXTERNAL
 }, {
 }, {
 	input: ["src/core/content/content-bootstrap.js"],
 	input: ["src/core/content/content-bootstrap.js"],
 	output: [{
 	output: [{
@@ -89,14 +118,18 @@ export default [{
 		file: "lib/single-file-extension-editor-init.js",
 		file: "lib/single-file-extension-editor-init.js",
 		format: "iife",
 		format: "iife",
 		plugins: []
 		plugins: []
-	}]
+	}],
+	plugins: PLUGINS,
+	external: EXTERNAL
 }, {
 }, {
 	input: ["src/ui/content/content-ui-editor-web.js"],
 	input: ["src/ui/content/content-ui-editor-web.js"],
 	output: [{
 	output: [{
 		file: "lib/single-file-extension-editor.js",
 		file: "lib/single-file-extension-editor.js",
 		format: "iife",
 		format: "iife",
 		plugins: []
 		plugins: []
-	}]
+	}],
+	plugins: PLUGINS,
+	external: EXTERNAL
 }, {
 }, {
 	input: ["single-file-core/single-file-editor-helper.js"],
 	input: ["single-file-core/single-file-editor-helper.js"],
 	output: [{
 	output: [{

+ 35 - 2
rollup.config.js

@@ -54,6 +54,35 @@ export default [{
 	}],
 	}],
 	plugins: PLUGINS,
 	plugins: PLUGINS,
 	external: EXTERNAL
 	external: EXTERNAL
+}, {
+	input: ["single-file-core/vendor/zip/z-worker.js"],
+	output: [{
+		file: "lib/single-file-z-worker.js",
+		format: "es",
+		plugins: [terser()]
+	}],
+	plugins: PLUGINS,
+	external: EXTERNAL
+}, {
+	input: ["single-file-core/vendor/zip/zip.js"],
+	output: [{
+		file: "lib/single-file-zip.js",
+		format: "es",
+		plugins: [terser()]
+	}],
+	context: "this",
+	plugins: PLUGINS,
+	external: EXTERNAL
+}, {
+	input: ["single-file-core/vendor/zip/zip.min.js"],
+	output: [{
+		file: "lib/single-file-zip.min.js",
+		format: "es",
+		plugins: [terser()]
+	}],
+	context: "this",
+	plugins: PLUGINS,
+	external: EXTERNAL
 }, {
 }, {
 	input: ["src/core/content/content-bootstrap.js"],
 	input: ["src/core/content/content-bootstrap.js"],
 	output: [{
 	output: [{
@@ -89,14 +118,18 @@ export default [{
 		file: "lib/single-file-extension-editor-init.js",
 		file: "lib/single-file-extension-editor-init.js",
 		format: "iife",
 		format: "iife",
 		plugins: [terser()]
 		plugins: [terser()]
-	}]
+	}],
+	plugins: PLUGINS,
+	external: EXTERNAL
 }, {
 }, {
 	input: ["src/ui/content/content-ui-editor-web.js"],
 	input: ["src/ui/content/content-ui-editor-web.js"],
 	output: [{
 	output: [{
 		file: "lib/single-file-extension-editor.js",
 		file: "lib/single-file-extension-editor.js",
 		format: "iife",
 		format: "iife",
 		plugins: []
 		plugins: []
-	}]
+	}],
+	plugins: PLUGINS,
+	external: EXTERNAL
 }, {
 }, {
 	input: ["single-file-core/single-file-editor-helper.js"],
 	input: ["single-file-core/single-file-editor-helper.js"],
 	output: [{
 	output: [{

+ 54 - 37
src/core/bg/autosave.js

@@ -160,46 +160,63 @@ async function saveContent(message, tab) {
 				if (options.passReferrerOnError) {
 				if (options.passReferrerOnError) {
 					enableReferrerOnError();
 					enableReferrerOnError();
 				}
 				}
+				options.tabId = tabId;
 				pageData = await getPageData(options, null, null, { fetch });
 				pageData = await getPageData(options, null, null, { fetch });
-				if (options.saveToGDrive) {
-					const blob = new Blob([pageData.content], { type: "text/html" });
-					await downloads.saveToGDrive(message.taskId, downloads.encodeSharpCharacter(pageData.filename), blob, options, {
-						forceWebAuthFlow: options.forceWebAuthFlow
-					}, {
-						filenameConflictAction: options.filenameConflictAction
-					});
-				} else if (options.saveWithWebDAV) {
-					await downloads.saveWithWebDAV(message.taskId, downloads.encodeSharpCharacter(pageData.filename), pageData.content, options.webDAVURL, options.webDAVUser, options.webDAVPassword, {
-						filenameConflictAction: options.filenameConflictAction
-					});
-				} else if (options.saveToGitHub) {
-					await (await downloads.saveToGitHub(message.taskId, downloads.encodeSharpCharacter(pageData.filename), pageData.content, options.githubToken, options.githubUser, options.githubRepository, options.githubBranch, {
-						filenameConflictAction: options.filenameConflictAction
-					})).pushPromise;
-				} else if (options.saveWithCompanion) {
-					await companion.save({
-						filename: pageData.filename,
-						content: pageData.content,
-						filenameConflictAction: options.filenameConflictAction
-					});
-				} else {
-					const blob = new Blob([pageData.content], { type: "text/html" });
-					pageData.url = URL.createObjectURL(blob);
-					await downloads.downloadPage(pageData, options);
-					if (options.openSavedPage) {
-						const createTabProperties = { active: true, url: URL.createObjectURL(blob), windowId: tab.windowId };
-						const index = tab.index;
-						try {
-							await browser.tabs.get(tabId);
-							createTabProperties.index = index + 1;
-						} catch (error) {
-							createTabProperties.index = index;
+				let skipped;
+				if (!options.saveToGDrive && !options.saveWithWebDAV && !options.saveToGitHub && !options.saveWithCompanion) {
+					const testSkip = await downloads.testSkipSave(pageData.filename, options);
+					skipped = testSkip.skipped;
+					options.filenameConflictAction = testSkip.filenameConflictAction;
+				}
+				if (!skipped) {
+					let { content } = pageData;
+					if (options.compressContent) {
+						content = new Blob([new Uint8Array(content)], { type: "text/html" });
+					}
+					if (options.saveToGDrive) {
+						if (!(content instanceof Blob)) {
+							content = new Blob([content], { type: "text/html" });
+						}
+						await downloads.saveToGDrive(message.taskId, downloads.encodeSharpCharacter(pageData.filename), content, options, {
+							forceWebAuthFlow: options.forceWebAuthFlow
+						}, {
+							filenameConflictAction: options.filenameConflictAction
+						});
+					} else if (options.saveWithWebDAV) {
+						await downloads.saveWithWebDAV(message.taskId, downloads.encodeSharpCharacter(pageData.filename), content, options.webDAVURL, options.webDAVUser, options.webDAVPassword, {
+							filenameConflictAction: options.filenameConflictAction
+						});
+					} else if (options.saveToGitHub) {
+						await (await downloads.saveToGitHub(message.taskId, downloads.encodeSharpCharacter(pageData.filename), content, options.githubToken, options.githubUser, options.githubRepository, options.githubBranch, {
+							filenameConflictAction: options.filenameConflictAction
+						})).pushPromise;
+					} else if (options.saveWithCompanion && !options.compressContent) {
+						await companion.save({
+							filename: pageData.filename,
+							content: pageData.content,
+							filenameConflictAction: options.filenameConflictAction
+						});
+					} else {
+						if (!(content instanceof Blob)) {
+							content = new Blob([content], { type: "text/html" });
+						}
+						pageData.url = URL.createObjectURL(content);
+						await downloads.downloadPage(pageData, options);
+						if (options.openSavedPage && !options.compressContent) {
+							const createTabProperties = { active: true, url: URL.createObjectURL(content), windowId: tab.windowId };
+							const index = tab.index;
+							try {
+								await browser.tabs.get(tabId);
+								createTabProperties.index = index + 1;
+							} catch (error) {
+								createTabProperties.index = index;
+							}
+							browser.tabs.create(createTabProperties);
 						}
 						}
-						browser.tabs.create(createTabProperties);
 					}
 					}
-				}
-				if (pageData.hash) {
-					await woleet.anchor(pageData.hash, options.woleetKey);
+					if (pageData.hash) {
+						await woleet.anchor(pageData.hash, options.woleetKey);
+					}
 				}
 				}
 			}
 			}
 		} finally {
 		} finally {

+ 6 - 0
src/core/bg/config.js

@@ -126,10 +126,16 @@ const DEFAULT_CONFIG = {
 	includeBOM: false,
 	includeBOM: false,
 	warnUnsavedPage: true,
 	warnUnsavedPage: true,
 	displayInfobarInEditor: false,
 	displayInfobarInEditor: false,
+	compressContent: false,
+	createRootDirectory: false,
+	selfExtractingArchive: true,
+	extractDataFromPage: true,
+	insertTextBody: false,
 	autoSaveExternalSave: false,
 	autoSaveExternalSave: false,
 	insertMetaNoIndex: false,
 	insertMetaNoIndex: false,
 	insertMetaCSP: true,
 	insertMetaCSP: true,
 	passReferrerOnError: false,
 	passReferrerOnError: false,
+	password: "",
 	insertSingleFileComment: true,
 	insertSingleFileComment: true,
 	removeSavedDate: false,
 	removeSavedDate: false,
 	blockMixedContent: false,
 	blockMixedContent: false,

+ 223 - 99
src/core/bg/downloads.js

@@ -21,7 +21,7 @@
  *   Source.
  *   Source.
  */
  */
 
 
-/* global browser, Blob, URL, document, fetch */
+/* global browser, singlefile, URL, fetch, document, Blob */
 
 
 import * as config from "./config.js";
 import * as config from "./config.js";
 import * as bookmarks from "./bookmarks.js";
 import * as bookmarks from "./bookmarks.js";
@@ -35,8 +35,10 @@ import { GDrive } from "./../../lib/gdrive/gdrive.js";
 import { WebDAV } from "./../../lib/webdav/webdav.js";
 import { WebDAV } from "./../../lib/webdav/webdav.js";
 import { GitHub } from "./../../lib/github/github.js";
 import { GitHub } from "./../../lib/github/github.js";
 import { download } from "./download-util.js";
 import { download } from "./download-util.js";
+import * as yabson from "./../../lib/yabson/yabson.js";
 
 
 const partialContents = new Map();
 const partialContents = new Map();
+const parsers = new Map();
 const MIMETYPE_HTML = "text/html";
 const MIMETYPE_HTML = "text/html";
 const GDRIVE_CLIENT_ID = "207618107333-7tjs1im1pighftpoepea2kvkubnfjj44.apps.googleusercontent.com";
 const GDRIVE_CLIENT_ID = "207618107333-7tjs1im1pighftpoepea2kvkubnfjj44.apps.googleusercontent.com";
 const GDRIVE_CLIENT_KEY = "VQJ8Gq8Vxx72QyxPyeLtWvUt";
 const GDRIVE_CLIENT_KEY = "VQJ8Gq8Vxx72QyxPyeLtWvUt";
@@ -49,6 +51,7 @@ const gDrive = new GDrive(GDRIVE_CLIENT_ID, GDRIVE_CLIENT_KEY, SCOPES);
 export {
 export {
 	onMessage,
 	onMessage,
 	downloadPage,
 	downloadPage,
+	testSkipSave,
 	saveToGDrive,
 	saveToGDrive,
 	saveToGitHub,
 	saveToGitHub,
 	saveWithWebDAV,
 	saveWithWebDAV,
@@ -94,100 +97,209 @@ async function onMessage(message, sender) {
 }
 }
 
 
 async function downloadTabPage(message, tab) {
 async function downloadTabPage(message, tab) {
+	const tabId = tab.id;
 	let contents;
 	let contents;
 	if (message.blobURL) {
 	if (message.blobURL) {
 		try {
 		try {
-			message.content = await (await fetch(message.blobURL)).text();
+			if (message.compressContent) {
+				message.pageData = await yabson.parse(new Uint8Array(await (await fetch(message.blobURL)).arrayBuffer()));
+				await downloadCompressedContent(message, tab);
+			} else {
+				message.content = await (await fetch(message.blobURL)).text();
+				await downloadContent([message.content], tab, tab.incognito, message);
+			}
 		} catch (error) {
 		} catch (error) {
 			return { error: true };
 			return { error: true };
 		}
 		}
-	}
-	if (message.truncated) {
-		contents = partialContents.get(tab.id);
-		if (!contents) {
-			contents = [];
-			partialContents.set(tab.id, contents);
+	} else if (message.compressContent) {
+		let parser = parsers.get(tabId);
+		if (!parser) {
+			parser = yabson.getParser();
+			parsers.set(tabId, parser);
+		}
+		let result = await parser.next(message.data);
+		if (result.done) {
+			const message = result.value;
+			parsers.delete(tabId);
+			await downloadCompressedContent(message, tab);
+		}
+	} else {
+		if (message.truncated) {
+			contents = partialContents.get(tabId);
+			if (!contents) {
+				contents = [];
+				partialContents.set(tabId, contents);
+			}
+			contents.push(message.content);
+			if (message.finished) {
+				partialContents.delete(tabId);
+			}
+		} else if (message.content) {
+			contents = [message.content];
 		}
 		}
-		contents.push(message.content);
-		if (message.finished) {
-			partialContents.delete(tab.id);
+		if (!message.truncated || message.finished) {
+			await downloadContent(contents, tab, tab.incognito, message);
 		}
 		}
-	} else if (message.content) {
-		contents = [message.content];
 	}
 	}
-	if (!message.truncated || message.finished) {
+	return {};
+}
+
+async function downloadContent(contents, tab, incognito, message) {
+	const tabId = tab.id;
+	let skipped;
+	if (message.backgroundSave && !message.saveToGDrive && !message.saveWithWebDAV && !message.saveToGitHub) {
+		const testSkip = await testSkipSave(message.filename, message);
+		message.filenameConflictAction = testSkip.filenameConflictAction;
+		skipped = testSkip.skipped;
+	}
+	if (skipped) {
+		ui.onEnd(tabId);
+	} else {
 		if (message.openEditor) {
 		if (message.openEditor) {
-			ui.onEdit(tab.id);
+			ui.onEdit(tabId);
 			await editor.open({ tabIndex: tab.index + 1, filename: message.filename, content: contents.join("") });
 			await editor.open({ tabIndex: tab.index + 1, filename: message.filename, content: contents.join("") });
 		} else {
 		} else {
 			if (message.saveToClipboard) {
 			if (message.saveToClipboard) {
 				message.content = contents.join("");
 				message.content = contents.join("");
 				saveToClipboard(message);
 				saveToClipboard(message);
-				ui.onEnd(tab.id);
+				ui.onEnd(tabId);
 			} else {
 			} else {
-				await downloadContent(contents, tab, tab.incognito, message);
+				try {
+					const prompt = filename => promptFilename(tabId, filename);
+					let response;
+					if (message.saveWithWebDAV) {
+						response = await saveWithWebDAV(message.taskId, encodeSharpCharacter(message.filename), contents.join(""), message.webDAVURL, message.webDAVUser, message.webDAVPassword, { filenameConflictAction: message.filenameConflictAction, prompt });
+					} else if (message.saveToGDrive) {
+						await saveToGDrive(message.taskId, encodeSharpCharacter(message.filename), new Blob(contents, { type: MIMETYPE_HTML }), {
+							forceWebAuthFlow: message.forceWebAuthFlow
+						}, {
+							onProgress: (offset, size) => ui.onUploadProgress(tabId, offset, size),
+							filenameConflictAction: message.filenameConflictAction,
+							prompt
+						});
+					} else if (message.saveToGitHub) {
+						response = await saveToGitHub(message.taskId, encodeSharpCharacter(message.filename), contents.join(""), message.githubToken, message.githubUser, message.githubRepository, message.githubBranch, {
+							filenameConflictAction: message.filenameConflictAction,
+							prompt
+						});
+						await response.pushPromise;
+					} else if (message.saveWithCompanion) {
+						await companion.save({
+							filename: message.filename,
+							content: message.content,
+							filenameConflictAction: message.filenameConflictAction
+						});
+					} else {
+						message.url = URL.createObjectURL(new Blob(contents, { type: MIMETYPE_HTML }));
+						response = await downloadPage(message, {
+							confirmFilename: message.confirmFilename,
+							incognito,
+							filenameConflictAction: message.filenameConflictAction,
+							filenameReplacementCharacter: message.filenameReplacementCharacter,
+							includeInfobar: message.includeInfobar
+						});
+					}
+					if (message.replaceBookmarkURL && response && response.url) {
+						await bookmarks.update(message.bookmarkId, { url: response.url });
+					}
+					ui.onEnd(tabId);
+					if (message.openSavedPage) {
+						const createTabProperties = { active: true, url: URL.createObjectURL(new Blob(contents, { type: MIMETYPE_HTML })) };
+						if (tab.index != null) {
+							createTabProperties.index = tab.index + 1;
+						}
+						browser.tabs.create(createTabProperties);
+					}
+				} catch (error) {
+					if (!error.message || error.message != "upload_cancelled") {
+						console.error(error); // eslint-disable-line no-console
+						ui.onError(tabId, error.message, error.link);
+					}
+				} finally {
+					if (message.url) {
+						URL.revokeObjectURL(message.url);
+					}
+				}
 			}
 			}
 		}
 		}
 	}
 	}
-	return {};
 }
 }
 
 
-async function downloadContent(contents, tab, incognito, message) {
+async function downloadCompressedContent(message, tab) {
 	const tabId = tab.id;
 	const tabId = tab.id;
-	try {
-		const prompt = filename => promptFilename(tabId, filename);
-		let response;
-		if (message.saveWithWebDAV) {
-			response = await saveWithWebDAV(message.taskId, encodeSharpCharacter(message.filename), contents.join(""), message.webDAVURL, message.webDAVUser, message.webDAVPassword, { filenameConflictAction: message.filenameConflictAction, prompt });
-		} else if (message.saveToGDrive) {
-			await saveToGDrive(message.taskId, encodeSharpCharacter(message.filename), new Blob(contents, { type: MIMETYPE_HTML }), {
-				forceWebAuthFlow: message.forceWebAuthFlow
-			}, {
-				onProgress: (offset, size) => ui.onUploadProgress(tabId, offset, size),
-				filenameConflictAction: message.filenameConflictAction,
-				prompt
-			});
-		} else if (message.saveToGitHub) {
-			response = await saveToGitHub(message.taskId, encodeSharpCharacter(message.filename), contents.join(""), message.githubToken, message.githubUser, message.githubRepository, message.githubBranch, {
-				filenameConflictAction: message.filenameConflictAction,
-				prompt
-			});
-			await response.pushPromise;
-		} else if (message.saveWithCompanion) {
-			await companion.save({
-				filename: message.filename,
-				content: message.content,
-				filenameConflictAction: message.filenameConflictAction
-			});
-		} else {
-			message.url = URL.createObjectURL(new Blob(contents, { type: MIMETYPE_HTML }));
-			response = await downloadPage(message, {
-				confirmFilename: message.confirmFilename,
-				incognito,
-				filenameConflictAction: message.filenameConflictAction,
-				filenameReplacementCharacter: message.filenameReplacementCharacter,
-				includeInfobar: message.includeInfobar
-			});
-		}
-		if (message.replaceBookmarkURL && response && response.url) {
-			await bookmarks.update(message.bookmarkId, { url: response.url });
-		}
+	let skipped;
+	if (message.backgroundSave && !message.saveToGDrive && !message.saveWithWebDAV && !message.saveToGitHub) {
+		const testSkip = await testSkipSave(message.filename, message);
+		message.filenameConflictAction = testSkip.filenameConflictAction;
+		skipped = testSkip.skipped;
+	}
+	if (skipped) {
 		ui.onEnd(tabId);
 		ui.onEnd(tabId);
-		if (message.openSavedPage) {
-			const createTabProperties = { active: true, url: URL.createObjectURL(new Blob(contents, { type: MIMETYPE_HTML })) };
-			if (tab.index != null) {
-				createTabProperties.index = tab.index + 1;
+	} else {
+		const pageData = message.pageData;
+		const blob = await singlefile.processors.compression.process(pageData, {
+			insertTextBody: message.insertTextBody,
+			url: pageData.url || tab.url,
+			createRootDirectory: message.createRootDirectory,
+			tabId,
+			selfExtractingArchive: message.selfExtractingArchive,
+			extractDataFromPage: message.extractDataFromPage,
+			insertCanonicalLink: message.insertCanonicalLink,
+			insertMetaNoIndex: message.insertMetaNoIndex,
+			password: message.password
+		});
+		if (message.openEditor) {
+			ui.onEdit(tabId);
+			await editor.open({ tabIndex: tab.index + 1, filename: message.filename, content: Array.from(new Uint8Array(await blob.arrayBuffer())), compressContent: true });
+		} else {
+			try {
+				const prompt = filename => promptFilename(tabId, filename);
+				let response;
+				if (message.saveWithWebDAV) {
+					response = await saveWithWebDAV(message.taskId, encodeSharpCharacter(message.filename), blob, message.webDAVURL, message.webDAVUser, message.webDAVPassword, { filenameConflictAction: message.filenameConflictAction, prompt });
+				} else if (message.saveToGDrive) {
+					await saveToGDrive(message.taskId, encodeSharpCharacter(message.filename), blob, {
+						forceWebAuthFlow: message.forceWebAuthFlow
+					}, {
+						onProgress: (offset, size) => ui.onUploadProgress(tabId, offset, size),
+						filenameConflictAction: message.filenameConflictAction,
+						prompt
+					});
+				} else if (message.saveToGitHub) {
+					response = await saveToGitHub(message.taskId, encodeSharpCharacter(message.filename), blob, message.githubToken, message.githubUser, message.githubRepository, message.githubBranch, {
+						filenameConflictAction: message.filenameConflictAction,
+						prompt
+					});
+					await response.pushPromise;
+				} else {
+					if (message.backgroundSave) {
+						message.url = URL.createObjectURL(blob);
+						response = await downloadPage(message, {
+							confirmFilename: message.confirmFilename,
+							incognito: tab.incognito,
+							filenameConflictAction: message.filenameConflictAction,
+							filenameReplacementCharacter: message.filenameReplacementCharacter,
+							bookmarkId: message.bookmarkId,
+							replaceBookmarkURL: message.replaceBookmarkURL
+						});
+					} else {
+						await downloadPageForeground(message.taskId, message.filename, blob, tabId);
+					}
+				}
+				if (message.bookmarkId && message.replaceBookmarkURL && response && response.url) {
+					await bookmarks.update(message.bookmarkId, { url: response.url });
+				}
+				ui.onEnd(tabId);
+			} catch (error) {
+				if (!error.message || error.message != "upload_cancelled") {
+					console.error(error); // eslint-disable-line no-console
+					ui.onError(tabId, error.message);
+				}
+			} finally {
+				if (message.url) {
+					URL.revokeObjectURL(message.url);
+				}
 			}
 			}
-			browser.tabs.create(createTabProperties);
-		}
-	} catch (error) {
-		if (!error.message || error.message != "upload_cancelled") {
-			console.error(error); // eslint-disable-line no-console
-			ui.onError(tabId, error.message, error.link);
-		}
-	} finally {
-		if (message.url) {
-			URL.revokeObjectURL(message.url);
 		}
 		}
 	}
 	}
 }
 }
@@ -278,45 +390,46 @@ async function saveToGDrive(taskId, filename, blob, authOptions, uploadOptions)
 	}
 	}
 }
 }
 
 
-function promptFilename(tabId, filename) {
-	return browser.tabs.sendMessage(tabId, { method: "content.prompt", message: "Filename conflict, please enter a new filename", value: filename });
-}
-
-async function downloadPage(pageData, options) {
-	const filenameConflictAction = options.filenameConflictAction;
-	let skipped;
+async function testSkipSave(filename, options) {
+	let skipped, filenameConflictAction = options.filenameConflictAction;
 	if (filenameConflictAction == CONFLICT_ACTION_SKIP) {
 	if (filenameConflictAction == CONFLICT_ACTION_SKIP) {
 		const downloadItems = await browser.downloads.search({
 		const downloadItems = await browser.downloads.search({
-			filenameRegex: "(\\\\|/)" + getRegExp(pageData.filename) + "$",
+			filenameRegex: "(\\\\|/)" + getRegExp(filename) + "$",
 			exists: true
 			exists: true
 		});
 		});
 		if (downloadItems.length) {
 		if (downloadItems.length) {
 			skipped = true;
 			skipped = true;
 		} else {
 		} else {
-			options.filenameConflictAction = CONFLICT_ACTION_UNIQUIFY;
+			filenameConflictAction = CONFLICT_ACTION_UNIQUIFY;
 		}
 		}
 	}
 	}
-	if (!skipped) {
-		const downloadInfo = {
-			url: pageData.url,
-			saveAs: options.confirmFilename,
-			filename: pageData.filename,
-			conflictAction: options.filenameConflictAction
-		};
-		if (options.incognito) {
-			downloadInfo.incognito = true;
-		}
-		const downloadData = await download(downloadInfo, options.filenameReplacementCharacter);
-		if (downloadData.filename) {
-			let url = downloadData.filename;
-			if (!url.startsWith("file:")) {
-				if (url.startsWith("/")) {
-					url = downloadData.filename.substring(1);
-				}
-				url = "file:///" + encodeSharpCharacter(url);
+	return { skipped, filenameConflictAction };
+}
+
+function promptFilename(tabId, filename) {
+	return browser.tabs.sendMessage(tabId, { method: "content.prompt", message: "Filename conflict, please enter a new filename", value: filename });
+}
+
+async function downloadPage(pageData, options) {
+	const downloadInfo = {
+		url: pageData.url,
+		saveAs: options.confirmFilename,
+		filename: pageData.filename,
+		conflictAction: options.filenameConflictAction
+	};
+	if (options.incognito) {
+		downloadInfo.incognito = true;
+	}
+	const downloadData = await download(downloadInfo, options.filenameReplacementCharacter);
+	if (downloadData.filename) {
+		let url = downloadData.filename;
+		if (!url.startsWith("file:")) {
+			if (url.startsWith("/")) {
+				url = url.substring(1);
 			}
 			}
-			return { url };
+			url = "file:///" + encodeSharpCharacter(url);
 		}
 		}
+		return { url };
 	}
 	}
 }
 }
 
 
@@ -331,4 +444,15 @@ function saveToClipboard(pageData) {
 		event.clipboardData.setData("text/plain", pageData.content);
 		event.clipboardData.setData("text/plain", pageData.content);
 		event.preventDefault();
 		event.preventDefault();
 	}
 	}
+}
+
+async function downloadPageForeground(taskId, filename, content, tabId) {
+	const serializer = yabson.getSerializer({ filename, taskId, content: await content.arrayBuffer() });
+	for await (const data of serializer) {
+		await browser.tabs.sendMessage(tabId, {
+			method: "content.download",
+			data: Array.from(data)
+		});
+	}
+	await browser.tabs.sendMessage(tabId, { method: "content.download" });
 }
 }

+ 14 - 4
src/core/bg/editor.js

@@ -39,13 +39,13 @@ export {
 	EDITOR_URL
 	EDITOR_URL
 };
 };
 
 
-async function open({ tabIndex, content, filename }) {
+async function open({ tabIndex, content, filename, compressContent }) {
 	const createTabProperties = { active: true, url: EDITOR_PAGE_URL };
 	const createTabProperties = { active: true, url: EDITOR_PAGE_URL };
 	if (tabIndex != null) {
 	if (tabIndex != null) {
 		createTabProperties.index = tabIndex;
 		createTabProperties.index = tabIndex;
 	}
 	}
 	const tab = await browser.tabs.create(createTabProperties);
 	const tab = await browser.tabs.create(createTabProperties);
-	tabsData.set(tab.id, { content, filename });
+	tabsData.set(tab.id, { content, filename, compressContent });
 }
 }
 
 
 function onTabRemoved(tabId) {
 function onTabRemoved(tabId) {
@@ -65,7 +65,8 @@ async function onMessage(message, sender) {
 			const content = JSON.stringify(tabData);
 			const content = JSON.stringify(tabData);
 			for (let blockIndex = 0; blockIndex * MAX_CONTENT_SIZE < content.length; blockIndex++) {
 			for (let blockIndex = 0; blockIndex * MAX_CONTENT_SIZE < content.length; blockIndex++) {
 				const message = {
 				const message = {
-					method: "editor.setTabData"
+					method: "editor.setTabData",
+					compressContent: tabData.compressContent
 				};
 				};
 				message.truncated = content.length > MAX_CONTENT_SIZE;
 				message.truncated = content.length > MAX_CONTENT_SIZE;
 				if (message.truncated) {
 				if (message.truncated) {
@@ -102,7 +103,16 @@ async function onMessage(message, sender) {
 		if (!message.truncated || message.finished) {
 		if (!message.truncated || message.finished) {
 			const updateTabProperties = { url: EDITOR_PAGE_URL };
 			const updateTabProperties = { url: EDITOR_PAGE_URL };
 			await browser.tabs.update(tab.id, updateTabProperties);
 			await browser.tabs.update(tab.id, updateTabProperties);
-			tabsData.set(tab.id, { url: tab.url, content: contents.join(""), filename: message.filename });
+			const content = message.compressContent ? contents.flat() : contents.join("");
+			tabsData.set(tab.id, { 
+				url: tab.url, 
+				content, 
+				filename: message.filename, 
+				compressContent: message.compressContent,
+				selfExtractingArchive: message.selfExtractingArchive,
+				extractDataFromPageTags: message.extractDataFromPageTags,
+				insertTextBody: message.insertTextBody
+			});
 		}
 		}
 		return {};
 		return {};
 	}
 	}

+ 87 - 51
src/core/common/download.js

@@ -23,6 +23,8 @@
 
 
 /* global browser, document, URL, Blob, MouseEvent, setTimeout, open */
 /* global browser, document, URL, Blob, MouseEvent, setTimeout, open */
 
 
+import * as yabson from "./../../lib/yabson/yabson.js";
+
 const MAX_CONTENT_SIZE = 32 * (1024 * 1024);
 const MAX_CONTENT_SIZE = 32 * (1024 * 1024);
 
 
 export {
 export {
@@ -33,67 +35,101 @@ async function downloadPage(pageData, options) {
 	if (options.includeBOM) {
 	if (options.includeBOM) {
 		pageData.content = "\ufeff" + pageData.content;
 		pageData.content = "\ufeff" + pageData.content;
 	}
 	}
-	if (options.backgroundSave || options.openEditor || options.saveToGDrive || options.saveToGitHub || options.saveWithCompanion || options.saveWithWebDAV) {
-		const blobURL = URL.createObjectURL(new Blob([pageData.content], { type: "text/html" }));
-		const message = {
-			method: "downloads.download",
-			taskId: options.taskId,
-			confirmFilename: options.confirmFilename,
-			filenameConflictAction: options.filenameConflictAction,
-			filename: pageData.filename,
-			saveToClipboard: options.saveToClipboard,
-			saveToGDrive: options.saveToGDrive,
-			saveWithWebDAV: options.saveWithWebDAV,
-			webDAVURL: options.webDAVURL,
-			webDAVUser: options.webDAVUser,
-			webDAVPassword: options.webDAVPassword,
-			saveToGitHub: options.saveToGitHub,
-			githubToken: options.githubToken,
-			githubUser: options.githubUser,
-			githubRepository: options.githubRepository,
-			githubBranch: options.githubBranch,
-			saveWithCompanion: options.saveWithCompanion,
-			forceWebAuthFlow: options.forceWebAuthFlow,
-			filenameReplacementCharacter: options.filenameReplacementCharacter,
-			openEditor: options.openEditor,
-			openSavedPage: options.openSavedPage,
-			compressHTML: options.compressHTML,
-			backgroundSave: options.backgroundSave,
-			bookmarkId: options.bookmarkId,
-			replaceBookmarkURL: options.replaceBookmarkURL,
-			applySystemTheme: options.applySystemTheme,
-			defaultEditorMode: options.defaultEditorMode,
-			includeInfobar: options.includeInfobar,
-			warnUnsavedPage: options.warnUnsavedPage,
-			blobURL
-		};
+	const message = {
+		method: "downloads.download",
+		taskId: options.taskId,
+		insertTextBody: options.insertTextBody,
+		confirmFilename: options.confirmFilename,
+		filenameConflictAction: options.filenameConflictAction,
+		filename: pageData.filename,
+		saveToClipboard: options.saveToClipboard,
+		saveToGDrive: options.saveToGDrive,
+		saveWithWebDAV: options.saveWithWebDAV,
+		webDAVURL: options.webDAVURL,
+		webDAVUser: options.webDAVUser,
+		webDAVPassword: options.webDAVPassword,
+		saveToGitHub: options.saveToGitHub,
+		githubToken: options.githubToken,
+		githubUser: options.githubUser,
+		githubRepository: options.githubRepository,
+		githubBranch: options.githubBranch,
+		saveWithCompanion: options.saveWithCompanion,
+		forceWebAuthFlow: options.forceWebAuthFlow,
+		filenameReplacementCharacter: options.filenameReplacementCharacter,
+		openEditor: options.openEditor,
+		openSavedPage: options.openSavedPage,
+		compressHTML: options.compressHTML,
+		backgroundSave: options.backgroundSave,
+		bookmarkId: options.bookmarkId,
+		replaceBookmarkURL: options.replaceBookmarkURL,
+		applySystemTheme: options.applySystemTheme,
+		defaultEditorMode: options.defaultEditorMode,
+		includeInfobar: options.includeInfobar,
+		warnUnsavedPage: options.warnUnsavedPage,
+		createRootDirectory: options.createRootDirectory,
+		selfExtractingArchive: options.selfExtractingArchive,
+		extractDataFromPage: options.extractDataFromPage,
+		insertCanonicalLink: options.insertCanonicalLink,
+		insertMetaNoIndex: options.insertMetaNoIndex,
+		password: options.password,
+		compressContent: options.compressContent
+	};
+	if (options.compressContent) {
+		const blobURL = URL.createObjectURL(new Blob([await yabson.serialize(pageData)], { type: "application/octet-stream" }));
+		message.blobURL = blobURL;
 		const result = await browser.runtime.sendMessage(message);
 		const result = await browser.runtime.sendMessage(message);
 		URL.revokeObjectURL(blobURL);
 		URL.revokeObjectURL(blobURL);
 		if (result.error) {
 		if (result.error) {
 			message.blobURL = null;
 			message.blobURL = null;
-			for (let blockIndex = 0; blockIndex * MAX_CONTENT_SIZE < pageData.content.length; blockIndex++) {
-				message.truncated = pageData.content.length > MAX_CONTENT_SIZE;
-				if (message.truncated) {
-					message.finished = (blockIndex + 1) * MAX_CONTENT_SIZE > pageData.content.length;
-					message.content = pageData.content.substring(blockIndex * MAX_CONTENT_SIZE, (blockIndex + 1) * MAX_CONTENT_SIZE);
-				} else {
-					message.content = pageData.content;
-				}
-				await browser.runtime.sendMessage(message);
+			message.pageData = pageData;
+			const serializer = yabson.getSerializer(message);
+			for await (const data of serializer) {
+				await browser.runtime.sendMessage({
+					method: "downloads.download",
+					compressContent: true,
+					data: Array.from(data)
+				});
 			}
 			}
+			await browser.runtime.sendMessage({
+				method: "downloads.download",
+				compressContent: true
+			});
+		}
+		if (options.backgroundSave) {
+			await browser.runtime.sendMessage({ method: "downloads.end", taskId: options.taskId });
 		}
 		}
 	} else {
 	} else {
-		if (options.saveToClipboard) {
-			saveToClipboard(pageData);
+		if (options.backgroundSave || options.openEditor || options.saveToGDrive || options.saveToGitHub || options.saveWithCompanion || options.saveWithWebDAV) {
+			const blobURL = URL.createObjectURL(new Blob([pageData.content], { type: "text/html" }));
+			message.blobURL = blobURL;
+			const result = await browser.runtime.sendMessage(message);
+			URL.revokeObjectURL(blobURL);
+			if (result.error) {
+				message.blobURL = null;
+				for (let blockIndex = 0; blockIndex * MAX_CONTENT_SIZE < pageData.content.length; blockIndex++) {
+					message.truncated = pageData.content.length > MAX_CONTENT_SIZE;
+					if (message.truncated) {
+						message.finished = (blockIndex + 1) * MAX_CONTENT_SIZE > pageData.content.length;
+						message.content = pageData.content.substring(blockIndex * MAX_CONTENT_SIZE, (blockIndex + 1) * MAX_CONTENT_SIZE);
+					} else {
+						message.content = pageData.content;
+					}
+					await browser.runtime.sendMessage(message);
+				}
+			}
 		} else {
 		} else {
-			await downloadPageForeground(pageData);
-		}
-		if (options.openSavedPage) {
-			open(URL.createObjectURL(new Blob([pageData.content], { type: "text/html" })));
+			if (options.saveToClipboard) {
+				saveToClipboard(pageData);
+			} else {
+				await downloadPageForeground(pageData);
+			}
+			if (options.openSavedPage) {
+				open(URL.createObjectURL(new Blob([pageData.content], { type: "text/html" })));
+			}
+			browser.runtime.sendMessage({ method: "ui.processEnd" });
 		}
 		}
-		browser.runtime.sendMessage({ method: "ui.processEnd" });
+		await browser.runtime.sendMessage({ method: "downloads.end", taskId: options.taskId, hash: pageData.hash, woleetKey: options.woleetKey });
 	}
 	}
-	await browser.runtime.sendMessage({ method: "downloads.end", taskId: options.taskId, hash: pageData.hash, woleetKey: options.woleetKey });
 }
 }
 
 
 async function downloadPageForeground(pageData) {
 async function downloadPageForeground(pageData) {

+ 116 - 14
src/core/content/content-bootstrap.js

@@ -21,13 +21,14 @@
  *   Source.
  *   Source.
  */
  */
 
 
-/* global browser, globalThis, window, document, location, setTimeout, Node */
-
-const singlefile = globalThis.singlefileBootstrap;
+/* global browser, globalThis, window, document, location, setTimeout, XMLHttpRequest, Node, DOMParser */
 
 
 const MAX_CONTENT_SIZE = 32 * (1024 * 1024);
 const MAX_CONTENT_SIZE = 32 * (1024 * 1024);
 
 
-let unloadListenerAdded, optionsAutoSave, tabId, tabIndex, autoSaveEnabled, autoSaveTimeout, autoSavingPage, pageAutoSaved, previousLocationHref;
+const singlefile = globalThis.singlefileBootstrap;
+const pendingResponses = new Map();
+
+let unloadListenerAdded, optionsAutoSave, tabId, tabIndex, autoSaveEnabled, autoSaveTimeout, autoSavingPage, pageAutoSaved, previousLocationHref, savedPageDetected, compressContent, extractDataFromPageTags, insertTextBody;
 singlefile.pageInfo = {
 singlefile.pageInfo = {
 	updatedResources: {},
 	updatedResources: {},
 	visitDate: new Date()
 	visitDate: new Date()
@@ -57,11 +58,62 @@ browser.runtime.onMessage.addListener(message => {
 		message.method == "content.maybeInit" ||
 		message.method == "content.maybeInit" ||
 		message.method == "content.init" ||
 		message.method == "content.init" ||
 		message.method == "content.openEditor" ||
 		message.method == "content.openEditor" ||
-		message.method == "devtools.resourceCommitted") {
+		message.method == "devtools.resourceCommitted" ||
+		message.method == "singlefile.fetchResponse") {
 		return onMessage(message);
 		return onMessage(message);
 	}
 	}
 });
 });
 document.addEventListener("DOMContentLoaded", init, false);
 document.addEventListener("DOMContentLoaded", init, false);
+if (globalThis.window == globalThis.top && location && location.href && (location.href.startsWith("file://") || location.href.startsWith("content://"))) {
+	if (document.readyState == "loading") {
+		document.addEventListener("DOMContentLoaded", extractFile, false);
+	} else {
+		extractFile();
+	}
+}
+
+async function extractFile() {
+	if (document.documentElement.dataset.sfz !== undefined) {
+		const data = await getContent();
+		executeBootstrap(data);
+	} else {
+		if ((document.body && document.body.childNodes.length == 1 && document.body.childNodes[0].tagName == "PRE" && /<html[^>]* data-sfz[^>]*>/i.test(document.body.childNodes[0].textContent))) {
+			const doc = (new DOMParser()).parseFromString(document.body.childNodes[0].textContent, "text/html");
+			document.replaceChild(doc.documentElement, document.documentElement);
+			document.querySelectorAll("script").forEach(element => {
+				const scriptElement = document.createElement("script");
+				scriptElement.textContent = element.textContent;
+				element.parentElement.replaceChild(scriptElement, element);
+			});
+			await extractFile();
+		}
+	}
+}
+
+function getContent() {
+	return new Promise((resolve, reject) => {
+		const xhr = new XMLHttpRequest();
+		xhr.open("GET", location.href);
+		xhr.send();
+		xhr.responseType = "arraybuffer";
+		xhr.onload = () => resolve(new Uint8Array(xhr.response));
+		xhr.onerror = () => {
+			const errorMessageElement = document.getElementById("sfz-error-message");
+			if (errorMessageElement) {
+				errorMessageElement.remove();
+			}
+			const requestId = pendingResponses.size;
+			pendingResponses.set(requestId, { resolve, reject });
+			browser.runtime.sendMessage({ method: "singlefile.fetch", requestId, url: location.href });
+		};
+	});
+}
+
+function executeBootstrap(data) {
+	const scriptElement = document.createElement("script");
+	scriptElement.textContent = "(() => { document.currentScript.remove(); const bootstrapReady = this.bootstrap && this.bootstrap([" + (new Uint8Array(data)).toString() + "]); if (bootstrapReady) { bootstrapReady.then(() => document.dispatchEvent(new CustomEvent(\"single-file-display-infobar\"))); } })()";
+	document.body.appendChild(scriptElement);
+}
 
 
 async function onMessage(message) {
 async function onMessage(message) {
 	if (autoSaveEnabled && message.method == "content.autosave") {
 	if (autoSaveEnabled && message.method == "content.autosave") {
@@ -90,6 +142,36 @@ async function onMessage(message) {
 		singlefile.pageInfo.updatedResources[message.url] = { content: message.content, type: message.type, encoding: message.encoding };
 		singlefile.pageInfo.updatedResources[message.url] = { content: message.content, type: message.type, encoding: message.encoding };
 		return {};
 		return {};
 	}
 	}
+	if (message.method == "singlefile.fetchResponse") {
+		return await onFetchResponse(message);
+	}
+}
+
+async function onFetchResponse(message) {
+	const pendingResponse = pendingResponses.get(message.requestId);
+	if (pendingResponse) {
+		if (message.error) {
+			pendingResponse.reject(new Error(message.error));
+			pendingResponses.delete(message.requestId);
+		} else {
+			if (message.truncated) {
+				if (pendingResponse.array) {
+					pendingResponse.array = pendingResponse.array.concat(message.array);
+				} else {
+					pendingResponse.array = message.array;
+					pendingResponses.set(message.requestId, pendingResponse);
+				}
+				if (message.finished) {
+					message.array = pendingResponse.array;
+				}
+			}
+			if (!message.truncated || message.finished) {
+				pendingResponse.resolve(message.array);
+				pendingResponses.delete(message.requestId);
+			}
+		}
+		return {};
+	}
 }
 }
 
 
 function init() {
 function init() {
@@ -228,29 +310,49 @@ function savePage(docData, frames, { autoSaveUnload, autoSaveDiscard, autoSaveRe
 }
 }
 
 
 async function openEditor(document) {
 async function openEditor(document) {
-	serializeShadowRoots(document);
-	const content = singlefile.helper.serialize(document);
+	let content;
+	if (compressContent) {
+		content = await getContent();
+	} else {
+		serializeShadowRoots(document);
+		content = singlefile.helper.serialize(document);
+	}
 	for (let blockIndex = 0; blockIndex * MAX_CONTENT_SIZE < content.length; blockIndex++) {
 	for (let blockIndex = 0; blockIndex * MAX_CONTENT_SIZE < content.length; blockIndex++) {
 		const message = {
 		const message = {
 			method: "editor.open",
 			method: "editor.open",
-			filename: decodeURIComponent(location.href.match(/^.*\/(.*)$/)[1])
+			filename: decodeURIComponent(location.href.match(/^.*\/(.*)$/)[1]),
+			compressContent,
+			extractDataFromPageTags,
+			insertTextBody,
+			selfExtractingArchive: compressContent
 		};
 		};
 		message.truncated = content.length > MAX_CONTENT_SIZE;
 		message.truncated = content.length > MAX_CONTENT_SIZE;
 		if (message.truncated) {
 		if (message.truncated) {
 			message.finished = (blockIndex + 1) * MAX_CONTENT_SIZE > content.length;
 			message.finished = (blockIndex + 1) * MAX_CONTENT_SIZE > content.length;
-			message.content = content.substring(blockIndex * MAX_CONTENT_SIZE, (blockIndex + 1) * MAX_CONTENT_SIZE);
+			if (content instanceof Uint8Array) {
+				message.content = Array.from(content.subarray(blockIndex * MAX_CONTENT_SIZE, (blockIndex + 1) * MAX_CONTENT_SIZE));
+			} else {
+				message.content = content.substring(blockIndex * MAX_CONTENT_SIZE, (blockIndex + 1) * MAX_CONTENT_SIZE);
+			}
 		} else {
 		} else {
-			message.content = content;
+			message.content = content instanceof Uint8Array ? Array.from(content) : content;
 		}
 		}
 		await browser.runtime.sendMessage(message);
 		await browser.runtime.sendMessage(message);
 	}
 	}
 }
 }
 
 
 function detectSavedPage(document) {
 function detectSavedPage(document) {
-	const helper = singlefile.helper;
-	const firstDocumentChild = document.documentElement.firstChild;
-	return firstDocumentChild.nodeType == Node.COMMENT_NODE &&
-		(firstDocumentChild.textContent.includes(helper.COMMENT_HEADER) || firstDocumentChild.textContent.includes(helper.COMMENT_HEADER_LEGACY));
+	if (savedPageDetected === undefined) {
+		const helper = singlefile.helper;
+		const firstDocumentChild = document.documentElement.firstChild;
+		compressContent = document.documentElement.dataset.sfz == "";
+		extractDataFromPageTags = Boolean(document.querySelector("sfz-extra-data"));
+		insertTextBody = Boolean(document.querySelector("body > main[hidden]"));
+		savedPageDetected = compressContent || (
+			firstDocumentChild.nodeType == Node.COMMENT_NODE &&
+			(firstDocumentChild.textContent.includes(helper.COMMENT_HEADER) || firstDocumentChild.textContent.includes(helper.COMMENT_HEADER_LEGACY)));
+	}
+	return savedPageDetected;
 }
 }
 
 
 function serializeShadowRoots(node) {
 function serializeShadowRoots(node) {

+ 20 - 3
src/core/content/content.js

@@ -21,24 +21,25 @@
  *   Source.
  *   Source.
  */
  */
 
 
-/* global browser, document, globalThis, location */
+/* global browser, document, globalThis, location, URL, Blob, MouseEvent */
 
 
 import * as download from "./../common/download.js";
 import * as download from "./../common/download.js";
 import { fetch, frameFetch } from "./../../lib/single-file/fetch/content/content-fetch.js";
 import { fetch, frameFetch } from "./../../lib/single-file/fetch/content/content-fetch.js";
 import * as ui from "./../../ui/content/content-ui.js";
 import * as ui from "./../../ui/content/content-ui.js";
 import { onError } from "./../../ui/common/content-error.js";
 import { onError } from "./../../ui/common/content-error.js";
+import * as yabson from "./../../lib/yabson/yabson.js";
 
 
 const singlefile = globalThis.singlefile;
 const singlefile = globalThis.singlefile;
 const bootstrap = globalThis.singlefileBootstrap;
 const bootstrap = globalThis.singlefileBootstrap;
 
 
 const MOZ_EXTENSION_PROTOCOL = "moz-extension:";
 const MOZ_EXTENSION_PROTOCOL = "moz-extension:";
 
 
-let processor, processing;
+let processor, processing, downloadParser;
 
 
 if (!bootstrap || !bootstrap.initializedSingleFile) {
 if (!bootstrap || !bootstrap.initializedSingleFile) {
 	singlefile.init({ fetch, frameFetch });
 	singlefile.init({ fetch, frameFetch });
 	browser.runtime.onMessage.addListener(message => {
 	browser.runtime.onMessage.addListener(message => {
-		if (message.method == "content.save" || message.method == "content.cancelSave" || message.method == "content.getSelectedLinks" || message.method == "content.error" || message.method == "content.prompt") {
+		if (message.method == "content.save" || message.method == "content.cancelSave" || message.method == "content.download" || message.method == "content.getSelectedLinks" || message.method == "content.error" || message.method == "content.prompt") {
 			return onMessage(message);
 			return onMessage(message);
 		}
 		}
 	});
 	});
@@ -71,6 +72,22 @@ async function onMessage(message) {
 				urls: ui.getSelectedLinks()
 				urls: ui.getSelectedLinks()
 			};
 			};
 		}
 		}
+		if (message.method == "content.download") {
+			if (!downloadParser) {
+				downloadParser = yabson.getParser();
+			}
+			const result = await downloadParser.next(message.data);
+			if (result.done) {
+				downloadParser = null;
+				const link = document.createElement("a");
+				link.download = result.value.filename;
+				link.href = URL.createObjectURL(new Blob([result.value.content]), "text/html");
+				link.dispatchEvent(new MouseEvent("click"));
+				URL.revokeObjectURL(link.href);
+				await browser.runtime.sendMessage({ method: "downloads.end", taskId: result.value.taskId });
+			}
+			return {};
+		}
 		if (message.method == "content.error") {
 		if (message.method == "content.error") {
 			onError(message.error, message.link);
 			onError(message.error, message.link);
 			return {};
 			return {};

+ 1 - 1
src/lib/woleet/woleet.js

@@ -22,7 +22,7 @@
  */
  */
 /* global fetch */
 /* global fetch */
 const urlService = "https://api.woleet.io/v1/anchor";
 const urlService = "https://api.woleet.io/v1/anchor";
-const apiKey = "";
+const apiKey = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhYzZmZTMzMi0wODNjLTRjZmMtYmYxNC0xNWU5MTJmMWY4OWIiLCJpYXQiOjE1NzYxNzQzNDV9.n31j9ctJj7R1Vjwyc5yd1d6Cmg0NDnpwSaLWsqtZJQA";
 export {
 export {
 	anchor
 	anchor
 };
 };

+ 980 - 0
src/lib/yabson/yabson.js

@@ -0,0 +1,980 @@
+/* global TextEncoder, TextDecoder */
+
+const DEFAULT_CHUNK_SIZE = 8 * 1024 * 1024;
+const TYPE_REFERENCE = 0;
+const SPECIAL_TYPES = [TYPE_REFERENCE];
+const EMPTY_SLOT_VALUE = Symbol();
+
+const textEncoder = new TextEncoder();
+const textDecoder = new TextDecoder();
+const types = new Array(256);
+let typeIndex = 0;
+
+registerType(serializeCircularReference, parseCircularReference, testCircularReference, TYPE_REFERENCE);
+registerType(null, parseObject, testObject);
+registerType(serializeArray, parseArray, testArray);
+registerType(serializeString, parseString, testString);
+registerType(serializeTypedArray, parseFloat64Array, testFloat64Array);
+registerType(serializeTypedArray, parseFloat32Array, testFloat32Array);
+registerType(serializeTypedArray, parseUint32Array, testUint32Array);
+registerType(serializeTypedArray, parseInt32Array, testInt32Array);
+registerType(serializeTypedArray, parseUint16Array, testUint16Array);
+registerType(serializeTypedArray, parseInt16Array, testInt16Array);
+registerType(serializeTypedArray, parseUint8ClampedArray, testUint8ClampedArray);
+registerType(serializeTypedArray, parseUint8Array, testUint8Array);
+registerType(serializeTypedArray, parseInt8Array, testInt8Array);
+registerType(serializeArrayBuffer, parseArrayBuffer, testArrayBuffer);
+registerType(serializeNumber, parseNumber, testNumber);
+registerType(serializeUint32, parseUint32, testUint32);
+registerType(serializeInt32, parseInt32, testInt32);
+registerType(serializeUint16, parseUint16, testUint16);
+registerType(serializeInt16, parseInt16, testInt16);
+registerType(serializeUint8, parseUint8, testUint8);
+registerType(serializeInt8, parseInt8, testInt8);
+registerType(null, parseUndefined, testUndefined);
+registerType(null, parseNull, testNull);
+registerType(null, parseNaN, testNaN);
+registerType(serializeBoolean, parseBoolean, testBoolean);
+registerType(serializeSymbol, parseSymbol, testSymbol);
+registerType(null, parseEmptySlot, testEmptySlot);
+registerType(serializeMap, parseMap, testMap);
+registerType(serializeSet, parseSet, testSet);
+registerType(serializeDate, parseDate, testDate);
+registerType(serializeError, parseError, testError);
+registerType(serializeRegExp, parseRegExp, testRegExp);
+registerType(serializeStringObject, parseStringObject, testStringObject);
+registerType(serializeNumberObject, parseNumberObject, testNumberObject);
+registerType(serializeBooleanObject, parseBooleanObject, testBooleanObject);
+
+export {
+	getSerializer,
+	getParser,
+	registerType,
+	clone,
+	serialize,
+	parse,
+	serializeValue,
+	serializeArray,
+	serializeString,
+	serializeTypedArray,
+	serializeArrayBuffer,
+	serializeNumber,
+	serializeUint32,
+	serializeInt32,
+	serializeUint16,
+	serializeInt16,
+	serializeUint8,
+	serializeInt8,
+	serializeBoolean,
+	serializeMap,
+	serializeSet,
+	serializeDate,
+	serializeError,
+	serializeRegExp,
+	serializeStringObject,
+	serializeNumberObject,
+	serializeBooleanObject,
+	serializeSymbol,
+	parseValue,
+	parseObject,
+	parseArray,
+	parseString,
+	parseFloat64Array,
+	parseFloat32Array,
+	parseUint32Array,
+	parseInt32Array,
+	parseUint16Array,
+	parseInt16Array,
+	parseUint8ClampedArray,
+	parseUint8Array,
+	parseInt8Array,
+	parseArrayBuffer,
+	parseNumber,
+	parseUint32,
+	parseInt32,
+	parseUint16,
+	parseInt16,
+	parseUint8,
+	parseInt8,
+	parseUndefined,
+	parseNull,
+	parseNaN,
+	parseBoolean,
+	parseMap,
+	parseSet,
+	parseDate,
+	parseError,
+	parseRegExp,
+	parseStringObject,
+	parseNumberObject,
+	parseBooleanObject,
+	parseSymbol,
+	testObject,
+	testArray,
+	testString,
+	testFloat64Array,
+	testFloat32Array,
+	testUint32Array,
+	testInt32Array,
+	testUint16Array,
+	testInt16Array,
+	testUint8ClampedArray,
+	testUint8Array,
+	testInt8Array,
+	testArrayBuffer,
+	testNumber,
+	testBigInt,
+	testUint32,
+	testInt32,
+	testUint16,
+	testInt16,
+	testUint8,
+	testInt8,
+	testInteger,
+	testUndefined,
+	testNull,
+	testNaN,
+	testBoolean,
+	testMap,
+	testSet,
+	testDate,
+	testError,
+	testRegExp,
+	testStringObject,
+	testNumberObject,
+	testBooleanObject,
+	testSymbol
+};
+
+function registerType(serialize, parse, test, type) {
+	if (type === undefined) {
+		typeIndex++;
+		if (types.length - typeIndex >= SPECIAL_TYPES.length) {
+			types[types.length - typeIndex] = { serialize, parse, test };
+		} else {
+			throw new Error("Reached maximum number of custom types");
+		}
+	} else {
+		types[type] = { serialize, parse, test };
+	}
+}
+
+async function clone(object, options) {
+	const serializer = getSerializer(object, options);
+	const parser = getParser();
+	let result;
+	for await (const chunk of serializer) {
+		result = await parser.next(chunk);
+	}
+	result = await parser.next();
+	return result.value;
+}
+
+async function serialize(object, options) {
+	const serializer = getSerializer(object, options);
+	let result = new Uint8Array([]);
+	for await (const chunk of serializer) {
+		const previousResult = result;
+		result = new Uint8Array(previousResult.length + chunk.length);
+		result.set(previousResult, 0);
+		result.set(chunk, previousResult.length);
+	}
+	return result;
+}
+
+async function parse(array) {
+	const parser = getParser();
+	await parser.next(array);
+	const result = await parser.next();
+	return result.value;
+}
+
+class SerializerData {
+	constructor(appendData, chunkSize) {
+		this.stream = new WriteStream(appendData, chunkSize);
+		this.objects = [];
+	}
+
+	append(array) {
+		return this.stream.append(array);
+	}
+
+	flush() {
+		return this.stream.flush();
+	}
+
+	addObject(value) {
+		this.objects.push(testReferenceable(value) && !testCircularReference(value, this) ? value : undefined);
+	}
+}
+
+class WriteStream {
+	constructor(appendData, chunkSize) {
+		this.offset = 0;
+		this.appendData = appendData;
+		this.value = new Uint8Array(chunkSize);
+	}
+
+	async append(array) {
+		if (this.offset + array.length > this.value.length) {
+			const offset = this.value.length - this.offset;
+			await this.append(array.subarray(0, offset));
+			await this.appendData({ value: this.value });
+			this.offset = 0;
+			await this.append(array.subarray(offset));
+		} else {
+			this.value.set(array, this.offset);
+			this.offset += array.length;
+		}
+	}
+
+	async flush() {
+		if (this.offset) {
+			await this.appendData({ value: this.value.subarray(0, this.offset), done: true });
+		}
+	}
+}
+
+function getSerializer(value, { chunkSize = DEFAULT_CHUNK_SIZE } = {}) {
+	let serializerData, result, setResult, iterationDone, previousResult, resolvePreviousResult;
+	return {
+		[Symbol.asyncIterator]() {
+			return {
+				next() {
+					return iterationDone ? { done: iterationDone } : getResult();
+				},
+				return() {
+					return { done: true };
+				}
+			};
+		}
+	};
+
+	async function getResult() {
+		if (resolvePreviousResult) {
+			resolvePreviousResult();
+		} else {
+			initSerializerData().catch(() => { /* ignored */ });
+		}
+		initPreviousData();
+		const value = await getValue();
+		return { value };
+	}
+
+	async function initSerializerData() {
+		initResult();
+		serializerData = new SerializerData(appendData, chunkSize);
+		await serializeValue(serializerData, value);
+		await serializerData.flush();
+	}
+
+	function initResult() {
+		result = new Promise(resolve => setResult = resolve);
+	}
+
+	function initPreviousData() {
+		previousResult = new Promise(resolve => resolvePreviousResult = resolve);
+	}
+
+	async function appendData(result) {
+		setResult(result);
+		await previousResult;
+	}
+
+	async function getValue() {
+		const { value, done } = await result;
+		iterationDone = done;
+		if (!done) {
+			initResult();
+		}
+		return value;
+	}
+}
+
+async function serializeValue(data, value) {
+	const type = types.findIndex(({ test } = {}) => test && test(value, data));
+	data.addObject(value);
+	await data.append(new Uint8Array([type]));
+	const serialize = types[type].serialize;
+	if (serialize) {
+		await serialize(data, value);
+	}
+	if (type != TYPE_REFERENCE && testObject(value)) {
+		await serializeSymbols(data, value);
+		await serializeOwnProperties(data, value);
+	}
+}
+
+async function serializeSymbols(data, value) {
+	const ownPropertySymbols = Object.getOwnPropertySymbols(value);
+	const symbols = ownPropertySymbols.map(propertySymbol => [propertySymbol, value[propertySymbol]]);
+	await serializeArray(data, symbols);
+}
+
+async function serializeOwnProperties(data, value) {
+	let entries = Object.entries(value);
+	if (testArray(value)) {
+		entries = entries.filter(([key]) => !testInteger(Number(key)));
+	}
+	await serializeValue(data, entries.length);
+	for (const [key, value] of entries) {
+		await serializeString(data, key);
+		await serializeValue(data, value);
+	}
+}
+
+async function serializeCircularReference(data, value) {
+	const index = data.objects.indexOf(value);
+	await serializeValue(data, index);
+}
+
+async function serializeArray(data, array) {
+	await serializeValue(data, array.length);
+	const notEmptyIndexes = Object.keys(array).filter(key => testInteger(Number(key))).map(key => Number(key));
+	let indexNotEmptyIndexes = 0, currentNotEmptyIndex = notEmptyIndexes[indexNotEmptyIndexes];
+	for (const [indexArray, value] of array.entries()) {
+		if (currentNotEmptyIndex == indexArray) {
+			currentNotEmptyIndex = notEmptyIndexes[++indexNotEmptyIndexes];
+			await serializeValue(data, value);
+		} else {
+			await serializeValue(data, EMPTY_SLOT_VALUE);
+		}
+	}
+}
+
+async function serializeString(data, string) {
+	const encodedString = textEncoder.encode(string);
+	await serializeValue(data, encodedString.length);
+	await data.append(encodedString);
+}
+
+async function serializeTypedArray(data, array) {
+	await serializeValue(data, array.length);
+	await data.append(new Uint8Array(array.buffer));
+}
+
+async function serializeArrayBuffer(data, arrayBuffer) {
+	await serializeValue(data, arrayBuffer.byteLength);
+	await data.append(new Uint8Array(arrayBuffer));
+}
+
+async function serializeNumber(data, number) {
+	const serializedNumber = new Uint8Array(new Float64Array([number]).buffer);
+	await data.append(serializedNumber);
+}
+
+async function serializeUint32(data, number) {
+	const serializedNumber = new Uint8Array(new Uint32Array([number]).buffer);
+	await data.append(serializedNumber);
+}
+
+async function serializeInt32(data, number) {
+	const serializedNumber = new Uint8Array(new Int32Array([number]).buffer);
+	await data.append(serializedNumber);
+}
+
+async function serializeUint16(data, number) {
+	const serializedNumber = new Uint8Array(new Uint16Array([number]).buffer);
+	await data.append(serializedNumber);
+}
+
+async function serializeInt16(data, number) {
+	const serializedNumber = new Uint8Array(new Int16Array([number]).buffer);
+	await data.append(serializedNumber);
+}
+
+async function serializeUint8(data, number) {
+	const serializedNumber = new Uint8Array([number]);
+	await data.append(serializedNumber);
+}
+
+async function serializeInt8(data, number) {
+	const serializedNumber = new Uint8Array(new Int8Array([number]).buffer);
+	await data.append(serializedNumber);
+}
+
+async function serializeBoolean(data, boolean) {
+	const serializedBoolean = new Uint8Array([Number(boolean)]);
+	await data.append(serializedBoolean);
+}
+
+async function serializeMap(data, map) {
+	const entries = map.entries();
+	await serializeValue(data, map.size);
+	for (const [key, value] of entries) {
+		await serializeValue(data, key);
+		await serializeValue(data, value);
+	}
+}
+
+async function serializeSet(data, set) {
+	await serializeValue(data, set.size);
+	for (const value of set) {
+		await serializeValue(data, value);
+	}
+}
+
+async function serializeDate(data, date) {
+	await serializeNumber(data, date.getTime());
+}
+
+async function serializeError(data, error) {
+	await serializeString(data, error.message);
+	await serializeString(data, error.stack);
+}
+
+async function serializeRegExp(data, regExp) {
+	await serializeString(data, regExp.source);
+	await serializeString(data, regExp.flags);
+}
+
+async function serializeStringObject(data, string) {
+	await serializeString(data, string.valueOf());
+}
+
+async function serializeNumberObject(data, number) {
+	await serializeNumber(data, number.valueOf());
+}
+
+async function serializeBooleanObject(data, boolean) {
+	await serializeBoolean(data, boolean.valueOf());
+}
+
+async function serializeSymbol(data, symbol) {
+	await serializeString(data, symbol.description);
+}
+
+class Reference {
+	constructor(index, data) {
+		this.index = index;
+		this.data = data;
+	}
+
+	getObject() {
+		return this.data.objects[this.index];
+	}
+}
+
+class ParserData {
+	constructor(consumeData) {
+		this.stream = new ReadStream(consumeData);
+		this.objects = [];
+		this.setters = [];
+	}
+
+	consume(size) {
+		return this.stream.consume(size);
+	}
+
+	getObjectId() {
+		const objectIndex = this.objects.length;
+		this.objects.push(undefined);
+		return objectIndex;
+	}
+
+	resolveObject(objectId, value) {
+		if (testReferenceable(value) && !testReference(value)) {
+			this.objects[objectId] = value;
+		}
+	}
+
+	setObject(functionArguments, setterFunction) {
+		this.setters.push({ functionArguments, setterFunction });
+	}
+
+	executeSetters() {
+		this.setters.forEach(({ functionArguments, setterFunction }) => {
+			const resolvedArguments = functionArguments.map(argument => testReference(argument) ? argument.getObject() : argument);
+			setterFunction(...resolvedArguments);
+		});
+	}
+}
+
+class ReadStream {
+	constructor(consumeData) {
+		this.offset = 0;
+		this.value = new Uint8Array(0);
+		this.consumeData = consumeData;
+	}
+
+	async consume(size) {
+		if (this.offset + size > this.value.length) {
+			const pending = this.value.subarray(this.offset, this.value.length);
+			const value = await this.consumeData();
+			if (pending.length + value.length != this.value.length) {
+				this.value = new Uint8Array(pending.length + value.length);
+			}
+			this.value.set(pending);
+			this.value.set(value, pending.length);
+			this.offset = 0;
+			return this.consume(size);
+		} else {
+			const result = this.value.slice(this.offset, this.offset + size);
+			this.offset += result.length;
+			return result;
+		}
+	}
+}
+
+function getParser() {
+	let parserData, input, setInput, value, previousData, resolvePreviousData;
+	return {
+		async next(input) {
+			return input ? getResult(input) : { value: await value, done: true };
+		},
+		return() {
+			return { done: true };
+		}
+	};
+
+	async function getResult(input) {
+		if (previousData) {
+			await previousData;
+		} else {
+			initParserData().catch(() => { /* ignored */ });
+		}
+		initPreviousData();
+		setInput(input);
+		return { done: false };
+	}
+
+	async function initParserData() {
+		let setValue;
+		value = new Promise(resolve => setValue = resolve);
+		parserData = new ParserData(consumeData);
+		initChunk();
+		const data = await parseValue(parserData);
+		parserData.executeSetters();
+		setValue(data);
+	}
+
+	function initChunk() {
+		input = new Promise(resolve => setInput = resolve);
+	}
+
+	function initPreviousData() {
+		previousData = new Promise(resolve => resolvePreviousData = resolve);
+	}
+
+	async function consumeData() {
+		const data = await input;
+		initChunk();
+		if (resolvePreviousData) {
+			resolvePreviousData();
+		}
+		return data;
+	}
+}
+
+async function parseValue(data) {
+	const array = await data.consume(1);
+	const parserType = array[0];
+	const parse = types[parserType].parse;
+	const valueId = data.getObjectId();
+	const result = await parse(data);
+	if (parserType != TYPE_REFERENCE && testObject(result)) {
+		await parseSymbols(data, result);
+		await parseOwnProperties(data, result);
+	}
+	data.resolveObject(valueId, result);
+	return result;
+}
+
+async function parseSymbols(data, value) {
+	const symbols = await parseArray(data);
+	data.setObject([symbols], symbols => symbols.forEach(([symbol, propertyValue]) => value[symbol] = propertyValue));
+}
+
+async function parseOwnProperties(data, object) {
+	const size = await parseValue(data);
+	if (size) {
+		await parseNextProperty();
+	}
+
+	async function parseNextProperty(indexKey = 0) {
+		const key = await parseString(data);
+		const value = await parseValue(data);
+		data.setObject([value], value => object[key] = value);
+		if (indexKey < size - 1) {
+			await parseNextProperty(indexKey + 1);
+		}
+	}
+}
+
+async function parseCircularReference(data) {
+	const index = await parseValue(data);
+	const result = new Reference(index, data);
+	return result;
+}
+
+function parseObject() {
+	return {};
+}
+
+async function parseArray(data) {
+	const length = await parseValue(data);
+	const array = new Array(length);
+	if (length) {
+		await parseNextSlot();
+	}
+	return array;
+
+	async function parseNextSlot(indexArray = 0) {
+		const value = await parseValue(data);
+		if (!testEmptySlot(value)) {
+			data.setObject([value], value => array[indexArray] = value);
+		}
+		if (indexArray < length - 1) {
+			await parseNextSlot(indexArray + 1);
+		}
+	}
+}
+
+function parseEmptySlot() {
+	return EMPTY_SLOT_VALUE;
+}
+
+async function parseString(data) {
+	const size = await parseValue(data);
+	const array = await data.consume(size);
+	return textDecoder.decode(array);
+}
+
+async function parseFloat64Array(data) {
+	const length = await parseValue(data);
+	const array = await data.consume(length * 8);
+	return new Float64Array(array.buffer);
+}
+
+async function parseFloat32Array(data) {
+	const length = await parseValue(data);
+	const array = await data.consume(length * 4);
+	return new Float32Array(array.buffer);
+}
+
+async function parseUint32Array(data) {
+	const length = await parseValue(data);
+	const array = await data.consume(length * 4);
+	return new Uint32Array(array.buffer);
+}
+
+async function parseInt32Array(data) {
+	const length = await parseValue(data);
+	const array = await data.consume(length * 4);
+	return new Int32Array(array.buffer);
+}
+
+async function parseUint16Array(data) {
+	const length = await parseValue(data);
+	const array = await data.consume(length * 2);
+	return new Uint16Array(array.buffer);
+}
+
+async function parseInt16Array(data) {
+	const length = await parseValue(data);
+	const array = await data.consume(length * 2);
+	return new Int16Array(array.buffer);
+}
+
+async function parseUint8ClampedArray(data) {
+	const length = await parseValue(data);
+	const array = await data.consume(length);
+	return new Uint8ClampedArray(array.buffer);
+}
+
+async function parseUint8Array(data) {
+	const length = await parseValue(data);
+	const array = await data.consume(length);
+	return array;
+}
+
+async function parseInt8Array(data) {
+	const length = await parseValue(data);
+	const array = await data.consume(length);
+	return new Int8Array(array.buffer);
+}
+
+async function parseArrayBuffer(data) {
+	const length = await parseValue(data);
+	const array = await data.consume(length);
+	return array.buffer;
+}
+
+async function parseNumber(data) {
+	const array = await data.consume(8);
+	return new Float64Array(array.buffer)[0];
+}
+
+async function parseUint32(data) {
+	const array = await data.consume(4);
+	return new Uint32Array(array.buffer)[0];
+}
+
+async function parseInt32(data) {
+	const array = await data.consume(4);
+	return new Int32Array(array.buffer)[0];
+}
+
+async function parseUint16(data) {
+	const array = await data.consume(2);
+	return new Uint16Array(array.buffer)[0];
+}
+
+async function parseInt16(data) {
+	const array = await data.consume(2);
+	return new Int16Array(array.buffer)[0];
+}
+
+async function parseUint8(data) {
+	const array = await data.consume(1);
+	return new Uint8Array(array.buffer)[0];
+}
+
+async function parseInt8(data) {
+	const array = await data.consume(1);
+	return new Int8Array(array.buffer)[0];
+}
+
+function parseUndefined() {
+	return undefined;
+}
+
+function parseNull() {
+	return null;
+}
+
+function parseNaN() {
+	return NaN;
+}
+
+async function parseBoolean(data) {
+	const array = await data.consume(1);
+	return Boolean(array[0]);
+}
+
+async function parseMap(data) {
+	const size = await parseValue(data);
+	const map = new Map();
+	if (size) {
+		await parseNextEntry();
+	}
+	return map;
+
+	async function parseNextEntry(indexKey = 0) {
+		const key = await parseValue(data);
+		const value = await parseValue(data);
+		data.setObject([key, value], (key, value) => map.set(key, value));
+		if (indexKey < size - 1) {
+			await parseNextEntry(indexKey + 1);
+		}
+	}
+}
+
+async function parseSet(data) {
+	const size = await parseValue(data);
+	const set = new Set();
+	if (size) {
+		await parseNextEntry();
+	}
+	return set;
+
+	async function parseNextEntry(indexKey = 0) {
+		const value = await parseValue(data);
+		data.setObject([value], value => set.add(value));
+		if (indexKey < size - 1) {
+			await parseNextEntry(indexKey + 1);
+		}
+	}
+}
+
+async function parseDate(data) {
+	const milliseconds = await parseNumber(data);
+	return new Date(milliseconds);
+}
+
+async function parseError(data) {
+	const message = await parseString(data);
+	const stack = await parseString(data);
+	const error = new Error(message);
+	error.stack = stack;
+	return error;
+}
+
+async function parseRegExp(data) {
+	const source = await parseString(data);
+	const flags = await parseString(data);
+	return new RegExp(source, flags);
+}
+
+async function parseStringObject(data) {
+	return new String(await parseString(data));
+}
+
+async function parseNumberObject(data) {
+	return new Number(await parseNumber(data));
+}
+
+async function parseBooleanObject(data) {
+	return new Boolean(await parseBoolean(data));
+}
+
+async function parseSymbol(data) {
+	const description = await parseString(data);
+	return Symbol(description);
+}
+
+function testCircularReference(value, data) {
+	return testObject(value) && data.objects.includes(value);
+}
+
+function testReference(value) {
+	return value instanceof Reference;
+}
+
+function testObject(value) {
+	return value === Object(value);
+}
+
+function testArray(value) {
+	return typeof value.length == "number";
+}
+
+function testEmptySlot(value) {
+	return value === EMPTY_SLOT_VALUE;
+}
+
+function testString(value) {
+	return typeof value == "string";
+}
+
+function testFloat64Array(value) {
+	return value instanceof Float64Array;
+}
+
+function testUint32Array(value) {
+	return value instanceof Uint32Array;
+}
+
+function testInt32Array(value) {
+	return value instanceof Int32Array;
+}
+
+function testUint16Array(value) {
+	return value instanceof Uint16Array;
+}
+
+function testFloat32Array(value) {
+	return value instanceof Float32Array;
+}
+
+function testInt16Array(value) {
+	return value instanceof Int16Array;
+}
+
+function testUint8ClampedArray(value) {
+	return value instanceof Uint8ClampedArray;
+}
+
+function testUint8Array(value) {
+	return value instanceof Uint8Array;
+}
+
+function testInt8Array(value) {
+	return value instanceof Int8Array;
+}
+
+function testArrayBuffer(value) {
+	return value instanceof ArrayBuffer;
+}
+
+function testNumber(value) {
+	return typeof value == "number";
+}
+
+function testBigInt(value) {
+	return typeof value == "bigint";
+}
+
+function testUint32(value) {
+	return testInteger(value) && value >= 0 && value <= 4294967295;
+}
+
+function testInt32(value) {
+	return testInteger(value) && value >= -2147483648 && value <= 2147483647;
+}
+
+function testUint16(value) {
+	return testInteger(value) && value >= 0 && value <= 65535;
+}
+
+function testInt16(value) {
+	return testInteger(value) && value >= -32768 && value <= 32767;
+}
+
+function testUint8(value) {
+	return testInteger(value) && value >= 0 && value <= 255;
+}
+
+function testInt8(value) {
+	return testInteger(value) && value >= -128 && value <= 127;
+}
+
+function testInteger(value) {
+	return testNumber(value) && Number.isInteger(value);
+}
+
+function testUndefined(value) {
+	return value === undefined;
+}
+
+function testNull(value) {
+	return value === null;
+}
+
+function testNaN(value) {
+	return Number.isNaN(value);
+}
+
+function testBoolean(value) {
+	return typeof value == "boolean";
+}
+
+function testMap(value) {
+	return value instanceof Map;
+}
+
+function testSet(value) {
+	return value instanceof Set;
+}
+
+function testDate(value) {
+	return value instanceof Date;
+}
+
+function testError(value) {
+	return value instanceof Error;
+}
+
+function testRegExp(value) {
+	return value instanceof RegExp;
+}
+
+function testStringObject(value) {
+	return value instanceof String;
+}
+
+function testNumberObject(value) {
+	return value instanceof Number;
+}
+
+function testBooleanObject(value) {
+	return value instanceof Boolean;
+}
+
+function testSymbol(value) {
+	return typeof value == "symbol";
+}
+
+function testReferenceable(value) {
+	return testObject(value) || testSymbol(value);
+}

+ 140 - 9
src/ui/bg/ui-editor.js

@@ -21,10 +21,12 @@
  *   Source.
  *   Source.
  */
  */
 
 
-/* global browser, document, matchMedia, addEventListener, navigator, setInterval */
+/* global browser, document, matchMedia, addEventListener, navigator, prompt, URL, MouseEvent, Blob, setInterval */
 
 
 import * as download from "../../core/common/download.js";
 import * as download from "../../core/common/download.js";
 import { onError } from "./../common/content-error.js";
 import { onError } from "./../common/content-error.js";
+import * as zip from "./../../../lib/single-file-zip.js";
+import * as yabson from "./../../lib/yabson/yabson.js";
 
 
 const FOREGROUND_SAVE = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent) && !/Vivaldi/.test(navigator.userAgent) && !/OPR/.test(navigator.userAgent);
 const FOREGROUND_SAVE = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent) && !/Vivaldi/.test(navigator.userAgent) && !/OPR/.test(navigator.userAgent);
 
 
@@ -53,7 +55,7 @@ const savePageButton = document.querySelector(".save-page-button");
 const printPageButton = document.querySelector(".print-page-button");
 const printPageButton = document.querySelector(".print-page-button");
 const lastButton = toolbarElement.querySelector(".buttons:last-of-type [type=button]:last-of-type");
 const lastButton = toolbarElement.querySelector(".buttons:last-of-type [type=button]:last-of-type");
 
 
-let tabData, tabDataContents = [];
+let tabData, tabDataContents = [], downloadParser;
 
 
 addYellowNoteButton.title = browser.i18n.getMessage("editorAddYellowNote");
 addYellowNoteButton.title = browser.i18n.getMessage("editorAddYellowNote");
 addPinkNoteButton.title = browser.i18n.getMessage("editorAddPinkNote");
 addPinkNoteButton.title = browser.i18n.getMessage("editorAddPinkNote");
@@ -266,13 +268,37 @@ addEventListener("resize", viewportSizeChange);
 addEventListener("message", event => {
 addEventListener("message", event => {
 	const message = JSON.parse(event.data);
 	const message = JSON.parse(event.data);
 	if (message.method == "setContent") {
 	if (message.method == "setContent") {
-		const pageData = {
-			content: message.content,
-			filename: tabData.filename
-		};
 		tabData.options.openEditor = false;
 		tabData.options.openEditor = false;
 		tabData.options.openSavedPage = false;
 		tabData.options.openSavedPage = false;
-		download.downloadPage(pageData, tabData.options);
+		if (message.compressContent) {	
+			tabData.options.compressContent = true;
+			if (tabData.selfExtractingArchive !== undefined) {
+				tabData.options.selfExtractingArchive = tabData.selfExtractingArchive;
+			}
+			if (tabData.extractDataFromPageTags !== undefined) {
+				tabData.options.extractDataFromPage = tabData.extractDataFromPageTags;
+			}
+			if (tabData.options.insertTextBody !== undefined) {
+				tabData.options.insertTextBody = tabData.insertTextBody;
+			}
+			getContentPageData(tabData.content, message.content, { password: tabData.options.password })
+				.then(pageData => {
+					pageData.content = message.content;
+					pageData.title = message.title;
+					pageData.doctype = message.doctype;
+					pageData.viewport = message.viewport;
+					pageData.url = message.url;
+					pageData.filename = tabData.filename;
+					download.downloadPage(pageData, tabData.options);
+				});
+		} else {
+			const pageData = {
+				content: message.content,
+				filename: tabData.filename
+			};
+			tabData.options.compressContent = false;
+			download.downloadPage(pageData, tabData.options);
+		}
 	}
 	}
 	if (message.method == "onUpdate") {
 	if (message.method == "onUpdate") {
 		tabData.docSaved = message.saved;
 		tabData.docSaved = message.saved;
@@ -334,10 +360,9 @@ browser.runtime.onMessage.addListener(message => {
 			tabData = JSON.parse(tabDataContents.join(""));
 			tabData = JSON.parse(tabDataContents.join(""));
 			tabData.options = message.options;
 			tabData.options = message.options;
 			tabDataContents = [];
 			tabDataContents = [];
-			editorElement.contentWindow.postMessage(JSON.stringify({ method: "init", content: tabData.content }), "*");
+			editorElement.contentWindow.postMessage(JSON.stringify({ method: "init", content: tabData.content, password: tabData.options.password, compressContent: message.compressContent }), "*");
 			editorElement.contentWindow.focus();
 			editorElement.contentWindow.focus();
 			setInterval(() => browser.runtime.sendMessage({ method: "editor.ping" }), 15000);
 			setInterval(() => browser.runtime.sendMessage({ method: "editor.ping" }), 15000);
-			delete tabData.content;
 		}
 		}
 		return Promise.resolve({});
 		return Promise.resolve({});
 	}
 	}
@@ -347,6 +372,9 @@ browser.runtime.onMessage.addListener(message => {
 	if (message.method == "content.error") {
 	if (message.method == "content.error") {
 		onError(message.error, message.link);
 		onError(message.error, message.link);
 	}
 	}
+	if (message.method == "content.download") {
+		return downloadContent(message);
+	}
 });
 });
 
 
 addEventListener("load", () => {
 addEventListener("load", () => {
@@ -360,6 +388,24 @@ addEventListener("beforeunload", event => {
 	}
 	}
 });
 });
 
 
+async function downloadContent(message) {
+	if (!downloadParser) {
+		downloadParser = yabson.getParser();
+	}
+	const result = await downloadParser.next(message.data);
+	if (result.done) {
+		downloadParser = null;
+		const link = document.createElement("a");
+		link.download = result.value.filename;
+		link.href = URL.createObjectURL(new Blob([result.value.content]), "text/html");
+		link.dispatchEvent(new MouseEvent("click"));
+		URL.revokeObjectURL(link.href);
+		return browser.runtime.sendMessage({ method: "downloads.end", taskId: result.value.taskId }).then(() => ({}));
+	} else {
+		return Promise.resolve({});
+	}
+}
+
 async function refreshOptions(profileName) {
 async function refreshOptions(profileName) {
 	const profiles = await browser.runtime.sendMessage({ method: "config.getProfiles" });
 	const profiles = await browser.runtime.sendMessage({ method: "config.getProfiles" });
 	tabData.options = profiles[profileName];
 	tabData.options = profiles[profileName];
@@ -460,4 +506,89 @@ function getPosition(event) {
 	} else {
 	} else {
 		return event;
 		return event;
 	}
 	}
+}
+
+async function getContentPageData(zipContent, page, options) {
+	zip.configure({ workerScripts: { inflate: ["/lib/single-file-z-worker.js"] } });
+	const zipReader = new zip.ZipReader(new zip.Uint8ArrayReader(new Uint8Array(zipContent)));
+	const entries = await zipReader.getEntries();
+	const resources = [];
+	await Promise.all(entries.map(async entry => {
+		let data;
+		if (!options.password && entry.bitFlag.encrypted) {
+			options.password = prompt("Please enter the password to view the page");
+		}
+		if (entry.filename.match(/^([0-9_]+\/)?index.html$/)) {
+			data = page;
+		} else {
+			if (entry.filename.endsWith(".html")) {
+				data = await entry.getData(new zip.TextWriter(), options);
+			} else {
+				data = await entry.getData(new zip.Uint8ArrayWriter(), options);
+			}
+		}
+		const extensionMatch = entry.filename.match(/\.([^.]+)/);
+		resources.push({
+			filename: entry.filename.match(/^([0-9_]+\/)?(.*)$/)[2],
+			extension: extensionMatch && extensionMatch[1],
+			content: data,
+			url: entry.comment
+		});
+	}));
+	return getPageData(resources);
+}
+
+function getPageData(resources) {
+	const pageData = JSON.parse(JSON.stringify(EMPTY_PAGE_DATA));
+	for (const resource of resources) {
+		const resourcePageData = getPageDataResource(resource, "", pageData);
+		const filename = resource.filename.substring(resourcePageData.prefixPath.length);
+		resource.name = filename;
+		if (filename.startsWith("images/")) {
+			resourcePageData.resources.images.push(resource);
+		}
+		if (filename.startsWith("fonts/")) {
+			resourcePageData.resources.fonts.push(resource);
+		}
+		if (filename.startsWith("scripts/")) {
+			resourcePageData.resources.scripts.push(resource);
+		}
+		if (filename.endsWith(".css")) {
+			resourcePageData.resources.stylesheets.push(resource);
+		}
+		if (filename.endsWith(".html")) {
+			resourcePageData.content = resource.content;
+		}
+	}
+	return pageData;
+}
+
+const EMPTY_PAGE_DATA = {
+	name: "",
+	prefixPath: "",
+	resources: {
+		stylesheets: [],
+		images: [],
+		fonts: [],
+		scripts: [],
+		frames: []
+	}
+};
+
+function getPageDataResource(resource, prefixPath = "", pageData) {
+	const filename = resource.filename.substring(prefixPath.length);
+	resource.name = filename;
+	if (filename.startsWith("frames/")) {
+		const framesIndex = Number(filename.match(/^frames\/(\d+)\//)[1]);
+		const framePath = "frames/" + framesIndex + "/";
+		if (!pageData.resources.frames[framesIndex]) {
+			pageData.resources.frames[framesIndex] = Object.assign(JSON.parse(JSON.stringify(EMPTY_PAGE_DATA)), {
+				name: framePath,
+				prefixPath: prefixPath + framePath
+			});
+		}
+		return getPageDataResource(resource, prefixPath + framePath, pageData.resources.frames[framesIndex]);
+	} else {
+		return pageData;
+	}
 }
 }

+ 40 - 4
src/ui/bg/ui-options.js

@@ -91,6 +91,7 @@ const githubRepositoryLabel = document.getElementById("githubRepositoryLabel");
 const githubBranchLabel = document.getElementById("githubBranchLabel");
 const githubBranchLabel = document.getElementById("githubBranchLabel");
 const saveWithCompanionLabel = document.getElementById("saveWithCompanionLabel");
 const saveWithCompanionLabel = document.getElementById("saveWithCompanionLabel");
 const compressHTMLLabel = document.getElementById("compressHTMLLabel");
 const compressHTMLLabel = document.getElementById("compressHTMLLabel");
+const insertTextBodyLabel = document.getElementById("insertTextBodyLabel");
 const compressCSSLabel = document.getElementById("compressCSSLabel");
 const compressCSSLabel = document.getElementById("compressCSSLabel");
 const moveStylesInHeadLabel = document.getElementById("moveStylesInHeadLabel");
 const moveStylesInHeadLabel = document.getElementById("moveStylesInHeadLabel");
 const loadDeferredImagesLabel = document.getElementById("loadDeferredImagesLabel");
 const loadDeferredImagesLabel = document.getElementById("loadDeferredImagesLabel");
@@ -136,12 +137,20 @@ const passReferrerOnErrorLabel = document.getElementById("passReferrerOnErrorLab
 const replaceBookmarkURLLabel = document.getElementById("replaceBookmarkURLLabel");
 const replaceBookmarkURLLabel = document.getElementById("replaceBookmarkURLLabel");
 const allowedBookmarkFoldersLabel = document.getElementById("allowedBookmarkFoldersLabel");
 const allowedBookmarkFoldersLabel = document.getElementById("allowedBookmarkFoldersLabel");
 const ignoredBookmarkFoldersLabel = document.getElementById("ignoredBookmarkFoldersLabel");
 const ignoredBookmarkFoldersLabel = document.getElementById("ignoredBookmarkFoldersLabel");
+const createRootDirectoryLabel = document.getElementById("createRootDirectoryLabel");
+const passwordLabel = document.getElementById("passwordLabel");
 const titleLabel = document.getElementById("titleLabel");
 const titleLabel = document.getElementById("titleLabel");
 const userInterfaceLabel = document.getElementById("userInterfaceLabel");
 const userInterfaceLabel = document.getElementById("userInterfaceLabel");
 const filenameLabel = document.getElementById("filenameLabel");
 const filenameLabel = document.getElementById("filenameLabel");
 const htmlContentLabel = document.getElementById("htmlContentLabel");
 const htmlContentLabel = document.getElementById("htmlContentLabel");
-const imagesLabel = document.getElementById("imagesLabel");
+const fileFormatLabel = document.getElementById("fileFormatLabel");
+const fileFormatSelectHTMLLabel = document.getElementById("fileFormatSelectHTMLLabel");
+const fileFormatSelectSelfExtractingUniversalLabel = document.getElementById("fileFormatSelectSelfExtractingUniversalLabel");
+const fileFormatSelectSelfExtractingLabel = document.getElementById("fileFormatSelectSelfExtractingLabel");
+const fileFormatSelectZIPLabel = document.getElementById("fileFormatSelectZIPLabel");
+const fileFormatSelectLabel = document.getElementById("fileFormatSelectLabel");
 const infobarLabel = document.getElementById("infobarLabel");
 const infobarLabel = document.getElementById("infobarLabel");
+const imagesLabel = document.getElementById("imagesLabel");
 const stylesheetsLabel = document.getElementById("stylesheetsLabel");
 const stylesheetsLabel = document.getElementById("stylesheetsLabel");
 const fontsLabel = document.getElementById("fontsLabel");
 const fontsLabel = document.getElementById("fontsLabel");
 const networkLabel = document.getElementById("networkLabel");
 const networkLabel = document.getElementById("networkLabel");
@@ -166,11 +175,11 @@ const autoOpenEditorLabel = document.getElementById("autoOpenEditorLabel");
 const defaultEditorModeLabel = document.getElementById("defaultEditorModeLabel");
 const defaultEditorModeLabel = document.getElementById("defaultEditorModeLabel");
 const applySystemThemeLabel = document.getElementById("applySystemThemeLabel");
 const applySystemThemeLabel = document.getElementById("applySystemThemeLabel");
 const warnUnsavedPageLabel = document.getElementById("warnUnsavedPageLabel");
 const warnUnsavedPageLabel = document.getElementById("warnUnsavedPageLabel");
+const displayInfobarInEditorLabel = document.getElementById("displayInfobarInEditorLabel");
 const infobarTemplateLabel = document.getElementById("infobarTemplateLabel");
 const infobarTemplateLabel = document.getElementById("infobarTemplateLabel");
 const blockMixedContentLabel = document.getElementById("blockMixedContentLabel");
 const blockMixedContentLabel = document.getElementById("blockMixedContentLabel");
 const saveOriginalURLsLabel = document.getElementById("saveOriginalURLsLabel");
 const saveOriginalURLsLabel = document.getElementById("saveOriginalURLsLabel");
 const includeInfobarLabel = document.getElementById("includeInfobarLabel");
 const includeInfobarLabel = document.getElementById("includeInfobarLabel");
-const displayInfobarInEditorLabel = document.getElementById("displayInfobarInEditorLabel");
 const removeInfobarSavedDateLabel = document.getElementById("removeInfobarSavedDateLabel");
 const removeInfobarSavedDateLabel = document.getElementById("removeInfobarSavedDateLabel");
 const miscLabel = document.getElementById("miscLabel");
 const miscLabel = document.getElementById("miscLabel");
 const helpLabel = document.getElementById("helpLabel");
 const helpLabel = document.getElementById("helpLabel");
@@ -218,6 +227,7 @@ const githubBranchInput = document.getElementById("githubBranchInput");
 const saveWithCompanionInput = document.getElementById("saveWithCompanionInput");
 const saveWithCompanionInput = document.getElementById("saveWithCompanionInput");
 const saveToFilesystemInput = document.getElementById("saveToFilesystemInput");
 const saveToFilesystemInput = document.getElementById("saveToFilesystemInput");
 const compressHTMLInput = document.getElementById("compressHTMLInput");
 const compressHTMLInput = document.getElementById("compressHTMLInput");
+const insertTextBodyInput = document.getElementById("insertTextBodyInput");
 const compressCSSInput = document.getElementById("compressCSSInput");
 const compressCSSInput = document.getElementById("compressCSSInput");
 const moveStylesInHeadInput = document.getElementById("moveStylesInHeadInput");
 const moveStylesInHeadInput = document.getElementById("moveStylesInHeadInput");
 const loadDeferredImagesInput = document.getElementById("loadDeferredImagesInput");
 const loadDeferredImagesInput = document.getElementById("loadDeferredImagesInput");
@@ -258,6 +268,9 @@ const passReferrerOnErrorInput = document.getElementById("passReferrerOnErrorInp
 const replaceBookmarkURLInput = document.getElementById("replaceBookmarkURLInput");
 const replaceBookmarkURLInput = document.getElementById("replaceBookmarkURLInput");
 const allowedBookmarkFoldersInput = document.getElementById("allowedBookmarkFoldersInput");
 const allowedBookmarkFoldersInput = document.getElementById("allowedBookmarkFoldersInput");
 const ignoredBookmarkFoldersInput = document.getElementById("ignoredBookmarkFoldersInput");
 const ignoredBookmarkFoldersInput = document.getElementById("ignoredBookmarkFoldersInput");
+const fileFormatSelectInput = document.getElementById("fileFormatSelectInput");
+const createRootDirectoryInput = document.getElementById("createRootDirectoryInput");
+const passwordInput = document.getElementById("passwordInput");
 const groupDuplicateImagesInput = document.getElementById("groupDuplicateImagesInput");
 const groupDuplicateImagesInput = document.getElementById("groupDuplicateImagesInput");
 const infobarTemplateInput = document.getElementById("infobarTemplateInput");
 const infobarTemplateInput = document.getElementById("infobarTemplateInput");
 const blockMixedContentInput = document.getElementById("blockMixedContentInput");
 const blockMixedContentInput = document.getElementById("blockMixedContentInput");
@@ -596,6 +609,7 @@ githubRepositoryLabel.textContent = browser.i18n.getMessage("optionGitHubReposit
 githubBranchLabel.textContent = browser.i18n.getMessage("optionGitHubBranch");
 githubBranchLabel.textContent = browser.i18n.getMessage("optionGitHubBranch");
 saveWithCompanionLabel.textContent = browser.i18n.getMessage("optionSaveWithCompanion");
 saveWithCompanionLabel.textContent = browser.i18n.getMessage("optionSaveWithCompanion");
 compressHTMLLabel.textContent = browser.i18n.getMessage("optionCompressHTML");
 compressHTMLLabel.textContent = browser.i18n.getMessage("optionCompressHTML");
+insertTextBodyLabel.textContent = browser.i18n.getMessage("optionInsertTextBody");
 compressCSSLabel.textContent = browser.i18n.getMessage("optionCompressCSS");
 compressCSSLabel.textContent = browser.i18n.getMessage("optionCompressCSS");
 moveStylesInHeadLabel.textContent = browser.i18n.getMessage("optionMoveStylesInHead");
 moveStylesInHeadLabel.textContent = browser.i18n.getMessage("optionMoveStylesInHead");
 loadDeferredImagesLabel.textContent = browser.i18n.getMessage("optionLoadDeferredImages");
 loadDeferredImagesLabel.textContent = browser.i18n.getMessage("optionLoadDeferredImages");
@@ -641,11 +655,19 @@ passReferrerOnErrorLabel.textContent = browser.i18n.getMessage("optionPassReferr
 replaceBookmarkURLLabel.textContent = browser.i18n.getMessage("optionReplaceBookmarkURL");
 replaceBookmarkURLLabel.textContent = browser.i18n.getMessage("optionReplaceBookmarkURL");
 allowedBookmarkFoldersLabel.textContent = browser.i18n.getMessage("optionAllowedBookmarkFolders");
 allowedBookmarkFoldersLabel.textContent = browser.i18n.getMessage("optionAllowedBookmarkFolders");
 ignoredBookmarkFoldersLabel.textContent = browser.i18n.getMessage("optionIgnoredBookmarkFolders");
 ignoredBookmarkFoldersLabel.textContent = browser.i18n.getMessage("optionIgnoredBookmarkFolders");
+createRootDirectoryLabel.textContent = browser.i18n.getMessage("optionCreateRootDirectory");
+passwordLabel.textContent = browser.i18n.getMessage("optionPassword");
 groupDuplicateImagesLabel.textContent = browser.i18n.getMessage("optionGroupDuplicateImages");
 groupDuplicateImagesLabel.textContent = browser.i18n.getMessage("optionGroupDuplicateImages");
 titleLabel.textContent = browser.i18n.getMessage("optionsTitle");
 titleLabel.textContent = browser.i18n.getMessage("optionsTitle");
 userInterfaceLabel.textContent = browser.i18n.getMessage("optionsUserInterfaceSubTitle");
 userInterfaceLabel.textContent = browser.i18n.getMessage("optionsUserInterfaceSubTitle");
 filenameLabel.textContent = browser.i18n.getMessage("optionsFileNameSubTitle");
 filenameLabel.textContent = browser.i18n.getMessage("optionsFileNameSubTitle");
 htmlContentLabel.textContent = browser.i18n.getMessage("optionsHTMLContentSubTitle");
 htmlContentLabel.textContent = browser.i18n.getMessage("optionsHTMLContentSubTitle");
+fileFormatLabel.textContent = browser.i18n.getMessage("optionsFileFormatSubTitle");
+fileFormatSelectHTMLLabel.textContent = browser.i18n.getMessage("optionFileFormatSelectHTML");
+fileFormatSelectSelfExtractingUniversalLabel.textContent = browser.i18n.getMessage("optionFileFormatSelectSelfExtractingUniversal");
+fileFormatSelectSelfExtractingLabel.textContent = browser.i18n.getMessage("optionFileFormatSelectSelfExtracting");
+fileFormatSelectZIPLabel.textContent = browser.i18n.getMessage("optionFileFormatSelectZIP");
+fileFormatSelectLabel.textContent = browser.i18n.getMessage("optionFileFormat");
 infobarLabel.textContent = browser.i18n.getMessage("optionsInfobarSubTitle");
 infobarLabel.textContent = browser.i18n.getMessage("optionsInfobarSubTitle");
 imagesLabel.textContent = browser.i18n.getMessage("optionsImagesSubTitle");
 imagesLabel.textContent = browser.i18n.getMessage("optionsImagesSubTitle");
 stylesheetsLabel.textContent = browser.i18n.getMessage("optionsStylesheetsSubTitle");
 stylesheetsLabel.textContent = browser.i18n.getMessage("optionsStylesheetsSubTitle");
@@ -931,10 +953,18 @@ async function refresh(profileName) {
 	passReferrerOnErrorInput.checked = profileOptions.passReferrerOnError;
 	passReferrerOnErrorInput.checked = profileOptions.passReferrerOnError;
 	replaceBookmarkURLInput.checked = profileOptions.replaceBookmarkURL;
 	replaceBookmarkURLInput.checked = profileOptions.replaceBookmarkURL;
 	replaceBookmarkURLInput.disabled = !profileOptions.saveCreatedBookmarks;
 	replaceBookmarkURLInput.disabled = !profileOptions.saveCreatedBookmarks;
-	allowedBookmarkFoldersInput.value = profileOptions.allowedBookmarkFolders.map(folder => folder.replace(/,/g, "\\,")).join(","); // eslint-disable-line no-useless-escape
+	allowedBookmarkFoldersInput.value = profileOptions.allowedBookmarkFolders.map(folder => folder.replace(/,/g, "\\,")).join(",");
 	allowedBookmarkFoldersInput.disabled = !profileOptions.saveCreatedBookmarks;
 	allowedBookmarkFoldersInput.disabled = !profileOptions.saveCreatedBookmarks;
-	ignoredBookmarkFoldersInput.value = profileOptions.ignoredBookmarkFolders.map(folder => folder.replace(/,/g, "\\,")).join(","); // eslint-disable-line no-useless-escape
+	ignoredBookmarkFoldersInput.value = profileOptions.ignoredBookmarkFolders.map(folder => folder.replace(/,/g, "\\,")).join(",");
 	ignoredBookmarkFoldersInput.disabled = !profileOptions.saveCreatedBookmarks;
 	ignoredBookmarkFoldersInput.disabled = !profileOptions.saveCreatedBookmarks;
+	fileFormatSelectInput.value = profileOptions.compressContent ? profileOptions.selfExtractingArchive ? profileOptions.extractDataFromPage ?
+		"self-extracting-zip-universal" : "self-extracting-zip" : "zip" : "html";
+	createRootDirectoryInput.checked = profileOptions.createRootDirectory;
+	createRootDirectoryInput.disabled = !profileOptions.compressContent;
+	passwordInput.value = profileOptions.password;
+	passwordInput.disabled = !profileOptions.compressContent;
+	insertTextBodyInput.checked = profileOptions.insertTextBody;
+	insertTextBodyInput.disabled = !profileOptions.compressContent || (!profileOptions.selfExtractingArchive && !profileOptions.extractDataFromPage);
 	infobarTemplateInput.value = profileOptions.infobarTemplate;
 	infobarTemplateInput.value = profileOptions.infobarTemplate;
 	blockMixedContentInput.checked = profileOptions.blockMixedContent;
 	blockMixedContentInput.checked = profileOptions.blockMixedContent;
 	saveOriginalURLsInput.checked = profileOptions.saveOriginalURLs;
 	saveOriginalURLsInput.checked = profileOptions.saveOriginalURLs;
@@ -1001,6 +1031,7 @@ async function update() {
 			githubBranch: githubBranchInput.value,
 			githubBranch: githubBranchInput.value,
 			saveWithCompanion: saveWithCompanionInput.checked,
 			saveWithCompanion: saveWithCompanionInput.checked,
 			compressHTML: compressHTMLInput.checked,
 			compressHTML: compressHTMLInput.checked,
+			insertTextBody: insertTextBodyInput.checked,
 			compressCSS: compressCSSInput.checked,
 			compressCSS: compressCSSInput.checked,
 			moveStylesInHead: moveStylesInHeadInput.checked,
 			moveStylesInHead: moveStylesInHeadInput.checked,
 			loadDeferredImages: loadDeferredImagesInput.checked,
 			loadDeferredImages: loadDeferredImagesInput.checked,
@@ -1040,6 +1071,11 @@ async function update() {
 			replaceBookmarkURL: replaceBookmarkURLInput.checked,
 			replaceBookmarkURL: replaceBookmarkURLInput.checked,
 			allowedBookmarkFolders: allowedBookmarkFoldersInput.value.replace(/([^\\]),/g, "$1 ,").split(/[^\\],/).map(folder => folder.replace(/\\,/g, ",")),
 			allowedBookmarkFolders: allowedBookmarkFoldersInput.value.replace(/([^\\]),/g, "$1 ,").split(/[^\\],/).map(folder => folder.replace(/\\,/g, ",")),
 			ignoredBookmarkFolders: ignoredBookmarkFoldersInput.value.replace(/([^\\]),/g, "$1 ,").split(/[^\\],/).map(folder => folder.replace(/\\,/g, ",")),
 			ignoredBookmarkFolders: ignoredBookmarkFoldersInput.value.replace(/([^\\]),/g, "$1 ,").split(/[^\\],/).map(folder => folder.replace(/\\,/g, ",")),
+			compressContent: fileFormatSelectInput.value.includes("zip"),
+			createRootDirectory: createRootDirectoryInput.checked,
+			selfExtractingArchive: fileFormatSelectInput.value.includes("self-extracting"),
+			extractDataFromPage: fileFormatSelectInput.value == "self-extracting-zip-universal",
+			password: passwordInput.value,
 			groupDuplicateImages: groupDuplicateImagesInput.checked,
 			groupDuplicateImages: groupDuplicateImagesInput.checked,
 			infobarTemplate: infobarTemplateInput.value,
 			infobarTemplate: infobarTemplateInput.value,
 			blockMixedContent: blockMixedContentInput.checked,
 			blockMixedContent: blockMixedContentInput.checked,

+ 229 - 65
src/ui/content/content-ui-editor-web.js

@@ -21,7 +21,11 @@
  *   Source.
  *   Source.
  */
  */
 
 
-/* global globalThis, window, document, fetch, DOMParser, getComputedStyle, setTimeout, clearTimeout, NodeFilter, Readability, isProbablyReaderable, matchMedia, TextDecoder, Node, URL, MouseEvent, Blob */
+/* global globalThis, window, document, fetch, DOMParser, getComputedStyle, setTimeout, clearTimeout, NodeFilter, Readability, isProbablyReaderable, matchMedia, TextDecoder, Node, URL, MouseEvent, Blob, prompt, MutationObserver, FileReader, Worker */
+
+import * as zip from "single-file-core/vendor/zip/zip.js";
+import { extract } from "single-file-core/processors/compression/compression-extract.js";
+import { display } from "single-file-core/processors/compression/compression-display.js";
 
 
 (globalThis => {
 (globalThis => {
 
 
@@ -974,8 +978,9 @@ pre code {
 
 
 	let NOTES_WEB_STYLESHEET, MASK_WEB_STYLESHEET, HIGHLIGHTS_WEB_STYLESHEET;
 	let NOTES_WEB_STYLESHEET, MASK_WEB_STYLESHEET, HIGHLIGHTS_WEB_STYLESHEET;
 	let selectedNote, anchorElement, maskNoteElement, maskPageElement, highlightSelectionMode, removeHighlightMode, resizingNoteMode, movingNoteMode, highlightColor, collapseNoteTimeout, cuttingOuterMode, cuttingMode, cuttingTouchTarget, cuttingPath, cuttingPathIndex, previousContent;
 	let selectedNote, anchorElement, maskNoteElement, maskPageElement, highlightSelectionMode, removeHighlightMode, resizingNoteMode, movingNoteMode, highlightColor, collapseNoteTimeout, cuttingOuterMode, cuttingMode, cuttingTouchTarget, cuttingPath, cuttingPathIndex, previousContent;
-	let removedElements = [], removedElementIndex = 0, initScriptContent, includeInfobar;
+	let removedElements = [], removedElementIndex = 0, initScriptContent, pageResources, pageUrl, pageCompressContent, includeInfobar;
 
 
+	globalThis.zip = zip;
 	window.onmessage = async event => {
 	window.onmessage = async event => {
 		const message = JSON.parse(event.data);
 		const message = JSON.parse(event.data);
 		if (message.method == "init") {
 		if (message.method == "init") {
@@ -1069,16 +1074,33 @@ pre code {
 			if (initScriptContent) {
 			if (initScriptContent) {
 				content = content.replace(/<script data-template-shadow-root src.*?<\/script>/g, initScriptContent);
 				content = content.replace(/<script data-template-shadow-root src.*?<\/script>/g, initScriptContent);
 			}
 			}
-			if (message.foregroundSave) {
-				if (message.filename && message.filename.length) {
-					const link = document.createElement("a");
-					link.download = message.filename;
-					link.href = URL.createObjectURL(new Blob([content], { type: "text/html" }));
-					link.dispatchEvent(new MouseEvent("click"));
-				}
-				return new Promise(resolve => setTimeout(resolve, 1));
+			debugger;
+			if (pageCompressContent) {
+				const viewport = document.head.querySelector("meta[name=viewport]");
+				window.parent.postMessage(JSON.stringify({
+					method: "setContent",
+					content,
+					title: document.title,
+					doctype: singlefile.helper.getDoctypeString(document),
+					url: pageUrl,
+					viewport: viewport ? viewport.content : null,
+					compressContent: true
+				}), "*");
 			} else {
 			} else {
-				window.parent.postMessage(JSON.stringify({ method: "setContent", content }), "*");
+				if (message.foregroundSave) {
+					if (message.filename && message.filename.length) {
+						const link = document.createElement("a");
+						link.download = message.filename;
+						link.href = URL.createObjectURL(new Blob([content], { type: "text/html" }));
+						link.dispatchEvent(new MouseEvent("click"));
+					}
+					return new Promise(resolve => setTimeout(resolve, 1));
+				} else {
+					window.parent.postMessage(JSON.stringify({
+						method: "setContent",
+						content
+					}), "*");
+				}
 			}
 			}
 		}
 		}
 		if (message.method == "printPage") {
 		if (message.method == "printPage") {
@@ -1095,67 +1117,150 @@ pre code {
 			const file = event.dataTransfer.files[0];
 			const file = event.dataTransfer.files[0];
 			event.preventDefault();
 			event.preventDefault();
 			const content = new TextDecoder().decode(await file.arrayBuffer());
 			const content = new TextDecoder().decode(await file.arrayBuffer());
-			await init({ content }, { filename: file.name });
+			const compressContent = /<html[^>]* data-sfz[^>]*>/i.test(content);
+			if (compressContent) {
+				await init({ content: file, compressContent }, { filename: file.name });
+			} else {
+				await init({ content }, { filename: file.name });
+			}
 		}
 		}
 	};
 	};
 
 
-	async function init({ content }, { filename, reset } = {}) {
+	async function init({ content, password, compressContent }, { filename, reset } = {}) {
 		await initConstants();
 		await initConstants();
-		const initScriptContentMatch = content.match(/<script data-template-shadow-root.*<\/script>/);
-		if (initScriptContentMatch && initScriptContentMatch[0]) {
-			initScriptContent = initScriptContentMatch[0];
-		}
-		content = content.replace(/<script data-template-shadow-root.*<\/script>/g, "<script data-template-shadow-root src=/lib/single-file-extension-editor-init.js></script>");
-		const contentDocument = (new DOMParser()).parseFromString(content, "text/html");
-		if (detectSavedPage(contentDocument)) {
-			if (contentDocument.doctype) {
-				if (document.doctype) {
-					document.replaceChild(contentDocument.doctype, document.doctype);
-				} else {
-					document.insertBefore(contentDocument.doctype, document.documentElement);
+		if (compressContent) {
+			const zipOptions = {
+				workerScripts: { inflate: ["/lib/single-file-z-worker.js"] }
+			};
+			try {
+				const worker = new Worker(zipOptions.workerScripts);
+				worker.terminate();
+			} catch (error) {
+				delete zipOptions.workerScripts;
+			}
+			const { docContent, origDocContent, resources, url } = await extract(content, {
+				password,
+				prompt,
+				shadowRootScriptURL: new URL("/lib/single-file-extension-editor-init.js", document.baseURI).href,
+				zipOptions
+			});
+			pageResources = resources;
+			pageUrl = url;
+			pageCompressContent = true;
+			const contentDocument = (new DOMParser()).parseFromString(docContent, "text/html");
+			if (detectSavedPage(contentDocument)) {
+				await display(document, docContent, { disableFramePointerEvents: true });
+				const infobarElement = document.querySelector(singlefile.helper.INFOBAR_TAGNAME);
+				if (infobarElement) {
+					infobarElement.remove();
+				}
+				await initPage();
+				let icon;
+				const origContentDocument = (new DOMParser()).parseFromString(origDocContent, "text/html");
+				const iconElement = origContentDocument.querySelector("link[rel*=icon]");
+				if (iconElement) {
+					const iconResource = resources.find(resource => resource.filename == iconElement.getAttribute("href"));
+					if (iconResource && iconResource.blob) {
+						const reader = new FileReader();
+						reader.readAsDataURL(iconResource.blob);
+						icon = await new Promise((resolve, reject) => {
+							reader.addEventListener("load", () => resolve(reader.result), false);
+							reader.addEventListener("error", reject, false);
+						});
+					} else {
+						icon = iconElement.href;
+					}
 				}
 				}
-			} else if (document.doctype) {
-				document.doctype.remove();
+				window.parent.postMessage(JSON.stringify({
+					method: "onInit",
+					title: document.title,
+					icon,
+					filename,
+					reset,
+					formatPageEnabled: isProbablyReaderable(document)
+				}), "*");
 			}
 			}
-			const infobarElement = contentDocument.querySelector(singlefile.helper.INFOBAR_TAGNAME);
-			if (infobarElement) {
-				infobarElement.remove();
+		} else {
+			const initScriptContentMatch = content.match(/<script data-template-shadow-root.*<\/script>/);
+			if (initScriptContentMatch && initScriptContentMatch[0]) {
+				initScriptContent = initScriptContentMatch[0];
+			}
+			content = content.replace(/<script data-template-shadow-root.*<\/script>/g, "<script data-template-shadow-root src=/lib/single-file-extension-editor-init.js></script>");
+			const contentDocument = (new DOMParser()).parseFromString(content, "text/html");
+			if (detectSavedPage(contentDocument)) {
+				if (contentDocument.doctype) {
+					if (document.doctype) {
+						document.replaceChild(contentDocument.doctype, document.doctype);
+					} else {
+						document.insertBefore(contentDocument.doctype, document.documentElement);
+					}
+				} else if (document.doctype) {
+					document.doctype.remove();
+				}
+				const infobarElement = contentDocument.querySelector(singlefile.helper.INFOBAR_TAGNAME);
+				if (infobarElement) {
+					infobarElement.remove();
+				}
+				contentDocument.querySelectorAll("noscript").forEach(element => {
+					element.setAttribute(DISABLED_NOSCRIPT_ATTRIBUTE_NAME, element.innerHTML);
+					element.textContent = "";
+				});
+				contentDocument.querySelectorAll("iframe").forEach(element => {
+					const pointerEvents = "pointer-events";
+					element.style.setProperty("-sf-" + pointerEvents, element.style.getPropertyValue(pointerEvents), element.style.getPropertyPriority(pointerEvents));
+					element.style.setProperty(pointerEvents, "none", "important");
+				});
+				document.replaceChild(contentDocument.documentElement, document.documentElement);
+				document.querySelectorAll("[data-single-file-note-refs]").forEach(noteRefElement => noteRefElement.dataset.singleFileNoteRefs = noteRefElement.dataset.singleFileNoteRefs.replace(/,/g, " "));
+				deserializeShadowRoots(document);
+				document.querySelectorAll(NOTE_TAGNAME).forEach(containerElement => attachNoteListeners(containerElement, true));
+				document.documentElement.appendChild(getStyleElement(HIGHLIGHTS_WEB_STYLESHEET));
+				maskPageElement = getMaskElement(PAGE_MASK_CLASS, PAGE_MASK_CONTAINER_CLASS);
+				maskNoteElement = getMaskElement(NOTE_MASK_CLASS);
+				document.documentElement.onmousedown = onMouseDown;
+				document.documentElement.onmouseup = document.documentElement.ontouchend = onMouseUp;
+				document.documentElement.onmouseover = onMouseOver;
+				document.documentElement.onmouseout = onMouseOut;
+				document.documentElement.onkeydown = onKeyDown;
+				document.documentElement.ontouchstart = document.documentElement.ontouchmove = onTouchMove;
+				window.onclick = event => event.preventDefault();
+				const iconElement = document.querySelector("link[rel*=icon]");
+				window.parent.postMessage(JSON.stringify({
+					method: "onInit",
+					title: document.title,
+					icon: iconElement && iconElement.href,
+					filename,
+					reset,
+					formatPageEnabled: isProbablyReaderable(document)
+				}), "*");
 			}
 			}
-			contentDocument.querySelectorAll("noscript").forEach(element => {
-				element.setAttribute(DISABLED_NOSCRIPT_ATTRIBUTE_NAME, element.innerHTML);
-				element.textContent = "";
-			});
-			contentDocument.querySelectorAll("iframe").forEach(element => {
-				const pointerEvents = "pointer-events";
-				element.style.setProperty("-sf-" + pointerEvents, element.style.getPropertyValue(pointerEvents), element.style.getPropertyPriority(pointerEvents));
-				element.style.setProperty(pointerEvents, "none", "important");
-			});
-			document.replaceChild(contentDocument.documentElement, document.documentElement);
-			document.querySelectorAll("[data-single-file-note-refs]").forEach(noteRefElement => noteRefElement.dataset.singleFileNoteRefs = noteRefElement.dataset.singleFileNoteRefs.replace(/,/g, " "));
-			deserializeShadowRoots(document);
-			document.querySelectorAll(NOTE_TAGNAME).forEach(containerElement => attachNoteListeners(containerElement, true));
-			document.documentElement.appendChild(getStyleElement(HIGHLIGHTS_WEB_STYLESHEET));
-			maskPageElement = getMaskElement(PAGE_MASK_CLASS, PAGE_MASK_CONTAINER_CLASS);
-			maskNoteElement = getMaskElement(NOTE_MASK_CLASS);
-			document.documentElement.onmousedown = onMouseDown;
-			document.documentElement.onmouseup = document.documentElement.ontouchend = onMouseUp;
-			document.documentElement.onmouseover = onMouseOver;
-			document.documentElement.onmouseout = onMouseOut;
-			document.documentElement.onkeydown = onKeyDown;
-			document.documentElement.ontouchstart = document.documentElement.ontouchmove = onTouchMove;
-			window.onclick = event => event.preventDefault();
-			const iconElement = document.querySelector("link[rel*=icon]");
-			window.parent.postMessage(JSON.stringify({
-				method: "onInit",
-				title: document.title,
-				icon: iconElement && iconElement.href,
-				filename,
-				reset,
-				formatPageEnabled: isProbablyReaderable(document)
-			}), "*");
 		}
 		}
 	}
 	}
 
 
+	async function initPage() {
+		document.querySelectorAll("iframe").forEach(element => {
+			const pointerEvents = "pointer-events";
+			element.style.setProperty("-sf-" + pointerEvents, element.style.getPropertyValue(pointerEvents), element.style.getPropertyPriority(pointerEvents));
+			element.style.setProperty(pointerEvents, "none", "important");
+		});
+		document.querySelectorAll("[data-single-file-note-refs]").forEach(noteRefElement => noteRefElement.dataset.singleFileNoteRefs = noteRefElement.dataset.singleFileNoteRefs.replace(/,/g, " "));
+		deserializeShadowRoots(document);
+		reflowNotes();
+		await waitResourcesLoad();
+		reflowNotes();
+		document.querySelectorAll(NOTE_TAGNAME).forEach(containerElement => attachNoteListeners(containerElement, true));
+		document.documentElement.appendChild(getStyleElement(HIGHLIGHTS_WEB_STYLESHEET));
+		maskPageElement = getMaskElement(PAGE_MASK_CLASS, PAGE_MASK_CONTAINER_CLASS);
+		maskNoteElement = getMaskElement(NOTE_MASK_CLASS);
+		document.documentElement.onmousedown = onMouseDown;
+		document.documentElement.onmouseup = document.documentElement.ontouchend = onMouseUp;
+		document.documentElement.onmouseover = onMouseOver;
+		document.documentElement.onmouseout = onMouseOut;
+		document.documentElement.onkeydown = onKeyDown;
+		document.documentElement.ontouchstart = document.documentElement.ontouchmove = onTouchMove;
+		window.onclick = event => event.preventDefault();
+	}
+
 	async function initConstants() {
 	async function initConstants() {
 		[NOTES_WEB_STYLESHEET, MASK_WEB_STYLESHEET, HIGHLIGHTS_WEB_STYLESHEET] = await Promise.all([
 		[NOTES_WEB_STYLESHEET, MASK_WEB_STYLESHEET, HIGHLIGHTS_WEB_STYLESHEET] = await Promise.all([
 			minifyText(await ((await fetch("../pages/editor-note-web.css")).text())),
 			minifyText(await ((await fetch("../pages/editor-note-web.css")).text())),
@@ -1824,7 +1929,13 @@ pre code {
 	}
 	}
 
 
 	function formatPage(applySystemTheme) {
 	function formatPage(applySystemTheme) {
-		previousContent = getContent(false, []);
+		if (pageCompressContent) {
+			serializeShadowRoots(document);
+			previousContent = document.documentElement.cloneNode(true);
+			deserializeShadowRoots(document);
+		} else {
+			previousContent = getContent(false, []);
+		}
 		const shadowRoots = {};
 		const shadowRoots = {};
 		const classesToPreserve = ["single-file-highlight", "single-file-highlight-yellow", "single-file-highlight-green", "single-file-highlight-pink", "single-file-highlight-blue"];
 		const classesToPreserve = ["single-file-highlight", "single-file-highlight-yellow", "single-file-highlight-green", "single-file-highlight-pink", "single-file-highlight-blue"];
 		document.querySelectorAll(NOTE_TAGNAME).forEach(containerElement => {
 		document.querySelectorAll(NOTE_TAGNAME).forEach(containerElement => {
@@ -1888,7 +1999,13 @@ pre code {
 	async function cancelFormatPage() {
 	async function cancelFormatPage() {
 		if (previousContent) {
 		if (previousContent) {
 			const contentEditable = document.body.contentEditable;
 			const contentEditable = document.body.contentEditable;
-			await init({ content: previousContent }, { reset: true });
+			if (pageCompressContent) {
+				document.replaceChild(previousContent, document.documentElement);
+				deserializeShadowRoots(document);
+				await initPage();
+			} else {
+				await init({ content: previousContent }, { reset: true });
+			}
 			document.body.contentEditable = contentEditable;
 			document.body.contentEditable = contentEditable;
 			onUpdate(false);
 			onUpdate(false);
 			previousContent = null;
 			previousContent = null;
@@ -1935,13 +2052,56 @@ pre code {
 			doc.body.appendChild(element);
 			doc.body.appendChild(element);
 			element.textContent = resource.content;
 			element.textContent = resource.content;
 		});
 		});
-		return singlefile.helper.serialize(doc, compressHTML) + "<script " + SCRIPT_TEMPLATE_SHADOW_ROOT + ">" + getEmbedScript() + "</script>";
+		if (pageCompressContent) {
+			const pageFilename = pageResources
+				.filter(resource => resource.filename.endsWith("index.html"))
+				.sort((resourceLeft, resourceRight) => resourceLeft.filename.length - resourceRight.filename.length)[0].filename;
+			const resources = pageResources.filter(resource => resource.parentResources.includes(pageFilename));
+			doc.querySelectorAll("[src]").forEach(element => resources.forEach(resource => {
+				if (element.src == resource.content) {
+					element.src = resource.name;
+				}
+			}));
+			let content = singlefile.helper.serialize(doc, compressHTML);
+			const REGEXP_ESCAPE = /([{}()^$&.*?/+|[\\\\]|\]|-)/g;
+			resources.forEach(resource => {
+				const searchRegExp = new RegExp(resource.content.replace(REGEXP_ESCAPE, "\\$1"), "g");
+				const position = content.search(searchRegExp);
+				if (position != -1) {
+					content = content.replace(searchRegExp, resource.name);
+				}
+			});
+			return content + "<script " + SCRIPT_TEMPLATE_SHADOW_ROOT + ">" + getEmbedScript() + "</script>";
+		} else {
+			return singlefile.helper.serialize(doc, compressHTML) + "<script " + SCRIPT_TEMPLATE_SHADOW_ROOT + ">" + getEmbedScript() + "</script>";
+		}
 	}
 	}
 
 
 	function onUpdate(saved) {
 	function onUpdate(saved) {
 		window.parent.postMessage(JSON.stringify({ "method": "onUpdate", saved }), "*");
 		window.parent.postMessage(JSON.stringify({ "method": "onUpdate", saved }), "*");
 	}
 	}
 
 
+	function waitResourcesLoad() {
+		return new Promise(resolve => {
+			let counterMutations = 0;
+			const done = () => {
+				observer.disconnect();
+				resolve();
+			};
+			let timeoutInit = setTimeout(done, 100);
+			const observer = new MutationObserver(() => {
+				if (counterMutations < 20) {
+					counterMutations++;
+					clearTimeout(timeoutInit);
+					timeoutInit = setTimeout(done, 100);
+				} else {
+					done();
+				}
+			});
+			observer.observe(document, { subtree: true, childList: true, attributes: true });
+		});
+	}
+
 	function reflowNotes() {
 	function reflowNotes() {
 		document.querySelectorAll(NOTE_TAGNAME).forEach(containerElement => {
 		document.querySelectorAll(NOTE_TAGNAME).forEach(containerElement => {
 			const noteElement = containerElement.shadowRoot.querySelector("." + NOTE_CLASS);
 			const noteElement = containerElement.shadowRoot.querySelector("." + NOTE_CLASS);
@@ -2086,6 +2246,7 @@ pre code {
 			const getPosition = ${minifyText(getPosition.toString())};
 			const getPosition = ${minifyText(getPosition.toString())};
 			const onMouseUp = ${minifyText(onMouseUp.toString())};
 			const onMouseUp = ${minifyText(onMouseUp.toString())};
 			const getShadowRoot = ${minifyText(getShadowRoot.toString())};
 			const getShadowRoot = ${minifyText(getShadowRoot.toString())};
+			const waitResourcesLoad = ${minifyText(waitResourcesLoad.toString())};
 			const maskNoteElement = getMaskElement(${JSON.stringify(NOTE_MASK_CLASS)});
 			const maskNoteElement = getMaskElement(${JSON.stringify(NOTE_MASK_CLASS)});
 			const maskPageElement = getMaskElement(${JSON.stringify(PAGE_MASK_CLASS)}, ${JSON.stringify(PAGE_MASK_CONTAINER_CLASS)});
 			const maskPageElement = getMaskElement(${JSON.stringify(PAGE_MASK_CLASS)}, ${JSON.stringify(PAGE_MASK_CONTAINER_CLASS)});
 			let selectedNote, highlightSelectionMode, removeHighlightMode, resizingNoteMode, movingNoteMode, collapseNoteTimeout, cuttingMode, cuttingOuterMode;
 			let selectedNote, highlightSelectionMode, removeHighlightMode, resizingNoteMode, movingNoteMode, collapseNoteTimeout, cuttingMode, cuttingOuterMode;
@@ -2095,6 +2256,9 @@ pre code {
 			processNode(document);
 			processNode(document);
 			reflowNotes();
 			reflowNotes();
 			document.querySelectorAll(${JSON.stringify(NOTE_TAGNAME)}).forEach(noteElement => attachNoteListeners(noteElement));
 			document.querySelectorAll(${JSON.stringify(NOTE_TAGNAME)}).forEach(noteElement => attachNoteListeners(noteElement));
+			if (document.documentElement.dataset.sfz !== undefined) {
+				waitResourcesLoad().then(reflowNotes);
+			}
 		})()`);
 		})()`);
 	}
 	}
 
 

+ 50 - 8
src/ui/pages/help.html

@@ -113,10 +113,10 @@
 						<p class="notice">It is recommended to <u>check</u> this option</p>
 						<p class="notice">It is recommended to <u>check</u> this option</p>
 					</li>
 					</li>
 					<li data-options-label="openSavedPageLabel" id="openSavedPageOption"> <span class="option">Option:
 					<li data-options-label="openSavedPageLabel" id="openSavedPageOption"> <span class="option">Option:
-							open saved pages in a new
-							tab</span>
+							open saved pages in a new tab</span>
 						<p>Check this option to display the saved page in a new tab. This option can be useful, for
 						<p>Check this option to display the saved page in a new tab. This option can be useful, for
-							example, in order to check the page is properly saved.</p>
+							example, in order to check the page is properly saved. This option does not work with
+							archive formats based on ZIP files.</p>
 					</li>
 					</li>
 					<li data-options-label="autoCloseLabel"> <span class="option">Option: auto-close the tab after the
 					<li data-options-label="autoCloseLabel"> <span class="option">Option: auto-close the tab after the
 							page is saved</span>
 							page is saved</span>
@@ -178,6 +178,47 @@
 						</p>
 						</p>
 					</li>
 					</li>
 				</ul>
 				</ul>
+				<p>File format</p>
+				<ul>
+					<li data-options-label="fileFormatSelectLabel">
+						<span class="option">Option: format</span>
+						<p>Select the ouput format of the saved file:
+						</p>
+						<ul>
+							<li><code>"HTML"</code>: HTML file (default format)</li>
+							<li><code>"self-extracting ZIP (universal)"</code>: self-extracting ZIP file that can be
+								opened on any platform. This format produces files smaller than the "HTML" format but
+								requires JavaScript to be enabled to open the file.
+							</li>
+							<li><code>"self-extracting ZIP"</code>: self-extracting ZIP file that can be
+								opened on any platform from HTTP but not from the filesystem in some browsers (e.g.
+								in browsers based on Chromium or WebKit). This format produces files smaller (approx.
+								1%) than the "self-extracting ZIP (universal)" format.</li>
+							<li><code>"ZIP"</code>: ZIP file</li>
+						</ul>
+					</li>
+					<li data-options-label="passwordLabel">
+						<span class="option">Option: password</span>
+						<p>Type a password to encrypt the ZIP file with AES-256 (compatible with Winzip). Be careful, if
+							you lose the password, it will be impossible to open the file. Enabling this option
+							increases the CPU consumption and the time needed to save or read a page.</p>
+						</p>
+					</li>
+					<li data-options-label="createRootDirectoryLabel">
+						<span class="option">Option: create a root directory</span>
+						<p>Check this option to create a root directory in the ZIP file. The directory name consists of
+							a timestamp and the identifier of the tab where the saved page is displayed.
+						</p>
+					</li>
+					<li data-options-label="insertTextBodyLabel">
+						<span class="option">Option: make text searchable</span>
+						<p>Check this option to insert the text content of the saved page into the self-extracting ZIP
+							file. This makes it possible, for example, to search for pages from the text. Note that if
+							the selected fomrmat is "self-extracting ZIP (universal)", the text content is
+							encoded in UTF-8 but the page is declared in ISO-8859-1.
+						</p>
+					</li>
+				</ul>
 				<p>HTML content</p>
 				<p>HTML content</p>
 				<ul>
 				<ul>
 					<li data-options-label="compressHTMLLabel"> <span class="option">Option: compress HTML
 					<li data-options-label="compressHTMLLabel"> <span class="option">Option: compress HTML
@@ -298,7 +339,7 @@
 							images together</span>
 							images together</span>
 						<p>Check this option to avoid saving multiple times duplicate images. Checking this option
 						<p>Check this option to avoid saving multiple times duplicate images. Checking this option
 							should not alter the document in modern browsers and can considerably reduce the size of the
 							should not alter the document in modern browsers and can considerably reduce the size of the
-							file.</p>
+							file. This option is ignored with archive formats based on ZIP files.</p>
 						<p class="notice">It is recommended to <u>check</u> this option</p>
 						<p class="notice">It is recommended to <u>check</u> this option</p>
 					</li>
 					</li>
 					<li data-options-label="loadDeferredImagesLabel"> <span class="option">Option: save deferred
 					<li data-options-label="loadDeferredImagesLabel"> <span class="option">Option: save deferred
@@ -369,7 +410,8 @@
 					</li>
 					</li>
 					<li data-options-label="saveToClipboardLabel" id="saveToClipboardOption"> <span
 					<li data-options-label="saveToClipboardLabel" id="saveToClipboardOption"> <span
 							class="option">Option: copy to clipboard</span>
 							class="option">Option: copy to clipboard</span>
-						<p>Check this option to copy the page to the clipboard.</p>
+						<p>Check this option to copy the page to the clipboard. This option does not work with
+							archive formats based on ZIP files.</p>
 						<p class="notice">It is recommended to <u>uncheck</u> this option</p>
 						<p class="notice">It is recommended to <u>uncheck</u> this option</p>
 					</li>
 					</li>
 					<li data-options-label="saveToGitHubLabel"> <span class="option">Option: upload to GitHub</span>
 					<li data-options-label="saveToGitHubLabel"> <span class="option">Option: upload to GitHub</span>
@@ -532,11 +574,11 @@
 						<p class="notice">It is recommended to <u>uncheck</u> this option</p>
 						<p class="notice">It is recommended to <u>uncheck</u> this option</p>
 					</li>
 					</li>
 					<li data-options-label="displayInfobarInEditorLabel"> <span class="option">Option:
 					<li data-options-label="displayInfobarInEditorLabel"> <span class="option">Option:
-						display the infobar</span>
+							display the infobar</span>
 						<p>Check this option to display the infobar when displaying a page in the annotation editor.</p>
 						<p>Check this option to display the infobar when displaying a page in the annotation editor.</p>
 						<p class="notice">It is recommended to <u>uncheck</u> this option</p>
 						<p class="notice">It is recommended to <u>uncheck</u> this option</p>
-					</p>
-				</li>
+						</p>
+					</li>
 				</ul>
 				</ul>
 				<p id="bookmarksSection">Bookmarks</p>
 				<p id="bookmarksSection">Bookmarks</p>
 				<ul id="bookmarksOptions">
 				<ul id="bookmarksOptions">

+ 24 - 0
src/ui/pages/options.html

@@ -85,6 +85,30 @@
 				<input type="checkbox" id="replaceEmojisInFilenameInput">
 				<input type="checkbox" id="replaceEmojisInFilenameInput">
 			</div>
 			</div>
 		</details>
 		</details>
+		<details>
+			<summary id="fileFormatLabel"></summary>
+			<div class="option">
+				<label for="fileFormatSelectInput" id="fileFormatSelectLabel"></label>
+				<select id="fileFormatSelectInput">
+					<option id="fileFormatSelectHTMLLabel" value="html"></option>
+					<option id="fileFormatSelectSelfExtractingUniversalLabel" value="self-extracting-zip-universal"></option>
+					<option id="fileFormatSelectSelfExtractingLabel" value="self-extracting-zip"></option>
+					<option id="fileFormatSelectZIPLabel" value="zip"></option>
+				</select>
+			</div>
+			<div class="option vertical">
+				<label for="passwordInput" id="passwordLabel"></label>
+				<input type="text" id="passwordInput">
+			</div>
+			<div class="option">
+				<label for="createRootDirectoryInput" id="createRootDirectoryLabel"></label>
+				<input type="checkbox" id="createRootDirectoryInput">
+			</div>
+			<div class="option">
+				<label for="insertTextBodyInput" id="insertTextBodyLabel"></label>
+				<input type="checkbox" id="insertTextBodyInput">
+			</div>
+		</details>
 		<details>
 		<details>
 			<summary id="htmlContentLabel"></summary>
 			<summary id="htmlContentLabel"></summary>
 			<div class="option">
 			<div class="option">