Repository: Y2Z/monolith
Pull request #464 (2025-04-08 18:10:11)

Various improvements


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 20b942b..51a053b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -266,9 +266,9 @@ dependencies = [
 
 [[package]]
 name = "bstr"
-version = "1.11.3"
+version = "1.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0"
+checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
 dependencies = [
  "memchr",
  "regex-automata",
@@ -298,7 +298,7 @@ dependencies = [
  "glib",
  "libc",
  "once_cell",
- "thiserror",
+ "thiserror 1.0.69",
 ]
 
 [[package]]
@@ -314,9 +314,9 @@ dependencies = [
 
 [[package]]
 name = "cc"
-version = "1.2.17"
+version = "1.2.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a"
+checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c"
 dependencies = [
  "shlex",
 ]
@@ -353,9 +353,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.5.34"
+version = "4.5.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff"
+checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -363,9 +363,9 @@ dependencies = [
 
 [[package]]
 name = "clap_builder"
-version = "4.5.34"
+version = "4.5.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489"
+checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9"
 dependencies = [
  "anstream",
  "anstyle",
@@ -542,9 +542,9 @@ dependencies = [
 
 [[package]]
 name = "deranged"
-version = "0.4.1"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28cfac68e08048ae1883171632c2aef3ebc555621ae56fbccce1cbf22dd7f058"
+checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
 dependencies = [
  "powerfmt",
 ]
@@ -565,6 +565,27 @@ dependencies = [
  "crypto-common",
 ]
 
+[[package]]
+name = "directories"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.59.0",
+]
+
 [[package]]
 name = "displaydoc"
 version = "0.2.5"
@@ -692,9 +713,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
 
 [[package]]
 name = "errno"
-version = "0.3.10"
+version = "0.3.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
+checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e"
 dependencies = [
  "libc",
  "windows-sys 0.59.0",
@@ -718,9 +739,9 @@ dependencies = [
 
 [[package]]
 name = "flate2"
-version = "1.1.0"
+version = "1.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc"
+checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
 dependencies = [
  "crc32fast",
  "miniz_oxide",
@@ -757,7 +778,7 @@ version = "0.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2a530c4694a6a8d528794ee9bbd8ba0122e779629ac908d15ad5a7ae7763a33d"
 dependencies = [
- "thiserror",
+ "thiserror 1.0.69",
 ]
 
 [[package]]
@@ -942,6 +963,17 @@ dependencies = [
  "version_check",
 ]
 
+[[package]]
+name = "getrandom"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+]
+
 [[package]]
 name = "getrandom"
 version = "0.3.2"
@@ -977,7 +1009,7 @@ dependencies = [
  "once_cell",
  "pin-project-lite",
  "smallvec",
- "thiserror",
+ "thiserror 1.0.69",
 ]
 
 [[package]]
@@ -1012,7 +1044,7 @@ dependencies = [
  "libc",
  "once_cell",
  "smallvec",
- "thiserror",
+ "thiserror 1.0.69",
 ]
 
 [[package]]
@@ -1135,16 +1167,14 @@ dependencies = [
 
 [[package]]
 name = "html5ever"
-version = "0.28.0"
+version = "0.29.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ff6858c1f7e2a470c5403091866fa95b36fe0dbac5d771f932c15e5ff1ee501"
+checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c"
 dependencies = [
  "log",
  "mac",
  "markup5ever",
- "proc-macro2",
- "quote",
- "syn 2.0.100",
+ "match_token",
 ]
 
 [[package]]
@@ -1224,9 +1254,9 @@ dependencies = [
 
 [[package]]
 name = "hyper-util"
-version = "0.1.10"
+version = "0.1.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
+checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2"
 dependencies = [
  "bytes",
  "futures-channel",
@@ -1234,6 +1264,7 @@ dependencies = [
  "http",
  "http-body",
  "hyper",
+ "libc",
  "pin-project-lite",
  "socket2",
  "tokio",
@@ -1243,9 +1274,9 @@ dependencies = [
 
 [[package]]
 name = "iana-time-zone"
-version = "0.1.62"
+version = "0.1.63"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2fd658b06e56721792c5df4475705b6cda790e9298d19d2f8af083457bcd127"
+checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
 dependencies = [
  "android_system_properties",
  "core-foundation-sys",
@@ -1406,9 +1437,9 @@ dependencies = [
 
 [[package]]
 name = "indexmap"
-version = "2.8.0"
+version = "2.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
+checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
 dependencies = [
  "equivalent",
  "hashbrown",
@@ -1503,6 +1534,16 @@ version = "0.2.171"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
 
+[[package]]
+name = "libredox"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
+dependencies = [
+ "bitflags 2.9.0",
+ "libc",
+]
+
 [[package]]
 name = "linux-raw-sys"
 version = "0.9.3"
@@ -1548,9 +1589,9 @@ dependencies = [
 
 [[package]]
 name = "markup5ever"
-version = "0.13.0"
+version = "0.14.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d581ff8be69d08a2efa23a959d81aa22b739073f749f067348bd4f4ba4b69195"
+checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18"
 dependencies = [
  "log",
  "phf",
@@ -1562,9 +1603,9 @@ dependencies = [
 
 [[package]]
 name = "markup5ever_rcdom"
-version = "0.4.0-unofficial"
+version = "0.5.0-unofficial"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "735d687429167a6b78304c018d57d6d91b6f8be38af495b004a23934720a3f03"
+checksum = "d9cb12459c4cab18dcc580159590f404ad78c0a9c5435ace80288ed43abdce31"
 dependencies = [
  "html5ever",
  "markup5ever",
@@ -1572,6 +1613,17 @@ dependencies = [
  "xml5ever",
 ]
 
+[[package]]
+name = "match_token"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.100",
+]
+
 [[package]]
 name = "matches"
 version = "0.1.10"
@@ -1601,9 +1653,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
 
 [[package]]
 name = "miniz_oxide"
-version = "0.8.5"
+version = "0.8.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
+checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
 dependencies = [
  "adler2",
 ]
@@ -1629,6 +1681,7 @@ dependencies = [
  "chrono",
  "clap",
  "cssparser",
+ "directories",
  "druid",
  "encoding_rs",
  "html5ever",
@@ -1769,6 +1822,12 @@ dependencies = [
  "vcpkg",
 ]
 
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
 [[package]]
 name = "overload"
 version = "0.1.1"
@@ -2136,13 +2195,24 @@ dependencies = [
 
 [[package]]
 name = "redox_syscall"
-version = "0.5.10"
+version = "0.5.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1"
+checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3"
 dependencies = [
  "bitflags 2.9.0",
 ]
 
+[[package]]
+name = "redox_users"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
+dependencies = [
+ "getrandom 0.2.15",
+ "libredox",
+ "thiserror 2.0.12",
+]
+
 [[package]]
 name = "regex"
 version = "1.11.1"
@@ -2238,9 +2308,9 @@ dependencies = [
 
 [[package]]
 name = "rustix"
-version = "1.0.3"
+version = "1.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96"
+checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf"
 dependencies = [
  "bitflags 2.9.0",
  "errno",
@@ -2431,15 +2501,15 @@ dependencies = [
 
 [[package]]
 name = "smallvec"
-version = "1.14.0"
+version = "1.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
+checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
 
 [[package]]
 name = "socket2"
-version = "0.5.8"
+version = "0.5.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
+checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
 dependencies = [
  "libc",
  "windows-sys 0.52.0",
@@ -2550,7 +2620,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf"
 dependencies = [
  "fastrand",
- "getrandom",
+ "getrandom 0.3.2",
  "once_cell",
  "rustix",
  "windows-sys 0.59.0",
@@ -2579,7 +2649,16 @@ version = "1.0.69"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
 dependencies = [
- "thiserror-impl",
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
+dependencies = [
+ "thiserror-impl 2.0.12",
 ]
 
 [[package]]
@@ -2593,6 +2672,17 @@ dependencies = [
  "syn 2.0.100",
 ]
 
+[[package]]
+name = "thiserror-impl"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.100",
+]
+
 [[package]]
 name = "thread_local"
 version = "1.1.8"
@@ -2712,7 +2802,7 @@ dependencies = [
  "serde",
  "serde_spanned",
  "toml_datetime",
- "winnow 0.7.4",
+ "winnow 0.7.6",
 ]
 
 [[package]]
@@ -3095,11 +3185,37 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
 [[package]]
 name = "windows-core"
-version = "0.52.0"
+version = "0.61.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
+checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
 dependencies = [
- "windows-targets 0.52.6",
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings 0.4.0",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.100",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.100",
 ]
 
 [[package]]
@@ -3115,7 +3231,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
 dependencies = [
  "windows-result",
- "windows-strings",
+ "windows-strings 0.3.1",
  "windows-targets 0.53.0",
 ]
 
@@ -3137,6 +3253,15 @@ dependencies = [
  "windows-link",
 ]
 
+[[package]]
+name = "windows-strings"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
+dependencies = [
+ "windows-link",
+]
+
 [[package]]
 name = "windows-sys"
 version = "0.52.0"
@@ -3294,9 +3419,9 @@ dependencies = [
 
 [[package]]
 name = "winnow"
-version = "0.7.4"
+version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36"
+checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10"
 dependencies = [
  "memchr",
 ]
@@ -3339,9 +3464,9 @@ checksum = "a67300977d3dc3f8034dae89778f502b6ba20b269527b3223ba59c0cf393bb8a"
 
 [[package]]
 name = "xml5ever"
-version = "0.19.0"
+version = "0.20.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7b906d34d867d216b2d79fb0e9470aaa7f4948ea86b44c27846efedd596076c"
+checksum = "2278b4bf33071ba8e30368a59436c65eec8e01c49d5c29b3dfeb0cdc45331383"
 dependencies = [
  "log",
  "mac",
diff --git a/Cargo.toml b/Cargo.toml
index a354cc7..6543d28 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -26,12 +26,13 @@ license = "CC0-1.0"
 atty = "0.2.14"  # Used for highlighting network errors
 base64 = "0.22.1"  # Used for integrity attributes
 chrono = "0.4.40"  # Used for formatting timestamps
-clap = { version = "4.5.34", features = ["derive"], optional = true }  # Used for processing CLI arguments
+clap = { version = "4.5.35", features = ["derive"], optional = true }  # Used for processing CLI arguments
 cssparser = "0.35.0"  # Used for dealing with CSS
+directories = { version = "6.0.0", optional = true }  # Used for GUI
 druid = { version = "0.8.3", optional = true }  # Used for GUI
 encoding_rs = "0.8.35"  # Used for parsing and converting document charsets
-html5ever = "0.28.0"  # Used for all things DOM
-markup5ever_rcdom = "=0.4.0-unofficial"  # Used for manipulating DOM
+html5ever = "0.29.1"  # Used for all things DOM
+markup5ever_rcdom = "=0.5.0-unofficial"  # Used for manipulating DOM
 percent-encoding = "2.3.1"  # Used for encoding URLs
 sha2 = "0.10.8"  # Used for calculating checksums during integrity checks
 redb = "2.4.0"  # Used for on-disk caching of remote assets
@@ -39,7 +40,7 @@ tempfile = { version = "3.19.1", optional = true }  # Used for on-disk caching o
 url = "2.5.4"  # Used for parsing URLs
 openssl = "0.10.72"  # Used for static linking of the OpenSSL library
 
-# Used for parsing srcset and NOSCRIPT
+# Used for unwrapping NOSCRIPT
 [dependencies.regex]
 version = "1.11.1"
 default-features = false
@@ -57,7 +58,7 @@ assert_cmd = "2.0.16"
 [features]
 default = ["cli", "vendored-openssl"]
 cli = ["clap", "tempfile"]  # Build a CLI tool that includes main() function
-gui = ["druid", "tempfile"]  # Build a GUI executable that includes main() function
+gui = ["directories", "druid", "tempfile"]  # Build a GUI executable that includes main() function
 vendored-openssl = ["openssl/vendored"]  # Compile and statically link a copy of OpenSSL
 
 [lib]
diff --git a/Makefile b/Makefile
index ab47a7e..b29f816 100644
--- a/Makefile
+++ b/Makefile
@@ -29,6 +29,7 @@ install:
 
 lint:
 	@cargo clippy --fix --allow-dirty --allow-staged
+# 	@cargo fix --allow-dirty --allow-staged
 .PHONY: lint
 
 lint_check:
diff --git a/README.md b/README.md
index 7451938..4a171e8 100644
--- a/README.md
+++ b/README.md
@@ -4,8 +4,6 @@
 [![Monolith Actor on Apify](https://apify.com/actor-badge?actor=netmilk/monolith)](https://apify.com/netmilk/monolith?fpr=monolith)
 
 
-
-
 ```
  _____    _____________   __________     ___________________    ___
 |     \  /             \ |          |   |                   |  |   |
@@ -225,6 +223,7 @@ monolith -I -d example.com -d www.example.com https://example.com -o example-onl
 monolith -I -B -d .googleusercontent.com -d googleanalytics.com -d .google.com https://example.com -o example-no-ads.html
 ```
 
+
 ---------------------------------------------------
 
 
@@ -242,6 +241,16 @@ chromium --headless --window-size=1920,1080 --run-all-compositor-stages-before-d
 ---------------------------------------------------
 
 
+## Authentication
+
+```console
+monolith https://username:password@example.com -o example-basic-auth.html
+```
+
+
+---------------------------------------------------
+
+
 ## Proxies
 
 Please set `https_proxy`, `http_proxy`, and `no_proxy` environment variables.
diff --git a/src/core.rs b/src/core.rs
index 81ab3b5..57709f6 100644
--- a/src/core.rs
+++ b/src/core.rs
@@ -266,10 +266,9 @@ pub fn create_monolithic_document_from_data(
     }
 
     // Append noindex META-tag
-    if let meta_robots_content_value = get_robots(&dom.document).unwrap_or_default() {
-        if meta_robots_content_value.trim().is_empty() || meta_robots_content_value != "none" {
-            dom = set_robots(dom, "none");
-        }
+    let meta_robots_content_value = get_robots(&dom.document).unwrap_or_default();
+    if meta_robots_content_value.trim().is_empty() || meta_robots_content_value != "none" {
+        dom = set_robots(dom, "none");
     }
 
     // Save using specified charset, if given
@@ -523,11 +522,14 @@ pub fn domain_is_within_domain(domain: &str, domain_to_match_against: &str) -> b
     ok
 }
 
-pub fn format_output_path(destination: &str, document_title: &str) -> String {
+pub fn format_output_path(
+    path: &str,
+    document_title: &str,
+    output_format: MonolithOutputFormat,
+) -> String {
     let datetime: &str = &Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
 
-    destination
-        .replace("%timestamp%", &datetime.replace(':', "_"))
+    path.replace("%timestamp%", &datetime.replace(':', "_"))
         .replace(
             "%title%",
             document_title
@@ -541,13 +543,22 @@ pub fn format_output_path(destination: &str, document_title: &str) -> String {
                 .replace('?', "")
                 .trim_start_matches('.'),
         )
-        .to_string()
-        .replace('<', "[")
-        .replace('>', "]")
-        .replace(':', " - ")
-        .replace('\"', "")
-        .replace('|', "-")
-        .replace('?', "")
+        .replace(
+            "%ext%",
+            if output_format == MonolithOutputFormat::HTML {
+                "html"
+            } else {
+                ""
+            },
+        )
+        .replace(
+            "%extension%",
+            if output_format == MonolithOutputFormat::HTML {
+                "html"
+            } else {
+                ""
+            },
+        )
         .to_string()
 }
 
diff --git a/src/css.rs b/src/css.rs
index ccdc3f3..6cc6f37 100644
--- a/src/css.rs
+++ b/src/css.rs
@@ -229,8 +229,10 @@ pub fn process_css<'a>(
                         result.push_str(format_quoted_string(EMPTY_IMAGE_DATA_URL).as_str());
                     } else {
                         let resolved_url: Url = resolve_url(document_url, value);
+
                         match retrieve_asset(cache, client, document_url, &resolved_url, options) {
                             Ok((data, final_url, media_type, charset)) => {
+                                // TODO: if it's @font-face, exclude definitions of non-woff/woff-2 fonts (if woff/woff-2 are present)
                                 let mut data_url =
                                     create_data_url(&media_type, &charset, &data, &final_url);
                                 data_url.set_fragment(resolved_url.fragment());
diff --git a/src/gui.rs b/src/gui.rs
index 0fe1943..6ca58d6 100644
--- a/src/gui.rs
+++ b/src/gui.rs
@@ -1,20 +1,28 @@
 use std::fs;
 use std::io::Write;
+use std::path;
 use std::thread;
 
-use druid::commands;
-use druid::widget::{Button, Checkbox, Flex, Label, TextBox};
+use directories::UserDirs;
+use druid::widget::{Button, Checkbox, Either, Flex, Label, Spinner, TextBox};
 use druid::{
-    AppDelegate, AppLauncher, Command, DelegateCtx, Env, FileDialogOptions, Handled,
-    LocalizedString, PlatformError, Target, Widget, WidgetExt, WindowDesc,
+    commands, AppDelegate, AppLauncher, Command, Data, DelegateCtx, Env, FileDialogOptions,
+    FileSpec, Handled, Lens, LocalizedString, PlatformError, Target, Widget, WidgetExt, WindowDesc,
 };
-use druid::{Data, Lens};
 use tempfile::{Builder, NamedTempFile};
 
 use monolith::cache::Cache;
-use monolith::core::{create_monolithic_document, format_output_path, MonolithError, Options};
+use monolith::core::{
+    create_monolithic_document, format_output_path, MonolithError, MonolithOutputFormat, Options,
+};
 
 const CACHE_ASSET_FILE_SIZE_THRESHOLD: usize = 1024 * 20; // Minimum file size for on-disk caching (in bytes)
+const FILESPEC_HTML: FileSpec = FileSpec::new("HTML files", &["html"]);
+const MONOLITH_GUI_WRITE_OUTPUT: druid::Selector<(Vec<u8>, Option<String>)> =
+    druid::Selector::new("monolith-gui.write-output");
+const MONOLITH_GUI_ERROR: druid::Selector<MonolithError> =
+    druid::Selector::new("monolith-gui.error");
+const TEXT_BOX_WIDTH: f64 = 512_f64;
 
 struct Delegate;
 
@@ -32,11 +40,6 @@ struct AppState {
     busy: bool,
 }
 
-const MONOLITH_GUI_WRITE_OUTPUT: druid::Selector<(Vec<u8>, Option<String>)> =
-    druid::Selector::new("monolith-gui.write-output");
-const MONOLITH_GUI_ERROR: druid::Selector<MonolithError> =
-    druid::Selector::new("monolith-gui.error");
-
 fn main() -> Result<(), PlatformError> {
     let mut program_name: String = env!("CARGO_PKG_NAME").to_string();
     if let Some(l) = program_name.get_mut(0..1) {
@@ -44,16 +47,22 @@ fn main() -> Result<(), PlatformError> {
     }
     let main_window = WindowDesc::new(ui_builder())
         .title(program_name)
-        .with_min_size((640., 320.));
+        .with_min_size((720_f64, 360_f64));
     let state = AppState {
         target: "".to_string(),
-        keep_fonts: true,
+        keep_fonts: false,
         keep_frames: true,
         keep_images: true,
         keep_scripts: true,
         keep_styles: true,
-        output_path: "".to_string(), // TODO: set it to ~/Downloads/%title%.html by default
-        isolate: false,
+        output_path: if let Some(base_dirs) = UserDirs::new() {
+            base_dirs.download_dir().unwrap().display().to_string()
+                + &path::MAIN_SEPARATOR.to_string()
+                + "%title%.%ext%"
+        } else {
+            "%title%.%ext%".to_string()
+        },
+        isolate: true,
         unwrap_noscript: false,
         busy: false,
     };
@@ -64,50 +73,79 @@ fn main() -> Result<(), PlatformError> {
 }
 
 fn ui_builder() -> impl Widget<AppState> {
+    let target_label: Label<AppState> = Label::new("Target:");
     let target_input = TextBox::new()
-        .with_placeholder("Target (http:/https:/file:/data: URL) or filesystem path")
+        .with_placeholder("URL or filesystem path")
+        .fix_width(TEXT_BOX_WIDTH)
         .lens(AppState::target)
         .disabled_if(|state: &AppState, _env| state.busy);
     let target_button = Button::new(LocalizedString::new("Open file"))
         .on_click(|ctx, _, _| {
-            ctx.submit_command(commands::SHOW_OPEN_PANEL.with(FileDialogOptions::new()))
+            ctx.submit_command(
+                commands::SHOW_OPEN_PANEL.with(
+                    FileDialogOptions::new()
+                        .allowed_types(vec![FILESPEC_HTML])
+                        .default_type(FILESPEC_HTML),
+                ),
+            )
         })
+        .disabled_if(|state: &AppState, _env| state.busy)
+        .padding(5.0);
+    let output_path_label: Label<AppState> = Label::new("Output path:");
+    let output_path_input = TextBox::new()
+        .with_placeholder("Filesystem path")
+        .fix_width(TEXT_BOX_WIDTH)
+        .lens(AppState::output_path)
         .disabled_if(|state: &AppState, _env| state.busy);
-    let output_path_label: Label<AppState> =
-        Label::new(|state: &AppState, _env: &_| format!("Output path: {}", state.output_path));
     let output_path_button = Button::new(LocalizedString::new("Browse"))
-        .on_click(|ctx, _, _| {
+        .on_click(|ctx, state: &mut AppState, _env| {
             ctx.submit_command(
-                commands::SHOW_SAVE_PANEL
-                    .with(FileDialogOptions::new().default_name("%title% - %timestamp%.html")),
+                commands::SHOW_SAVE_PANEL.with(
+                    FileDialogOptions::new()
+                        // .force_starting_directory(
+                        //     state
+                        //         .output_path.clone()
+                        //         .split(path::MAIN_SEPARATOR).collect::<Vec<&str>>()[..2]
+                        //         .join(&path::MAIN_SEPARATOR.to_string())
+                        // )
+                        .default_name(
+                            state
+                                .output_path
+                                .clone()
+                                .split(path::MAIN_SEPARATOR)
+                                .last()
+                                .unwrap_or_default(),
+                        ),
+                ),
             )
         })
-        .disabled_if(|state: &AppState, _env| state.busy);
-    let fonts_checkbox = Checkbox::new("Fonts")
+        .disabled_if(|state: &AppState, _env| state.busy)
+        .padding(5.0);
+    let fonts_checkbox = Checkbox::new("Include fonts")
         .lens(AppState::keep_fonts)
         .disabled_if(|state: &AppState, _env| state.busy)
         .padding(5.0);
-    let frames_checkbox = Checkbox::new("Frames")
+    let frames_checkbox = Checkbox::new("Include frames")
         .lens(AppState::keep_frames)
         .disabled_if(|state: &AppState, _env| state.busy)
         .padding(5.0);
-    let images_checkbox = Checkbox::new("Images")
+    let images_checkbox = Checkbox::new("Include images")
         .lens(AppState::keep_images)
         .disabled_if(|state: &AppState, _env| state.busy)
         .padding(5.0);
-    let styles_checkbox = Checkbox::new("Styles")
+    let styles_checkbox = Checkbox::new("Include styles")
         .lens(AppState::keep_styles)
         .disabled_if(|state: &AppState, _env| state.busy)
         .padding(5.0);
-    let scripts_checkbox = Checkbox::new("Scripts")
+    let scripts_checkbox = Checkbox::new("Include scripts")
         .lens(AppState::keep_scripts)
         .disabled_if(|state: &AppState, _env| state.busy)
         .padding(5.0);
-    let isolate_checkbox = Checkbox::new("Isolate")
+    let isolate_checkbox = Checkbox::new("Isolate document")
         .lens(AppState::isolate)
         .disabled_if(|state: &AppState, _env| state.busy)
         .padding(5.0);
-    let unwrap_noscript_checkbox = Checkbox::new("Unwrap NOSCRIPTs")
+    let unwrap_noscript_checkbox = Checkbox::new("Unwrap NOSCRIPT")
         .lens(AppState::unwrap_noscript)
         .disabled_if(|state: &AppState, _env| state.busy)
         .padding(5.0);
@@ -177,11 +215,21 @@ fn ui_builder() -> impl Widget<AppState> {
         })
         .disabled_if(|state: &AppState, _env| {
             state.busy || state.target.is_empty() || state.output_path.is_empty()
-        });
+        })
+        .padding(5.0);
+    let spinner = Either::new(
+        |sate: &AppState, _env| sate.busy,
+        Spinner::new(),
+        Label::new(""),
+    )
+    .padding(5.0);
 
     Flex::column()
+        .with_spacer(5_f64)
         .with_child(
             Flex::row()
+                .with_child(target_label)
+                .with_spacer(5_f64)
                 .with_child(target_input)
                 .with_child(target_button),
         )
@@ -193,6 +241,8 @@ fn ui_builder() -> impl Widget<AppState> {
         .with_child(
             Flex::row()
                 .with_child(output_path_label)
+                .with_spacer(5_f64)
+                .with_child(output_path_input)
                 .with_child(output_path_button),
         )
         .with_child(
@@ -201,6 +251,8 @@ fn ui_builder() -> impl Widget<AppState> {
                 .with_child(unwrap_noscript_checkbox),
         )
         .with_child(start_stop_button)
+        .with_child(spinner)
+        .with_spacer(5_f64)
 }
 
 impl AppDelegate<AppState> for Delegate {
@@ -212,13 +264,27 @@ impl AppDelegate<AppState> for Delegate {
         state: &mut AppState,
         _env: &Env,
     ) -> Handled {
-        if let Some(result) = cmd.get(MONOLITH_GUI_WRITE_OUTPUT) {
+        // Handle "Open file" button next to target input
+        if let Some(file_info) = cmd.get(commands::OPEN_FILE) {
+            state.target = file_info.path().display().to_string();
+
+            return Handled::Yes;
+        }
+        // Handle "Browse" button next to output path input
+        else if let Some(file_info) = cmd.get(commands::SAVE_FILE_AS) {
+            state.output_path = file_info.path().display().to_string();
+
+            return Handled::Yes;
+        }
+        // Write output
+        else if let Some(result) = cmd.get(MONOLITH_GUI_WRITE_OUTPUT) {
             let (html, title) = result;
 
             if !state.output_path.is_empty() {
                 match fs::File::create(format_output_path(
                     &state.output_path,
                     &title.clone().unwrap_or_default(),
+                    MonolithOutputFormat::HTML,
                 )) {
                     Ok(mut file) => {
                         let _ = file.write(&html);
@@ -234,24 +300,12 @@ impl AppDelegate<AppState> for Delegate {
             state.busy = false;
             return Handled::Yes;
         }
-
-        if let Some(_error) = cmd.get(MONOLITH_GUI_ERROR) {
+        // Handle errors
+        else if let Some(_error) = cmd.get(MONOLITH_GUI_ERROR) {
             state.busy = false;
             return Handled::Yes;
         }
 
-        if let Some(file_info) = cmd.get(commands::OPEN_FILE) {
-            state.target = file_info.path().display().to_string();
-
-            return Handled::Yes;
-        }
-
-        if let Some(file_info) = cmd.get(commands::SAVE_FILE_AS) {
-            state.output_path = file_info.path().display().to_string();
-
-            return Handled::Yes;
-        }
-
         Handled::No
     }
 }
diff --git a/src/main.rs b/src/main.rs
index d3e8313..b991950 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -9,7 +9,7 @@ use monolith::cache::Cache;
 use monolith::cookies::parse_cookie_file_contents;
 use monolith::core::{
     create_monolithic_document, create_monolithic_document_from_data, format_output_path,
-    print_error_message, Options,
+    print_error_message, MonolithOutputFormat, Options,
 };
 
 const ASCII: &str = " \
@@ -129,7 +129,8 @@ impl Output {
         if destination.is_empty() || destination.eq("-") {
             Ok(Output::Stdout(io::stdout()))
         } else {
-            let final_destination = format_output_path(destination, document_title);
+            let final_destination =
+                format_output_path(destination, document_title, MonolithOutputFormat::HTML);
             Ok(Output::File(fs::File::create(final_destination)?))
         }
     }


Test patch:
diff --git a/tests/core/format_output_path.rs b/tests/core/format_output_path.rs
index a7d94db..ea6fab7 100644
--- a/tests/core/format_output_path.rs
+++ b/tests/core/format_output_path.rs
@@ -7,19 +7,26 @@
 
 #[cfg(test)]
 mod passing {
-    use monolith::core::format_output_path;
+    use monolith::core::{format_output_path, MonolithOutputFormat};
 
     #[test]
     fn as_is() {
-        let final_destination = format_output_path("/home/username/Downloads/website.html", "");
+        let final_destination = format_output_path(
+            "/home/username/Downloads/website.html",
+            "",
+            MonolithOutputFormat::HTML,
+        );
 
         assert_eq!(final_destination, "/home/username/Downloads/website.html");
     }
 
     #[test]
     fn substitute_title() {
-        let final_destination =
-            format_output_path("/home/username/Downloads/%title%.html", "Document Title");
+        let final_destination = format_output_path(
+            "/home/username/Downloads/%title%.html",
+            "Document Title",
+            MonolithOutputFormat::HTML,
+        );
 
         assert_eq!(
             final_destination,
@@ -32,6 +39,7 @@ mod passing {
         let final_destination = format_output_path(
             "/home/username/Downloads/%title%/%title%.html",
             "Document Title",
+            MonolithOutputFormat::HTML,
         );
 
         assert_eq!(
@@ -45,18 +53,36 @@ mod passing {
         let final_destination = format_output_path(
             r#"/home/username/Downloads/<>:"|?/%title%.html"#,
             r#"/\<>:"|?"#,
+            MonolithOutputFormat::HTML,
         );
 
         assert_eq!(
             final_destination,
-            r#"/home/username/Downloads/[] - -/__[] - -.html"#
+            r#"/home/username/Downloads/<>:"|?/__[] - -.html"#
         );
     }
 
     #[test]
     fn level_up() {
-        let final_destination = format_output_path(r#"../%title%.html"#, r#".Title"#);
+        let final_destination =
+            format_output_path("../%title%.html", ".Title", MonolithOutputFormat::HTML);
 
         assert_eq!(final_destination, r#"../Title.html"#);
     }
+
+    #[test]
+    fn file_name_extension() {
+        let final_destination =
+            format_output_path("%title%.%extension%", "Title", MonolithOutputFormat::HTML);
+
+        assert_eq!(final_destination, r#"Title.html"#);
+    }
+
+    #[test]
+    fn file_name_extension_short() {
+        let final_destination =
+            format_output_path("%title%.%ext%", "Title", MonolithOutputFormat::HTML);
+
+        assert_eq!(final_destination, r#"Title.html"#);
+    }
 }