diff --git a/.gitignore b/.gitignore index 2f3e926d..fe21aa37 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *flatpak* build +Cargo.lock repo target \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index 8a467b6f..00000000 --- a/Cargo.lock +++ /dev/null @@ -1,1806 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "Yoda" -version = "0.12.10" -dependencies = [ - "ansi-parser", - "anyhow", - "ggemini", - "ggemtext", - "gtk4", - "indexmap", - "itertools", - "libadwaita", - "libspelling", - "maxminddb", - "openssl", - "plurify", - "r2d2", - "r2d2_sqlite", - "regex", - "rusqlite", - "sourceview5", - "strip-tags", - "syntect", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "ansi-parser" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43e7fd8284f025d0bd143c2855618ecdf697db55bde39211e5c9faec7669173" -dependencies = [ - "heapless", - "nom", -] - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] -name = "bitflags" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" - -[[package]] -name = "bumpalo" -version = "3.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "cairo-rs" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b01fe135c0bd16afe262b6dea349bd5ea30e6de50708cec639aae7c5c14cc7e4" -dependencies = [ - "bitflags", - "cairo-sys-rs", - "glib", - "libc", -] - -[[package]] -name = "cairo-sys-rs" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06c28280c6b12055b5e39e4554271ae4e6630b27c0da9148c4cf6485fc6d245c" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "cc" -version = "1.2.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-expr" -version = "0.20.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6b04e07d8080154ed4ac03546d9a2b303cc2fe1901ba0b35b301516e289368" -dependencies = [ - "smallvec", - "target-lexicon", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "chacha20" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" -dependencies = [ - "cfg-if", - "cpufeatures", - "rand_core", -] - -[[package]] -name = "cpufeatures" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - -[[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" - -[[package]] -name = "field-offset" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" -dependencies = [ - "memoffset", - "rustc_version", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-executor" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - -[[package]] -name = "futures-macro" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-core", - "futures-macro", - "futures-task", - "pin-project-lite", - "slab", -] - -[[package]] -name = "gdk-pixbuf" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "debb0d39e3cdd84626edfd54d6e4a6ba2da9a0ef2e796e691c4e9f8646fda00c" -dependencies = [ - "gdk-pixbuf-sys", - "gio", - "glib", - "libc", -] - -[[package]] -name = "gdk-pixbuf-sys" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd95ad50b9a3d2551e25dd4f6892aff0b772fe5372d84514e9d0583af60a0ce7" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gdk4" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756564212bbe4a4ce05d88ffbd2582581ac6003832d0d32822d0825cca84bfbf" -dependencies = [ - "cairo-rs", - "gdk-pixbuf", - "gdk4-sys", - "gio", - "glib", - "libc", - "pango", -] - -[[package]] -name = "gdk4-sys" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6d4e5b3ccf591826a4adcc83f5f57b4e59d1925cb4bf620b0d645f79498b034" -dependencies = [ - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "pkg-config", - "system-deps", -] - -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "rand_core", - "wasip2", - "wasip3", -] - -[[package]] -name = "ggemini" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c455ed1aa0c9e3d17431a14797ca1c5950dca1e02a8f07b2faf6d6c696f12f" -dependencies = [ - "gio", - "glib", -] - -[[package]] -name = "ggemtext" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bc3f1aec690916cf0c078039df3e28647ffe0e883ec013c080a4d1e0f0fae13" -dependencies = [ - "glib", -] - -[[package]] -name = "gio" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5ff48bf600c68b476e61dc6b7c762f2f4eb91deef66583ba8bb815c30b5811a" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "gio-sys", - "glib", - "libc", - "pin-project-lite", - "smallvec", -] - -[[package]] -name = "gio-sys" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0071fe88dba8e40086c8ff9bbb62622999f49628344b1d1bf490a48a29d80f22" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", - "windows-sys", -] - -[[package]] -name = "glib" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16de123c2e6c90ce3b573b7330de19be649080ec612033d397d72da265f1bd8b" -dependencies = [ - "bitflags", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "futures-util", - "gio-sys", - "glib-macros", - "glib-sys", - "gobject-sys", - "libc", - "memchr", - "smallvec", -] - -[[package]] -name = "glib-macros" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf59b675301228a696fe01c3073974643365080a76cc3ed5bc2cbc466ad87f17" -dependencies = [ - "heck", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "glib-sys" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d95e1a3a19ae464a7286e14af9a90683c64d70c02532d88d87ce95056af3e6c" -dependencies = [ - "libc", - "system-deps", -] - -[[package]] -name = "gobject-sys" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dca35da0d19a18f4575f3cb99fe1c9e029a2941af5662f326f738a21edaf294" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "graphene-rs" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2730030ac9db663fd8bfe1e7093742c1cafb92db9c315c9417c29032341fe2f9" -dependencies = [ - "glib", - "graphene-sys", - "libc", -] - -[[package]] -name = "graphene-sys" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915e32091ea9ad241e4b044af62b7351c2d68aeb24f489a0d7f37a0fc484fd93" -dependencies = [ - "glib-sys", - "libc", - "pkg-config", - "system-deps", -] - -[[package]] -name = "gsk4" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e755de9d8c5896c5beaa028b89e1969d067f1b9bf1511384ede971f5983aa153" -dependencies = [ - "cairo-rs", - "gdk4", - "glib", - "graphene-rs", - "gsk4-sys", - "libc", - "pango", -] - -[[package]] -name = "gsk4-sys" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ce91472391146f482065f1041876d8f869057b195b95399414caa163d72f4f7" -dependencies = [ - "cairo-sys-rs", - "gdk4-sys", - "glib-sys", - "gobject-sys", - "graphene-sys", - "libc", - "pango-sys", - "system-deps", -] - -[[package]] -name = "gtk4" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acb21d53cfc6f7bfaf43549731c43b67ca47d87348d81c8cfc4dcdd44828e1a4" -dependencies = [ - "cairo-rs", - "field-offset", - "futures-channel", - "gdk-pixbuf", - "gdk4", - "gio", - "glib", - "graphene-rs", - "gsk4", - "gtk4-macros", - "gtk4-sys", - "libc", - "pango", -] - -[[package]] -name = "gtk4-macros" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ccfb5a14a3d941244815d5f8101fa12d4577b59cc47245778d8d907b0003e42" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "gtk4-sys" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "842577fe5a1ee15d166cd3afe804ce0cab6173bc789ca32e21308834f20088dd" -dependencies = [ - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gdk4-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "graphene-sys", - "gsk4-sys", - "libc", - "pango-sys", - "system-deps", -] - -[[package]] -name = "hash32" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" -dependencies = [ - "byteorder", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "foldhash 0.2.0", -] - -[[package]] -name = "hashlink" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" -dependencies = [ - "hashbrown 0.16.1", -] - -[[package]] -name = "heapless" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" -dependencies = [ - "hash32", - "stable_deref_trait", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - -[[package]] -name = "ipnetwork" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf370abdafd54d13e54a620e8c3e1145f28e46cc9d704bc6d94414559df41763" - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "js-sys" -version = "0.3.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "libadwaita" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb09e12bf8f73342b3315c839d0a7668cc0ccebd78490c49fec48bab15d5484b" -dependencies = [ - "gdk4", - "gio", - "glib", - "gtk4", - "libadwaita-sys", - "libc", - "pango", -] - -[[package]] -name = "libadwaita-sys" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7f94227ba87eb596fecada2491f04e357d507324142f77bf76d9e6be4a3e31" -dependencies = [ - "gdk4-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "gtk4-sys", - "libc", - "pango-sys", - "system-deps", -] - -[[package]] -name = "libc" -version = "0.2.183" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" - -[[package]] -name = "libspelling" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc92df39e5bcfed67e8263756b1f4d4f839370baf8a493cbd958e192944c85c2" -dependencies = [ - "gio", - "glib", - "gtk4", - "libc", - "libspelling-sys", - "sourceview5", -] - -[[package]] -name = "libspelling-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "286fffcc712568745b5c7f74063f84d99b2094c49345966b1fc12ff1a216fa5f" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "gtk4-sys", - "libc", - "sourceview5-sys", - "system-deps", -] - -[[package]] -name = "libsqlite3-sys" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95b4103cffefa72eb8428cb6b47d6627161e51c2739fc5e3b734584157bc642a" -dependencies = [ - "pkg-config", - "vcpkg", -] - -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "maxminddb" -version = "0.27.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76371bd37ce742f8954daabd0fde7f1594ee43ac2200e20c003ba5c3d65e2192" -dependencies = [ - "ipnetwork", - "log", - "memchr", - "serde", - "thiserror", -] - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "num-conv" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "onig" -version = "6.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" -dependencies = [ - "bitflags", - "libc", - "once_cell", - "onig_sys", -] - -[[package]] -name = "onig_sys" -version = "69.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" -dependencies = [ - "cc", - "pkg-config", -] - -[[package]] -name = "openssl" -version = "0.10.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "openssl-sys" -version = "0.9.112" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "pango" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d1d85e2078077a065bb7fc072783d5bcd4e51b379f22d67107d0a16937eb69" -dependencies = [ - "gio", - "glib", - "libc", - "pango-sys", -] - -[[package]] -name = "pango-sys" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4f06627d36ed5ff303d2df65211fc2e52ba5b17bf18dd80ff3d9628d6e06cfd" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "plist" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" -dependencies = [ - "base64", - "indexmap", - "quick-xml", - "serde", - "time", -] - -[[package]] -name = "plurify" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0642d69883c3a79565ea9b89a9cd8a845c79a7be17fd4fb8c6347b329f03455b" - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quick-xml" -version = "0.38.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" -dependencies = [ - "memchr", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "r2d2" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" -dependencies = [ - "log", - "parking_lot", - "scheduled-thread-pool", -] - -[[package]] -name = "r2d2_sqlite" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2ebd03c29250cdf191da93a35118b4567c2ef0eacab54f65e058d6f4c9965f6" -dependencies = [ - "r2d2", - "rusqlite", - "uuid", -] - -[[package]] -name = "rand" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" -dependencies = [ - "chacha20", - "getrandom", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" - -[[package]] -name = "rsqlite-vfs" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" -dependencies = [ - "hashbrown 0.16.1", - "thiserror", -] - -[[package]] -name = "rusqlite" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1c93dd1c9683b438c392c492109cb702b8090b2bfc8fed6f6e4eb4523f17af3" -dependencies = [ - "bitflags", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink", - "libsqlite3-sys", - "smallvec", - "sqlite-wasm-rs", -] - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scheduled-thread-pool" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" -dependencies = [ - "parking_lot", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_spanned" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" -dependencies = [ - "serde_core", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "simd-adler32" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "sourceview5" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4acb02162917d7b689c84f4ed710c22302c67c7e61da28a4e8ac548c04442c" -dependencies = [ - "futures-channel", - "futures-core", - "gdk-pixbuf", - "gdk4", - "gio", - "glib", - "gtk4", - "libc", - "pango", - "sourceview5-sys", -] - -[[package]] -name = "sourceview5-sys" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1c1529de5df2653788828ef71d44b113bb80e496bc17315f4122106bd6eebae" -dependencies = [ - "gdk-pixbuf-sys", - "gdk4-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "gtk4-sys", - "libc", - "pango-sys", - "system-deps", -] - -[[package]] -name = "sqlite-wasm-rs" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4206ed3a67690b9c29b77d728f6acc3ce78f16bf846d83c94f76400320181b" -dependencies = [ - "cc", - "js-sys", - "rsqlite-vfs", - "wasm-bindgen", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "strip-tags" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd2b127e68202f5f285a116f616d5d11735cca5e4befaea0347becd445b05b2" - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syntect" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" -dependencies = [ - "bincode", - "flate2", - "fnv", - "once_cell", - "onig", - "plist", - "regex-syntax", - "serde", - "serde_derive", - "serde_json", - "thiserror", - "walkdir", - "yaml-rust", -] - -[[package]] -name = "system-deps" -version = "7.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c8f33736f986f16d69b6cb8b03f55ddcad5c41acc4ccc39dd88e84aa805e7f" -dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml", - "version-compare", -] - -[[package]] -name = "target-lexicon" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "time" -version = "0.3.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" - -[[package]] -name = "time-macros" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "toml" -version = "0.9.12+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow 0.7.15", -] - -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_datetime" -version = "1.0.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.25.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1" -dependencies = [ - "indexmap", - "toml_datetime 1.0.1+spec-1.1.0", - "toml_parser", - "winnow 1.0.0", -] - -[[package]] -name = "toml_parser" -version = "1.0.10+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" -dependencies = [ - "winnow 1.0.0", -] - -[[package]] -name = "toml_writer" -version = "1.0.7+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "uuid" -version = "1.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" -dependencies = [ - "getrandom", - "js-sys", - "rand", - "wasm-bindgen", -] - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version-compare" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" - -[[package]] -name = "winnow" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 75c5b126..58bc5c8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "Yoda" -version = "0.12.10" +version = "0.12.6" edition = "2024" license = "MIT" readme = "README.md" @@ -22,7 +22,7 @@ features = ["gnome_46"] [dependencies.sqlite] package = "rusqlite" -version = "0.38.0" +version = "0.37.0" [dependencies.sourceview] package = "sourceview5" @@ -36,13 +36,11 @@ ggemtext = "0.7.0" indexmap = "2.10.0" itertools = "0.14.0" libspelling = "0.4.1" -maxminddb = "0.27.3" +maxminddb = "0.26.0" openssl = "0.10.72" plurify = "0.2.0" r2d2 = "0.8.10" -r2d2_sqlite = "0.32.0" -regex = "1.12.3" -strip-tags = "0.1.0" +r2d2_sqlite = "0.31.0" syntect = "5.2.0" # development diff --git a/README.md b/README.md index 72e41dc8..146e4461 100644 --- a/README.md +++ b/README.md @@ -135,9 +135,8 @@ The Gemini protocol was designed as a minimalistic, tracking-resistant alternati #### Text * [x] `text/gemini` - * [x] `text/markdown` - * [x] `text/nex` * [x] `text/plain` + * [x] `text/nex` #### Images * [x] `image/gif` @@ -166,7 +165,7 @@ The Gemini protocol was designed as a minimalistic, tracking-resistant alternati * Glib `2.80+` * Gtk `4.14+` * GtkSourceView `5.14+` -* libadwaita `1.5+` (Ubuntu `24.04+`) +* libadwaita `1.5+` (Ubuntu 24.04+) * libspelling `0.1+` #### Debian @@ -235,7 +234,7 @@ flatpak-builder --force-clean build\ #### Contributors -![StandWithUkraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg) +![wakatime](https://wakatime.com/badge/user/0b7fe6c1-b091-4c98-b930-75cfee17c7a5/project/018ebca8-4d22-4f9e-b557-186be6553d9a.svg) ![StandWithUkraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg) ### Localization diff --git a/src/app/browser/window/header/bar.rs b/src/app/browser/window/header/bar.rs index a066d741..dc0c8d65 100644 --- a/src/app/browser/window/header/bar.rs +++ b/src/app/browser/window/header/bar.rs @@ -27,20 +27,10 @@ impl Bar for Box { .orientation(Orientation::Horizontal) .spacing(8) .build(); - // left controls placement - if gtk::Settings::default().is_some_and(|s| { - s.gtk_decoration_layout() - .is_some_and(|l| l.starts_with("close")) - }) { - g_box.append(&Control::left().window_controls); - g_box.append(&MenuButton::menu((browser_action, window_action))); - g_box.append(&TabBar::tab(window_action, view)) - // default layout - } else { - g_box.append(&TabBar::tab(window_action, view)); - g_box.append(&MenuButton::menu((browser_action, window_action))); - g_box.append(&Control::right().window_controls) - } + + g_box.append(&TabBar::tab(window_action, view)); + g_box.append(&MenuButton::menu((browser_action, window_action))); + g_box.append(&Control::new().window_controls); g_box } } diff --git a/src/app/browser/window/header/bar/control.rs b/src/app/browser/window/header/bar/control.rs index 41917aec..05848ac8 100644 --- a/src/app/browser/window/header/bar/control.rs +++ b/src/app/browser/window/header/bar/control.rs @@ -8,12 +8,13 @@ pub struct Control { impl Default for Control { fn default() -> Self { - Self::right() + Self::new() } } impl Control { - pub fn right() -> Self { + // Construct + pub fn new() -> Self { Self { window_controls: WindowControls::builder() .margin_end(MARGIN) @@ -21,12 +22,4 @@ impl Control { .build(), } } - pub fn left() -> Self { - Self { - window_controls: WindowControls::builder() - .margin_end(MARGIN) - .side(PackType::Start) - .build(), - } - } } diff --git a/src/app/browser/window/header/bar/tab.rs b/src/app/browser/window/header/bar/tab.rs index 8fd17c67..ae6ca0b2 100644 --- a/src/app/browser/window/header/bar/tab.rs +++ b/src/app/browser/window/header/bar/tab.rs @@ -14,12 +14,8 @@ impl Tab for TabBar { fn tab(window_action: &Rc, view: &TabView) -> Self { TabBar::builder() .autohide(false) - .end_action_widget(&Button::append(window_action)) // @TODO find solution to append after tabs .expand_tabs(false) - .inverted(gtk::Settings::default().is_some_and(|s| { - s.gtk_decoration_layout() - .is_some_and(|l| l.starts_with("close")) - })) // show `x` button at left by respecting the env settings + .end_action_widget(&Button::append(window_action)) // @TODO find solution to append after tabs .view(view) .build() } diff --git a/src/app/browser/window/tab.rs b/src/app/browser/window/tab.rs index 60d54b86..c9dd25cb 100644 --- a/src/app/browser/window/tab.rs +++ b/src/app/browser/window/tab.rs @@ -10,6 +10,7 @@ use adw::{TabPage, TabView}; use anyhow::Result; use gtk::{ Box, Orientation, + gio::Icon, glib::Propagation, prelude::{ActionExt, EditableExt, EntryExt, WidgetExt}, }; @@ -43,6 +44,13 @@ impl Tab { .menu_model(>k::gio::Menu::menu(window_action)) .build(); + // Change default icon (if available in the system icon set) + // * visible for pinned tabs only + // * @TODO not default GTK behavior, make this feature optional + if let Ok(default_icon) = Icon::for_string("view-pin-symbolic") { + tab_view.set_default_icon(&default_icon); + } + // Init events tab_view.connect_setup_menu({ let index = index.clone(); diff --git a/src/app/browser/window/tab/item/client/driver/file.rs b/src/app/browser/window/tab/item/client/driver/file.rs index c56edbab..2dd3abf9 100644 --- a/src/app/browser/window/tab/item/client/driver/file.rs +++ b/src/app/browser/window/tab/item/client/driver/file.rs @@ -71,31 +71,6 @@ impl File { .set_mime(Some(content_type.to_string())); } match content_type.as_str() { - "text/gemini" => { - if matches!(*feature, Feature::Source) { - load_contents_async(file, cancellable, move |result| { - match result { - Ok(data) => { - Text::Source(uri, data).handle(&page) - } - Err(message) => { - Status::Failure(message).handle(&page) - } - } - }) - } else { - load_contents_async(file, cancellable, move |result| { - match result { - Ok(data) => { - Text::Gemini(uri, data).handle(&page) - } - Err(message) => { - Status::Failure(message).handle(&page) - } - } - }) - } - } "text/plain" => { if matches!(*feature, Feature::Source) { load_contents_async(file, cancellable, move |result| { @@ -119,18 +94,6 @@ impl File { } } }); - } else if url.ends_with(".md") || url.ends_with(".markdown") - { - load_contents_async(file, cancellable, move |result| { - match result { - Ok(data) => { - Text::Markdown(uri, data).handle(&page) - } - Err(message) => { - Status::Failure(message).handle(&page) - } - } - }) } else { load_contents_async(file, cancellable, move |result| { match result { @@ -144,31 +107,6 @@ impl File { }) } } - "text/markdown" => { - if matches!(*feature, Feature::Source) { - load_contents_async(file, cancellable, move |result| { - match result { - Ok(data) => { - Text::Source(uri, data).handle(&page) - } - Err(message) => { - Status::Failure(message).handle(&page) - } - } - }) - } else { - load_contents_async(file, cancellable, move |result| { - match result { - Ok(data) => { - Text::Markdown(uri, data).handle(&page) - } - Err(message) => { - Status::Failure(message).handle(&page) - } - } - }) - } - } "image/png" | "image/gif" | "image/jpeg" | "image/webp" => { match gtk::gdk::Texture::from_file(&file) { Ok(texture) => { diff --git a/src/app/browser/window/tab/item/client/driver/file/text.rs b/src/app/browser/window/tab/item/client/driver/file/text.rs index 8be84797..b7f8aa31 100644 --- a/src/app/browser/window/tab/item/client/driver/file/text.rs +++ b/src/app/browser/window/tab/item/client/driver/file/text.rs @@ -2,13 +2,12 @@ use gtk::glib::Uri; pub enum Text { Gemini(Uri, String), - Markdown(Uri, String), Plain(Uri, String), Source(Uri, String), } impl Text { - pub fn handle(&self, page: &std::rc::Rc) { + pub fn handle(&self, page: &super::Page) { page.navigation .request .info @@ -21,15 +20,7 @@ impl Text { .info .borrow_mut() .set_mime(Some("text/gemini".to_string())); - page.content.to_text_gemini(&page.profile, uri, data) - }), - Self::Markdown(uri, data) => (uri, { - page.navigation - .request - .info - .borrow_mut() - .set_mime(Some("text/markdown".to_string())); - page.content.to_text_markdown(page, uri, data) + page.content.to_text_gemini(uri, data) }), Self::Plain(uri, data) => (uri, page.content.to_text_plain(data)), Self::Source(uri, data) => (uri, page.content.to_text_source(data)), diff --git a/src/app/browser/window/tab/item/client/driver/gemini.rs b/src/app/browser/window/tab/item/client/driver/gemini.rs index 40f33ad1..8a30f9f4 100644 --- a/src/app/browser/window/tab/item/client/driver/gemini.rs +++ b/src/app/browser/window/tab/item/client/driver/gemini.rs @@ -357,8 +357,7 @@ fn handle( page.content.to_text_source(data) } else { match m.as_str() { - "text/gemini" => page.content.to_text_gemini(&page.profile, &uri, data), - "text/markdown" => page.content.to_text_markdown(&page, &uri, data), + "text/gemini" => page.content.to_text_gemini(&uri, data), "text/plain" => page.content.to_text_plain(data), _ => panic!() // unexpected } diff --git a/src/app/browser/window/tab/item/client/driver/nex.rs b/src/app/browser/window/tab/item/client/driver/nex.rs index 919c8869..da3b2231 100644 --- a/src/app/browser/window/tab/item/client/driver/nex.rs +++ b/src/app/browser/window/tab/item/client/driver/nex.rs @@ -299,7 +299,7 @@ fn render( } else if q.ends_with("/") { p.content.to_text_nex(&u, d) } else if q.ends_with(".gmi") || q.ends_with(".gemini") { - p.content.to_text_gemini(&p.profile, &u, d) + p.content.to_text_gemini(&u, d) } else { p.content.to_text_plain(d) }; diff --git a/src/app/browser/window/tab/item/page/content.rs b/src/app/browser/window/tab/item/page/content.rs index 2f6b9551..016121dd 100644 --- a/src/app/browser/window/tab/item/page/content.rs +++ b/src/app/browser/window/tab/item/page/content.rs @@ -7,8 +7,6 @@ use directory::Directory; use image::Image; use text::Text; -use crate::{app::browser::window::tab::item::page::Page, profile::Profile}; - use super::{ItemAction, TabAction, WindowAction}; use adw::StatusPage; use gtk::{ @@ -128,14 +126,9 @@ impl Content { } /// `text/gemini` - pub fn to_text_gemini(&self, profile: &Rc, base: &Uri, data: &str) -> Text { + pub fn to_text_gemini(&self, base: &Uri, data: &str) -> Text { self.clean(); - match Text::gemini( - (&self.window_action, &self.item_action), - profile, - base, - data, - ) { + match Text::gemini((&self.window_action, &self.item_action), base, data) { Ok(text) => { self.g_box.append(&text.scrolled_window); text @@ -161,14 +154,6 @@ impl Content { } } - /// `text/markdown` - pub fn to_text_markdown(&self, page: &Rc, base: &Uri, data: &str) -> Text { - self.clean(); - let m = Text::markdown((&self.window_action, &self.item_action), page, base, data); - self.g_box.append(&m.scrolled_window); - m - } - /// `text/plain` pub fn to_text_plain(&self, data: &str) -> Text { self.clean(); diff --git a/src/app/browser/window/tab/item/page/content/directory/column/format.rs b/src/app/browser/window/tab/item/page/content/directory/column/format.rs index 328048c0..ba027dfb 100644 --- a/src/app/browser/window/tab/item/page/content/directory/column/format.rs +++ b/src/app/browser/window/tab/item/page/content/directory/column/format.rs @@ -15,8 +15,6 @@ impl Format for FileInfo { if content_type == "text/plain" { if display_name.ends_with(".gmi") || display_name.ends_with(".gemini") { "text/gemini".into() - } else if display_name.ends_with(".md") || display_name.ends_with(".markdown") { - "text/markdown".into() } else { content_type } diff --git a/src/app/browser/window/tab/item/page/content/text.rs b/src/app/browser/window/tab/item/page/content/text.rs index f400591c..d47c9e65 100644 --- a/src/app/browser/window/tab/item/page/content/text.rs +++ b/src/app/browser/window/tab/item/page/content/text.rs @@ -1,16 +1,12 @@ mod gemini; -mod markdown; mod nex; mod plain; mod source; -use crate::{app::browser::window::tab::item::page::Page, profile::Profile}; - use super::{ItemAction, WindowAction}; use adw::ClampScrollable; use gemini::Gemini; use gtk::{ScrolledWindow, TextView, glib::Uri}; -use markdown::Markdown; use nex::Nex; use plain::Plain; use source::Source; @@ -29,11 +25,10 @@ pub struct Text { impl Text { pub fn gemini( actions: (&Rc, &Rc), - profile: &Rc, base: &Uri, gemtext: &str, ) -> Result)> { - match Gemini::build(actions, profile, base, gemtext) { + match Gemini::build(actions, base, gemtext) { Ok(widget) => Ok(Self { scrolled_window: reader(&widget.text_view), text_view: widget.text_view, @@ -56,22 +51,6 @@ impl Text { } } - pub fn markdown( - actions: (&Rc, &Rc), - page: &Rc, - base: &Uri, - gemtext: &str, - ) -> Self { - let markdown = Markdown::build(actions, page, base, gemtext); - Self { - scrolled_window: reader(&markdown.text_view), - text_view: markdown.text_view, - meta: Meta { - title: markdown.title, - }, - } - } - pub fn plain(data: &str) -> Self { let text_view = TextView::plain(data); Self { diff --git a/src/app/browser/window/tab/item/page/content/text/gemini.rs b/src/app/browser/window/tab/item/page/content/text/gemini.rs index ebb90175..a5e58a1c 100644 --- a/src/app/browser/window/tab/item/page/content/text/gemini.rs +++ b/src/app/browser/window/tab/item/page/content/text/gemini.rs @@ -5,23 +5,23 @@ mod icon; mod syntax; mod tag; -use super::{ItemAction, WindowAction}; -use crate::{app::browser::window::action::Position, profile::Profile}; pub use error::Error; +use gutter::Gutter; +use icon::Icon; +use syntax::Syntax; +use tag::Tag; + +use super::{ItemAction, WindowAction}; +use crate::app::browser::window::action::Position; use gtk::{ EventControllerMotion, GestureClick, TextBuffer, TextTag, TextView, TextWindowType, UriLauncher, Window, WrapMode, - gdk::{BUTTON_MIDDLE, BUTTON_PRIMARY, BUTTON_SECONDARY, Display, RGBA}, - gio::{Cancellable, Menu, SimpleAction, SimpleActionGroup}, - glib::{Uri, uuid_string_random}, - prelude::{PopoverExt, TextBufferExt, TextBufferExtManual, TextTagExt, TextViewExt, WidgetExt}, + gdk::{BUTTON_MIDDLE, BUTTON_PRIMARY, RGBA}, + gio::Cancellable, + glib::Uri, + prelude::{TextBufferExt, TextBufferExtManual, TextTagExt, TextViewExt, WidgetExt}, }; -use gutter::Gutter; -use icon::Icon; -use sourceview::prelude::{ActionExt, ActionMapExt, DisplayExt, ToVariant}; use std::{cell::Cell, collections::HashMap, rc::Rc}; -use syntax::Syntax; -use tag::Tag; pub const NEW_LINE: &str = "\n"; @@ -36,7 +36,6 @@ impl Gemini { /// Build new `Self` pub fn build( (window_action, item_action): (&Rc, &Rc), - profile: &Rc, base: &Uri, gemtext: &str, ) -> Result { @@ -151,7 +150,11 @@ impl Gemini { match syntax.highlight(&c.value, alt) { Ok(highlight) => { for (syntax_tag, entity) in highlight { - assert!(tag.text_tag_table.add(&syntax_tag)); + // Register new tag + if !tag.text_tag_table.add(&syntax_tag) { + todo!() + } + // Append tag to buffer buffer.insert_with_tags( &mut buffer.end_iter(), &entity, @@ -162,7 +165,11 @@ impl Gemini { Err(_) => { // Try ANSI/SGR format (terminal emulation) @TODO optional for (syntax_tag, entity) in ansi::format(&c.value) { - assert!(tag.text_tag_table.add(&syntax_tag)); + // Register new tag + if !tag.text_tag_table.add(&syntax_tag) { + todo!() + } + // Append tag to buffer buffer.insert_with_tags( &mut buffer.end_iter(), &entity, @@ -179,7 +186,7 @@ impl Gemini { // Skip other actions for this line continue; } - Err(_) => panic!(), + Err(_) => todo!(), } } } @@ -210,10 +217,10 @@ impl Gemini { // Is link if let Some(link) = ggemtext::line::Link::parse(line) { if let Some(uri) = link.uri(Some(base)) { - let mut alt = Vec::with_capacity(2); + let mut alt = Vec::new(); if uri.scheme() != base.scheme() { - alt.push(LINK_EXTERNAL_INDICATOR.to_string()); + alt.push("⇖".to_string()); } alt.push(match link.alt { @@ -228,7 +235,9 @@ impl Gemini { .wrap_mode(WrapMode::Word) .build(); - assert!(tag.text_tag_table.add(&a)); + if !tag.text_tag_table.add(&a) { + panic!() + } buffer.insert_with_tags(&mut buffer.end_iter(), &alt.join(" "), &[&a]); buffer.insert(&mut buffer.end_iter(), NEW_LINE); @@ -275,170 +284,14 @@ impl Gemini { buffer.insert(&mut buffer.end_iter(), NEW_LINE); } - // Context menu - let action_link_tab = - SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant()); - action_link_tab.connect_activate({ - let window_action = window_action.clone(); - move |this, _| { - open_link_in_new_tab( - &this.state().unwrap().get::().unwrap(), - &window_action, - ) - } - }); - let action_link_copy_url = - SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant()); - action_link_copy_url.connect_activate(|this, _| { - Display::default() - .unwrap() - .clipboard() - .set_text(&this.state().unwrap().get::().unwrap()) - }); - let action_link_copy_text = - SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant()); - action_link_copy_text.connect_activate(|this, _| { - Display::default() - .unwrap() - .clipboard() - .set_text(&this.state().unwrap().get::().unwrap()) - }); - let action_link_copy_text_selected = - SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant()); - action_link_copy_text_selected.connect_activate(|this, _| { - Display::default() - .unwrap() - .clipboard() - .set_text(&this.state().unwrap().get::().unwrap()) - }); - let action_link_bookmark = - SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant()); - action_link_bookmark.connect_activate({ - let p = profile.clone(); - move |this, _| { - let state = this.state().unwrap().get::().unwrap(); - p.bookmark.toggle(&state, None).unwrap(); - } - }); - let action_link_download = - SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant()); - action_link_download.connect_activate({ - let window_action = window_action.clone(); - move |this, _| { - open_link_in_new_tab( - &link_prefix( - this.state().unwrap().get::().unwrap(), - LINK_PREFIX_DOWNLOAD, - ), - &window_action, - ) - } - }); - let action_link_source = - SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant()); - action_link_source.connect_activate({ - let window_action = window_action.clone(); - move |this, _| { - open_link_in_new_tab( - &link_prefix( - this.state().unwrap().get::().unwrap(), - LINK_PREFIX_SOURCE, - ), - &window_action, - ) - } - }); - let link_context_group_id = uuid_string_random(); - text_view.insert_action_group( - &link_context_group_id, - Some(&{ - let g = SimpleActionGroup::new(); - g.add_action(&action_link_tab); - g.add_action(&action_link_copy_url); - g.add_action(&action_link_copy_text); - g.add_action(&action_link_copy_text_selected); - g.add_action(&action_link_bookmark); - g.add_action(&action_link_download); - g.add_action(&action_link_source); - g - }), - ); - let link_context = gtk::PopoverMenu::from_model(Some(&{ - let m = Menu::new(); - m.append( - Some("Open Link in New Tab"), - Some(&format!( - "{link_context_group_id}.{}", - action_link_tab.name() - )), - ); - m.append_section(None, &{ - let m_copy = Menu::new(); - m_copy.append( - Some("Copy Link URL"), - Some(&format!( - "{link_context_group_id}.{}", - action_link_copy_url.name() - )), - ); - m_copy.append( - Some("Copy Link Text"), - Some(&format!( - "{link_context_group_id}.{}", - action_link_copy_text.name() - )), - ); - m_copy.append( - Some("Copy Text Selected"), - Some(&format!( - "{link_context_group_id}.{}", - action_link_copy_text_selected.name() - )), - ); - m_copy - }); - m.append_section(None, &{ - let m_other = Menu::new(); - m_other.append( - Some("Bookmark Link"), // @TODO highlight state - Some(&format!( - "{link_context_group_id}.{}", - action_link_bookmark.name() - )), - ); - m_other.append( - Some("Download Link"), - Some(&format!( - "{link_context_group_id}.{}", - action_link_download.name() - )), - ); - m_other.append( - Some("View Link as Source"), - Some(&format!( - "{link_context_group_id}.{}", - action_link_source.name() - )), - ); - m_other - }); - m - })); - link_context.set_parent(&text_view); - // Init additional controllers - let middle_button_controller = GestureClick::builder().button(BUTTON_MIDDLE).build(); let primary_button_controller = GestureClick::builder().button(BUTTON_PRIMARY).build(); - let secondary_button_controller = GestureClick::builder() - .button(BUTTON_SECONDARY) - .propagation_phase(gtk::PropagationPhase::Capture) - .build(); + let middle_button_controller = GestureClick::builder().button(BUTTON_MIDDLE).build(); let motion_controller = EventControllerMotion::new(); + text_view.add_controller(primary_button_controller.clone()); text_view.add_controller(middle_button_controller.clone()); text_view.add_controller(motion_controller.clone()); - text_view.add_controller(primary_button_controller.clone()); - text_view.add_controller(secondary_button_controller.clone()); // Init shared reference container for HashTable collected let links = Rc::new(links); @@ -455,92 +308,27 @@ impl Gemini { window_x as i32, window_y as i32, ); + if let Some(iter) = text_view.iter_at_location(buffer_x, buffer_y) { for tag in iter.tags() { // Tag is link if let Some(uri) = links.get(&tag) { - return open_link_in_current_tab(&uri.to_string(), &item_action); - } - } - } - } - }); - - secondary_button_controller.connect_pressed({ - let links = links.clone(); - let text_view = text_view.clone(); - let link_context = link_context.clone(); - move |_, _, window_x, window_y| { - let x = window_x as i32; - let y = window_y as i32; - // Detect tag match current coords hovered - let (buffer_x, buffer_y) = - text_view.window_to_buffer_coords(TextWindowType::Widget, x, y); - if let Some(iter) = text_view.iter_at_location(buffer_x, buffer_y) { - for tag in iter.tags() { - // Tag is link - if let Some(uri) = links.get(&tag) { - let request_str = uri.to_str(); - let request_var = request_str.to_variant(); - let is_prefix_link = is_prefix_link(&request_str); - - // Open in the new tab - action_link_tab.set_state(&request_var); - action_link_tab.set_enabled(!request_str.is_empty()); - - // Copy link to the clipboard - action_link_copy_url.set_state(&request_var); - action_link_copy_url.set_enabled(!request_str.is_empty()); - - // Copy link text - { - let mut start_iter = iter; - let mut end_iter = iter; - if !start_iter.starts_tag(Some(&tag)) { - start_iter.backward_to_tag_toggle(Some(&tag)); + // Select link handler by scheme + return match uri.scheme().as_str() { + "gemini" | "titan" | "nex" | "file" => { + item_action.load.activate(Some(&uri.to_str()), true, false) } - if !end_iter.ends_tag(Some(&tag)) { - end_iter.forward_to_tag_toggle(Some(&tag)); - } - let tagged_text = text_view - .buffer() - .text(&start_iter, &end_iter, false) - .replace(LINK_EXTERNAL_INDICATOR, "") - .trim() - .to_string(); - - action_link_copy_text.set_state(&tagged_text.to_variant()); - action_link_copy_text.set_enabled(!tagged_text.is_empty()); - } - - // Copy link text (if) selected - action_link_copy_text_selected.set_enabled( - if let Some((start, end)) = buffer.selection_bounds() { - let selected = buffer.text(&start, &end, false); - action_link_copy_text_selected - .set_state(&selected.to_variant()); - !selected.is_empty() - } else { - false - }, - ); - - // Bookmark - action_link_bookmark.set_state(&request_var); - action_link_bookmark.set_enabled(is_prefix_link); - - // Download (new tab) - action_link_download.set_state(&request_var); - action_link_download.set_enabled(is_prefix_link); - - // View as Source (new tab) - action_link_source.set_state(&request_var); - action_link_source.set_enabled(is_prefix_link); - - // Toggle - link_context - .set_pointing_to(Some(>k::gdk::Rectangle::new(x, y, 1, 1))); - link_context.popup() + // Scheme not supported, delegate + _ => UriLauncher::new(&uri.to_str()).launch( + Window::NONE, + Cancellable::NONE, + |result| { + if let Err(e) = result { + println!("{e}") + } + }, + ), + }; // @TODO common handler? } } } @@ -562,7 +350,30 @@ impl Gemini { for tag in iter.tags() { // Tag is link if let Some(uri) = links.get(&tag) { - return open_link_in_new_tab(&uri.to_string(), &window_action); + // Select link handler by scheme + return match uri.scheme().as_str() { + "gemini" | "titan" | "nex" | "file" => { + // Open new page in browser + window_action.append.activate_stateful_once( + Position::After, + Some(uri.to_string()), + false, + false, + true, + true, + ); + } + // Scheme not supported, delegate + _ => UriLauncher::new(&uri.to_str()).launch( + Window::NONE, + Cancellable::NONE, + |result| { + if let Err(e) = result { + println!("{e}") + } + }, + ), + }; // @TODO common handler? } } } @@ -621,59 +432,3 @@ impl Gemini { } } } - -fn is_internal_link(request: &str) -> bool { - // schemes - request.starts_with("gemini://") - || request.starts_with("titan://") - || request.starts_with("nex://") - || request.starts_with("file://") - // prefix - || request.starts_with("download:") - || request.starts_with("source:") -} - -fn is_prefix_link(request: &str) -> bool { - request.starts_with("gemini://") - || request.starts_with("nex://") - || request.starts_with("file://") -} - -fn open_link_in_external_app(request: &str) { - UriLauncher::new(request).launch(Window::NONE, Cancellable::NONE, |r| { - if let Err(e) = r { - println!("{e}") // @TODO use warn macro - } - }) -} - -fn open_link_in_current_tab(request: &str, item_action: &ItemAction) { - if is_internal_link(request) { - item_action.load.activate(Some(request), true, false) - } else { - open_link_in_external_app(request) - } -} - -fn open_link_in_new_tab(request: &str, window_action: &WindowAction) { - if is_internal_link(request) { - window_action.append.activate_stateful_once( - Position::After, - Some(request.into()), - false, - false, - true, - true, - ); - } else { - open_link_in_external_app(request) - } -} - -fn link_prefix(request: String, prefix: &str) -> String { - format!("{prefix}{}", request.trim_start_matches(prefix)) -} - -const LINK_EXTERNAL_INDICATOR: &str = "⇖"; -const LINK_PREFIX_DOWNLOAD: &str = "download:"; -const LINK_PREFIX_SOURCE: &str = "source:"; diff --git a/src/app/browser/window/tab/item/page/content/text/markdown.rs b/src/app/browser/window/tab/item/page/content/text/markdown.rs deleted file mode 100644 index e845bc0b..00000000 --- a/src/app/browser/window/tab/item/page/content/text/markdown.rs +++ /dev/null @@ -1,610 +0,0 @@ -mod gutter; -mod tags; - -use super::{ItemAction, WindowAction}; -use crate::app::browser::window::{action::Position, tab::item::page::Page}; -use gtk::{ - EventControllerMotion, GestureClick, PopoverMenu, TextBuffer, TextTag, TextTagTable, TextView, - TextWindowType, UriLauncher, Window, WrapMode, - gdk::{BUTTON_MIDDLE, BUTTON_PRIMARY, BUTTON_SECONDARY, Display, RGBA}, - gio::{Cancellable, Menu, SimpleAction, SimpleActionGroup}, - glib::{ControlFlow, GString, Uri, idle_add_local, uuid_string_random}, - prelude::{EditableExt, PopoverExt, TextBufferExt, TextTagExt, TextViewExt, WidgetExt}, -}; -use gutter::Gutter; -use regex::Regex; -use sourceview::prelude::{ActionExt, ActionMapExt, DisplayExt, ToVariant}; -use std::{cell::Cell, collections::HashMap, rc::Rc}; -use strip_tags::*; -use tags::Tags; - -pub struct Markdown { - pub title: Option, - pub text_view: TextView, -} - -impl Markdown { - // Constructors - - /// Build new `Self` - pub fn build( - (window_action, item_action): (&Rc, &Rc), - page: &Rc, - base: &Uri, - markdown: &str, - ) -> Self { - // Init HashMap storage (for event controllers) - let mut links: HashMap = HashMap::new(); - let mut headers: HashMap = HashMap::new(); - - // Init hovered tag storage for `links` - // * maybe less expensive than update entire HashMap by iter - let hover: Rc>> = Rc::new(Cell::new(None)); - - // Init colors - // @TODO use accent colors in adw 1.6 / ubuntu 24.10+ - let link_color = ( - RGBA::new(0.208, 0.518, 0.894, 1.0), - RGBA::new(0.208, 0.518, 0.894, 0.9), - ); - - // Init tags - let mut tags = Tags::new(); - - // Init new text buffer - let buffer = TextBuffer::new(Some(&TextTagTable::new())); - buffer.set_text( - Regex::new(r"\n{3,}") - .unwrap() - .replace_all(&strip_tags(markdown), "\n\n") - .trim(), - ); // @TODO extract `` tags? - - // Init main widget - let text_view = { - const MARGIN: i32 = 8; - TextView::builder() - .bottom_margin(MARGIN) - .buffer(&buffer) - .cursor_visible(false) - .editable(false) - .left_margin(MARGIN) - .right_margin(MARGIN) - .top_margin(MARGIN) - .vexpand(true) - .wrap_mode(WrapMode::Word) - .build() - }; - - // Init gutter widget (the tooltip on URL tags hover) - let gutter = Gutter::build(&text_view); - - // Render markdown tags - let title = tags.render(&text_view, base, &link_color.0, &mut links, &mut headers); - - // Headers context menu (fragment capture) - let action_header_copy_url = - SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant()); - action_header_copy_url.connect_activate(|this, _| { - Display::default() - .unwrap() - .clipboard() - .set_text(&this.state().unwrap().get::().unwrap()) - }); - let action_header_copy_text = - SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant()); - action_header_copy_text.connect_activate(|this, _| { - Display::default() - .unwrap() - .clipboard() - .set_text(&this.state().unwrap().get::().unwrap()) - }); - let action_header_copy_text_selected = - SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant()); - action_header_copy_text_selected.connect_activate(|this, _| { - Display::default() - .unwrap() - .clipboard() - .set_text(&this.state().unwrap().get::().unwrap()) - }); - let header_context_group_id = uuid_string_random(); - text_view.insert_action_group( - &header_context_group_id, - Some(&{ - let g = SimpleActionGroup::new(); - g.add_action(&action_header_copy_url); - g.add_action(&action_header_copy_text); - g.add_action(&action_header_copy_text_selected); - g - }), - ); - let header_context = PopoverMenu::from_model(Some(&{ - let m = Menu::new(); - m.append( - Some("Copy Header Link"), - Some(&format!( - "{header_context_group_id}.{}", - action_header_copy_url.name() - )), - ); - m.append( - Some("Copy Header Text"), - Some(&format!( - "{header_context_group_id}.{}", - action_header_copy_text.name() - )), - ); - m.append( - Some("Copy Text Selected"), - Some(&format!( - "{header_context_group_id}.{}", - action_header_copy_text_selected.name() - )), - ); - m - })); - header_context.set_parent(&text_view); - - // Link context menu - let action_link_tab = - SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant()); - action_link_tab.connect_activate({ - let window_action = window_action.clone(); - move |this, _| { - open_link_in_new_tab( - &this.state().unwrap().get::().unwrap(), - &window_action, - ) - } - }); - let action_link_copy_url = - SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant()); - action_link_copy_url.connect_activate(|this, _| { - Display::default() - .unwrap() - .clipboard() - .set_text(&this.state().unwrap().get::().unwrap()) - }); - let action_link_copy_text = - SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant()); - action_link_copy_text.connect_activate(|this, _| { - Display::default() - .unwrap() - .clipboard() - .set_text(&this.state().unwrap().get::().unwrap()) - }); - let action_link_copy_text_selected = - SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant()); - action_link_copy_text_selected.connect_activate(|this, _| { - Display::default() - .unwrap() - .clipboard() - .set_text(&this.state().unwrap().get::().unwrap()) - }); - let action_link_bookmark = - SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant()); - action_link_bookmark.connect_activate({ - let p = page.profile.clone(); - move |this, _| { - let state = this.state().unwrap().get::().unwrap(); - p.bookmark.toggle(&state, None).unwrap(); - } - }); - let action_link_download = - SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant()); - action_link_download.connect_activate({ - let window_action = window_action.clone(); - move |this, _| { - open_link_in_new_tab( - &link_prefix( - this.state().unwrap().get::().unwrap(), - LINK_PREFIX_DOWNLOAD, - ), - &window_action, - ) - } - }); - let action_link_source = - SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant()); - action_link_source.connect_activate({ - let window_action = window_action.clone(); - move |this, _| { - open_link_in_new_tab( - &link_prefix( - this.state().unwrap().get::().unwrap(), - LINK_PREFIX_SOURCE, - ), - &window_action, - ) - } - }); - let link_context_group_id = uuid_string_random(); - text_view.insert_action_group( - &link_context_group_id, - Some(&{ - let g = SimpleActionGroup::new(); - g.add_action(&action_link_tab); - g.add_action(&action_link_copy_url); - g.add_action(&action_link_copy_text); - g.add_action(&action_link_copy_text_selected); - g.add_action(&action_link_bookmark); - g.add_action(&action_link_download); - g.add_action(&action_link_source); - g - }), - ); - let link_context = PopoverMenu::from_model(Some(&{ - let m = Menu::new(); - m.append( - Some("Open Link in New Tab"), - Some(&format!( - "{link_context_group_id}.{}", - action_link_tab.name() - )), - ); - m.append_section(None, &{ - let m_copy = Menu::new(); - m_copy.append( - Some("Copy Link URL"), - Some(&format!( - "{link_context_group_id}.{}", - action_link_copy_url.name() - )), - ); - m_copy.append( - Some("Copy Link Text"), - Some(&format!( - "{link_context_group_id}.{}", - action_link_copy_text.name() - )), - ); - m_copy.append( - Some("Copy Text Selected"), - Some(&format!( - "{link_context_group_id}.{}", - action_link_copy_text_selected.name() - )), - ); - m_copy - }); - m.append_section(None, &{ - let m_other = Menu::new(); - m_other.append( - Some("Bookmark Link"), // @TODO highlight state - Some(&format!( - "{link_context_group_id}.{}", - action_link_bookmark.name() - )), - ); - m_other.append( - Some("Download Link"), - Some(&format!( - "{link_context_group_id}.{}", - action_link_download.name() - )), - ); - m_other.append( - Some("View Link as Source"), - Some(&format!( - "{link_context_group_id}.{}", - action_link_source.name() - )), - ); - m_other - }); - m - })); - link_context.set_parent(&text_view); - - // Init additional controllers - let middle_button_controller = GestureClick::builder().button(BUTTON_MIDDLE).build(); - let primary_button_controller = GestureClick::builder().button(BUTTON_PRIMARY).build(); - let secondary_button_controller = GestureClick::builder() - .button(BUTTON_SECONDARY) - .propagation_phase(gtk::PropagationPhase::Capture) - .build(); - let motion_controller = EventControllerMotion::new(); - - text_view.add_controller(middle_button_controller.clone()); - text_view.add_controller(motion_controller.clone()); - text_view.add_controller(primary_button_controller.clone()); - text_view.add_controller(secondary_button_controller.clone()); - - // Init shared reference container for HashTable collected - let links = Rc::new(links); - let headers = Rc::new(headers); - - // Init events - primary_button_controller.connect_released({ - let headers = headers.clone(); - let item_action = item_action.clone(); - let links = links.clone(); - let page = page.clone(); - let text_view = text_view.clone(); - move |_, _, window_x, window_y| { - // Detect tag match current coords hovered - let (buffer_x, buffer_y) = text_view.window_to_buffer_coords( - TextWindowType::Widget, - window_x as i32, - window_y as i32, - ); - if let Some(iter) = text_view.iter_at_location(buffer_x, buffer_y) { - for tag in iter.tags() { - // Tag is link - if let Some(uri) = links.get(&tag) { - return if let Some(fragment) = uri.fragment() { - scroll_to_anchor(&page, &text_view, &headers, fragment); - } else { - open_link_in_current_tab(&uri.to_string(), &item_action); - }; - } - } - } - } - }); - - secondary_button_controller.connect_pressed({ - let headers = headers.clone(); - let link_context = link_context.clone(); - let links = links.clone(); - let text_view = text_view.clone(); - move |_, _, window_x, window_y| { - let x = window_x as i32; - let y = window_y as i32; - // Detect tag match current coords hovered - let (buffer_x, buffer_y) = - text_view.window_to_buffer_coords(TextWindowType::Widget, x, y); - if let Some(iter) = text_view.iter_at_location(buffer_x, buffer_y) { - for tag in iter.tags() { - // Tag is link - if let Some(uri) = links.get(&tag) { - let request_str = uri.to_str(); - let request_var = request_str.to_variant(); - let is_prefix_link = is_prefix_link(&request_str); - - // Open in the new tab - action_link_tab.set_state(&request_var); - action_link_tab.set_enabled(!request_str.is_empty()); - - // Copy link to the clipboard - action_link_copy_url.set_state(&request_var); - action_link_copy_url.set_enabled(!request_str.is_empty()); - - // Copy link text - { - let mut start_iter = iter; - let mut end_iter = iter; - if !start_iter.starts_tag(Some(&tag)) { - start_iter.backward_to_tag_toggle(Some(&tag)); - } - if !end_iter.ends_tag(Some(&tag)) { - end_iter.forward_to_tag_toggle(Some(&tag)); - } - let tagged_text = text_view - .buffer() - .text(&start_iter, &end_iter, false) - .replace(LINK_EXTERNAL_INDICATOR, "") - .trim() - .to_string(); - - action_link_copy_text.set_state(&tagged_text.to_variant()); - action_link_copy_text.set_enabled(!tagged_text.is_empty()); - } - - // Copy link text (if) selected - action_link_copy_text_selected.set_enabled( - if let Some((start, end)) = buffer.selection_bounds() { - let selected = buffer.text(&start, &end, false); - action_link_copy_text_selected - .set_state(&selected.to_variant()); - !selected.is_empty() - } else { - false - }, - ); - - // Bookmark - action_link_bookmark.set_state(&request_var); - action_link_bookmark.set_enabled(is_prefix_link); - - // Download (new tab) - action_link_download.set_state(&request_var); - action_link_download.set_enabled(is_prefix_link); - - // View as Source (new tab) - action_link_source.set_state(&request_var); - action_link_source.set_enabled(is_prefix_link); - - // Toggle - link_context - .set_pointing_to(Some(>k::gdk::Rectangle::new(x, y, 1, 1))); - link_context.popup() - } - // Tag is header - if let Some((title, uri)) = headers.get(&tag) { - let request_str = uri.to_str(); - let request_var = request_str.to_variant(); - - // Copy link to the clipboard - action_header_copy_url.set_state(&request_var); - action_header_copy_url.set_enabled(!request_str.is_empty()); - - // Copy header text - action_header_copy_text.set_state(&title.to_variant()); - action_header_copy_text.set_enabled(!title.is_empty()); - - // Copy header text (if) selected - action_header_copy_text_selected.set_enabled( - if let Some((start, end)) = buffer.selection_bounds() { - let selected = buffer.text(&start, &end, false); - action_header_copy_text_selected - .set_state(&selected.to_variant()); - !selected.is_empty() - } else { - false - }, - ); - - // Toggle - header_context - .set_pointing_to(Some(>k::gdk::Rectangle::new(x, y, 1, 1))); - header_context.popup() - } - } - } - } - }); - - middle_button_controller.connect_pressed({ - let links = links.clone(); - let text_view = text_view.clone(); - let window_action = window_action.clone(); - move |_, _, window_x, window_y| { - // Detect tag match current coords hovered - let (buffer_x, buffer_y) = text_view.window_to_buffer_coords( - TextWindowType::Widget, - window_x as i32, - window_y as i32, - ); - if let Some(iter) = text_view.iter_at_location(buffer_x, buffer_y) { - for tag in iter.tags() { - // Tag is link - if let Some(uri) = links.get(&tag) { - return open_link_in_new_tab(&uri.to_string(), &window_action); - } - } - } - } - }); // for a note: this action sensitive to focus out - - motion_controller.connect_motion({ - let text_view = text_view.clone(); - let links = links.clone(); - let hover = hover.clone(); - move |_, window_x, window_y| { - // Detect tag match current coords hovered - let (buffer_x, buffer_y) = text_view.window_to_buffer_coords( - TextWindowType::Widget, - window_x as i32, - window_y as i32, - ); - // Reset link colors to default - if let Some(tag) = hover.replace(None) { - tag.set_foreground_rgba(Some(&link_color.0)); - } - // Apply hover effect - if let Some(iter) = text_view.iter_at_location(buffer_x, buffer_y) { - for tag in iter.tags() { - // Tag is link - if let Some(uri) = links.get(&tag) { - // Toggle color - tag.set_foreground_rgba(Some(&link_color.1)); - // Keep hovered tag in memory - hover.replace(Some(tag.clone())); - // Show tooltip - gutter.set_uri(Some(uri)); - // Toggle cursor - text_view.set_cursor_from_name(Some("pointer")); - // Redraw required to apply changes immediately - text_view.queue_draw(); - return; - } - } - } - // Restore defaults - gutter.set_uri(None); - text_view.set_cursor_from_name(Some("text")); - text_view.queue_draw(); - } - }); // @TODO may be expensive for CPU, add timeout? - - // Anchor auto-scroll behavior - idle_add_local({ - let base = base.clone(); - let page = page.clone(); - let text_view = text_view.clone(); - move || { - if let Some(fragment) = base.fragment() { - scroll_to_anchor(&page, &text_view, &headers, fragment); - } - ControlFlow::Break - } - }); - - Self { text_view, title } - } -} - -fn scroll_to_anchor( - page: &Rc, - text_view: &TextView, - headers: &HashMap, - fragment: GString, -) { - if let Some((tag, (_, uri))) = headers.iter().find(|(_, (_, uri))| { - uri.fragment() - .is_some_and(|f| fragment == tags::format_header_fragment(&f)) - }) { - let mut iter = text_view.buffer().start_iter(); - if iter.starts_tag(Some(tag)) || iter.forward_to_tag_toggle(Some(tag)) { - text_view.scroll_to_iter(&mut iter, 0.0, true, 0.0, 0.0); - } - page.navigation.request.entry.set_text(&uri.to_string()) - } -} - -fn is_internal_link(request: &str) -> bool { - // schemes - request.starts_with("gemini://") - || request.starts_with("titan://") - || request.starts_with("nex://") - || request.starts_with("file://") - // prefix - || request.starts_with("download:") - || request.starts_with("source:") -} - -fn is_prefix_link(request: &str) -> bool { - request.starts_with("gemini://") - || request.starts_with("nex://") - || request.starts_with("file://") -} - -fn open_link_in_external_app(request: &str) { - UriLauncher::new(request).launch(Window::NONE, Cancellable::NONE, |r| { - if let Err(e) = r { - println!("{e}") // @TODO use warn macro - } - }) -} - -fn open_link_in_current_tab(request: &str, item_action: &ItemAction) { - if is_internal_link(request) { - item_action.load.activate(Some(request), true, false) - } else { - open_link_in_external_app(request) - } -} - -fn open_link_in_new_tab(request: &str, window_action: &WindowAction) { - if is_internal_link(request) { - window_action.append.activate_stateful_once( - Position::After, - Some(request.into()), - false, - false, - true, - true, - ); - } else { - open_link_in_external_app(request) - } -} - -fn link_prefix(request: String, prefix: &str) -> String { - format!("{prefix}{}", request.trim_start_matches(prefix)) -} - -const LINK_EXTERNAL_INDICATOR: &str = "⇖"; -const LINK_PREFIX_DOWNLOAD: &str = "download:"; -const LINK_PREFIX_SOURCE: &str = "source:"; diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/gutter.rs b/src/app/browser/window/tab/item/page/content/text/markdown/gutter.rs deleted file mode 100644 index 6a558ef2..00000000 --- a/src/app/browser/window/tab/item/page/content/text/markdown/gutter.rs +++ /dev/null @@ -1,68 +0,0 @@ -use gtk::{ - Align, Label, TextView, TextWindowType, - glib::{Uri, timeout_add_local_once}, - pango::EllipsizeMode, - prelude::{TextViewExt, WidgetExt}, -}; -use std::{cell::Cell, rc::Rc, time::Duration}; - -pub struct Gutter { - pub label: Label, - is_active: Rc>, -} - -impl Gutter { - pub fn build(text_view: &TextView) -> Self { - const MARGIN_X: i32 = 8; - const MARGIN_Y: i32 = 2; - let label = Label::builder() - .css_classes(["caption", "dim-label"]) - .ellipsize(EllipsizeMode::Middle) - .halign(Align::Start) - .margin_bottom(MARGIN_Y) - .margin_end(MARGIN_X) - .margin_start(MARGIN_X) - .margin_top(MARGIN_Y) - .visible(false) - .build(); - - text_view.set_gutter(TextWindowType::Bottom, Some(&label)); - text_view - .gutter(TextWindowType::Bottom) - .unwrap() - .set_css_classes(&["view"]); // @TODO unspecified patch - - Self { - is_active: Rc::new(Cell::new(false)), - label, - } - } - - pub fn set_uri(&self, uri: Option<&Uri>) { - match uri { - Some(uri) => { - if !self.label.is_visible() { - if !self.is_active.replace(true) { - timeout_add_local_once(Duration::from_millis(250), { - let label = self.label.clone(); - let is_active = self.is_active.clone(); - let uri = uri.clone(); - move || { - if is_active.replace(false) { - label.set_label(&uri.to_string()); - label.set_visible(true) - } - } - }); - } - } else { - self.label.set_label(&uri.to_string()) - } - } - None => { - self.is_active.replace(false); - self.label.set_visible(false) - } - } - } -} diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/parser.rs b/src/app/browser/window/tab/item/page/content/text/markdown/parser.rs deleted file mode 100644 index fd708509..00000000 --- a/src/app/browser/window/tab/item/page/content/text/markdown/parser.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod code; -pub mod header; -pub mod link; -pub mod list; -pub mod quote; diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tags.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tags.rs deleted file mode 100644 index ece2ccf5..00000000 --- a/src/app/browser/window/tab/item/page/content/text/markdown/tags.rs +++ /dev/null @@ -1,139 +0,0 @@ -mod bold; -mod code; -mod header; -mod hr; -mod italic; -mod list; -mod pre; -mod quote; -mod reference; -mod strike; -mod underline; - -use bold::Bold; -use code::Code; -use gtk::{ - TextSearchFlags, TextTag, TextView, - gdk::RGBA, - glib::{GString, Uri}, - prelude::{TextBufferExt, TextViewExt}, -}; -use header::Header; -use italic::Italic; -use pre::Pre; -use quote::Quote; -use std::collections::HashMap; -use strike::Strike; -use underline::Underline; - -pub struct Tags { - pub bold: Bold, - pub code: Code, - pub header: Header, - pub italic: Italic, - pub pre: Pre, - pub quote: Quote, - pub strike: Strike, - pub underline: Underline, -} - -impl Default for Tags { - fn default() -> Self { - Self::new() - } -} - -impl Tags { - // Construct - pub fn new() -> Self { - Self { - bold: Bold::new(), - code: Code::new(), - header: Header::new(), - italic: Italic::new(), - pre: Pre::new(), - quote: Quote::new(), - strike: Strike::new(), - underline: Underline::new(), - } - } - pub fn render( - &mut self, - text_view: &TextView, - base: &Uri, - link_color: &RGBA, - links: &mut HashMap, - headers: &mut HashMap, - ) -> Option { - let buffer = text_view.buffer(); - - // Collect all code blocks first, - // and temporarily replace them with placeholder ID - self.code.collect(&buffer); - - // Keep in order! - let title = self.header.render(&buffer, base, headers); - - list::render(&buffer); - - self.quote.render(&buffer); - - self.bold.render(&buffer); - self.italic.render(&buffer); - self.pre.render(&buffer); - self.strike.render(&buffer); - self.underline.render(&buffer); - - reference::render(&buffer, base, link_color, links); - hr::render(text_view); - - // Cleanup unformatted escape chars - for e in ESCAPE_ENTRIES { - let mut cursor = buffer.start_iter(); - while let Some((mut match_start, mut match_end)) = - cursor.forward_search(e, TextSearchFlags::CASE_INSENSITIVE, None) - { - if match_end.backward_cursor_positions(1) { - buffer.delete(&mut match_start, &mut match_end) - } - cursor = match_end; - } - } - - // Render placeholders - self.code.render(&buffer); - - // Format document title string - title.map(|mut s| { - s = bold::strip_tags(&s); - s = hr::strip_tags(&s); - s = italic::strip_tags(&s); - s = pre::strip_tags(&s); - s = reference::strip_tags(&s); - s = strike::strip_tags(&s); - s = underline::strip_tags(&s); - for e in ESCAPE_ENTRIES { - s = s.replace(e, &e[1..]); - } - s - }) - } -} - -/// Shared URL #fragment logic (for the Header tags ref) -pub fn format_header_fragment(value: &str) -> GString { - Uri::escape_string(&value.to_lowercase().replace(" ", "-"), None, true) -} - -const ESCAPE_ENTRIES: &[&str] = &[ - "\\\n", "\\\\", "\\>", "\\`", "\\!", "\\[", "\\]", "\\(", "\\)", "\\*", "\\#", "\\~", "\\_", - "\\-", -]; -#[test] -fn test_escape_entries() { - let mut set = std::collections::HashSet::new(); - for e in ESCAPE_ENTRIES { - assert_eq!(e.len(), 2); - assert!(set.insert(*e)) - } -} diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tags/bold.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tags/bold.rs deleted file mode 100644 index 013f930a..00000000 --- a/src/app/browser/window/tab/item/page/content/text/markdown/tags/bold.rs +++ /dev/null @@ -1,104 +0,0 @@ -use gtk::{ - TextBuffer, TextTag, - WrapMode::Word, - prelude::{TextBufferExt, TextBufferExtManual}, -}; -use regex::Regex; - -const REGEX_BOLD: &str = r"(\*\*|__)(?P[^\*_]*)(\*\*|__)"; - -pub struct Bold(TextTag); - -impl Bold { - pub fn new() -> Self { - Self(TextTag::builder().weight(600).wrap_mode(Word).build()) - } - - /// Apply **bold**/__bold__ `Tag` to given `TextBuffer` - pub fn render(&self, buffer: &TextBuffer) { - assert!(buffer.tag_table().add(&self.0)); - - let (start, end) = buffer.bounds(); - let full_content = buffer.text(&start, &end, true).to_string(); - - let matches: Vec<_> = Regex::new(REGEX_BOLD) - .unwrap() - .captures_iter(&full_content) - .collect(); - - for cap in matches.into_iter().rev() { - let full_match = cap.get(0).unwrap(); - - let start_char_offset = full_content[..full_match.start()].chars().count() as i32; - let end_char_offset = full_content[..full_match.end()].chars().count() as i32; - - let mut start_iter = buffer.iter_at_offset(start_char_offset); - let mut end_iter = buffer.iter_at_offset(end_char_offset); - - if start_char_offset > 0 - && buffer - .text( - &buffer.iter_at_offset(start_char_offset - 1), - &end_iter, - false, - ) - .contains("\\") - { - continue; - } - - let mut tags = start_iter.tags(); - tags.push(self.0.clone()); - - buffer.delete(&mut start_iter, &mut end_iter); - buffer.insert_with_tags( - &mut start_iter, - &cap["text"], - &tags.iter().collect::>(), - ) - } - } -} - -pub fn strip_tags(value: &str) -> String { - let mut result = String::from(value); - for cap in Regex::new(REGEX_BOLD).unwrap().captures_iter(value) { - if let Some(m) = cap.get(0) { - result = result.replace(m.as_str(), &cap["text"]); - } - } - result -} - -#[test] -fn test_strip_tags() { - const VALUE: &str = - "Some **bold 1** and **bold 2** and __bold 3__ and *italic 1* and _italic 2_"; - let mut result = String::from(VALUE); - for cap in Regex::new(REGEX_BOLD).unwrap().captures_iter(VALUE) { - if let Some(m) = cap.get(0) { - result = result.replace(m.as_str(), &cap["text"]); - } - } - assert_eq!( - result, - "Some bold 1 and bold 2 and bold 3 and *italic 1* and _italic 2_" - ) -} - -#[test] -fn test_regex() { - let cap: Vec<_> = Regex::new(REGEX_BOLD) - .unwrap() - .captures_iter( - "Some **bold 1** and **bold 2** and __bold 3__ and *italic 1* and _italic 2_", - ) - .collect(); - - assert_eq!(cap.len(), 3); - - let mut c = cap.into_iter(); - assert_eq!(&c.next().unwrap()["text"], "bold 1"); - assert_eq!(&c.next().unwrap()["text"], "bold 2"); - assert_eq!(&c.next().unwrap()["text"], "bold 3"); -} diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tags/code.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tags/code.rs deleted file mode 100644 index 5d79041f..00000000 --- a/src/app/browser/window/tab/item/page/content/text/markdown/tags/code.rs +++ /dev/null @@ -1,128 +0,0 @@ -mod ansi; -mod syntax; - -use gtk::{ - TextBuffer, TextSearchFlags, TextTag, WrapMode, - glib::{GString, uuid_string_random}, - prelude::{TextBufferExt, TextBufferExtManual}, -}; -use regex::Regex; -use std::collections::HashMap; -use syntax::Syntax; - -const REGEX_CODE: &str = r"(?s)```[ \t]*(?P.*?)\n(?P.*?)```"; - -struct Entry { - alt: Option, - data: String, -} - -pub struct Code { - index: HashMap, - alt: TextTag, -} - -impl Code { - pub fn new() -> Self { - Self { - index: HashMap::new(), - alt: TextTag::builder() - .pixels_above_lines(4) - .pixels_below_lines(8) - .weight(500) - .wrap_mode(WrapMode::None) - .build(), - } - } - - /// Collect all code blocks into `Self.index` (to prevent formatting) - pub fn collect(&mut self, buffer: &TextBuffer) { - let (start, end) = buffer.bounds(); - let full_content = buffer.text(&start, &end, true).to_string(); - - let matches: Vec<_> = Regex::new(REGEX_CODE) - .unwrap() - .captures_iter(&full_content) - .collect(); - - for cap in matches.into_iter().rev() { - let id = uuid_string_random(); - - let full_match = cap.get(0).unwrap(); - - let start_char_offset = full_content[..full_match.start()].chars().count() as i32; - let end_char_offset = full_content[..full_match.end()].chars().count() as i32; - - let mut start_iter = buffer.iter_at_offset(start_char_offset); - let mut end_iter = buffer.iter_at_offset(end_char_offset); - - buffer.delete(&mut start_iter, &mut end_iter); - - buffer.insert_with_tags(&mut start_iter, &id, &[]); - assert!( - self.index - .insert( - id, - Entry { - alt: alt(cap["alt"].into()).map(|s| s.into()), - data: cap["data"].into(), - }, - ) - .is_none() - ) - } - } - - /// Apply code `Tag` to given `TextBuffer` using `Self.index` - pub fn render(&mut self, buffer: &TextBuffer) { - let syntax = Syntax::new(); - assert!(buffer.tag_table().add(&self.alt)); - for (k, v) in self.index.iter() { - while let Some((mut m_start, mut m_end)) = - buffer - .start_iter() - .forward_search(k, TextSearchFlags::VISIBLE_ONLY, None) - { - buffer.delete(&mut m_start, &mut m_end); - if let Some(ref alt) = v.alt { - buffer.insert_with_tags(&mut m_start, &format!("{alt}\n"), &[&self.alt]) - } - match syntax.highlight(&v.data, v.alt.as_ref()) { - Ok(highlight) => { - for (syntax_tag, entity) in highlight { - assert!(buffer.tag_table().add(&syntax_tag)); - buffer.insert_with_tags(&mut m_start, &entity, &[&syntax_tag]) - } - } - Err(_) => { - // Try ANSI/SGR format (terminal emulation) @TODO optional - for (syntax_tag, entity) in ansi::format(&v.data) { - assert!(buffer.tag_table().add(&syntax_tag)); - buffer.insert_with_tags(&mut m_start, &entity, &[&syntax_tag]) - } - } - } - } - } - } -} - -fn alt(value: Option<&str>) -> Option<&str> { - value.map(|m| m.trim()).filter(|s| !s.is_empty()) -} - -#[test] -fn test_regex() { - let cap: Vec<_> = Regex::new(REGEX_CODE) - .unwrap() - .captures_iter("Some ``` alt text\ncode line 1\ncode line 2``` and ```\ncode line 3\ncode line 4``` with ![img](https://link.com)") - .collect(); - - let first = cap.first().unwrap(); - assert_eq!(alt(first.name("alt").map(|m| m.as_str())), Some("alt text")); - assert_eq!(&first["data"], "code line 1\ncode line 2"); - - let second = cap.get(1).unwrap(); - assert_eq!(alt(second.name("alt").map(|m| m.as_str())), None); - assert_eq!(&second["data"], "code line 3\ncode line 4"); -} diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tags/code/ansi.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tags/code/ansi.rs deleted file mode 100644 index b617b69a..00000000 --- a/src/app/browser/window/tab/item/page/content/text/markdown/tags/code/ansi.rs +++ /dev/null @@ -1,33 +0,0 @@ -mod rgba; -mod tag; - -use tag::Tag; - -use ansi_parser::{AnsiParser, AnsiSequence, Output}; -use gtk::{TextTag, prelude::TextTagExt}; - -/// Apply ANSI/SGR format to new buffer -pub fn format(source_code: &str) -> Vec<(TextTag, String)> { - let mut buffer = Vec::new(); - let mut tag = Tag::new(); - - for ref entity in source_code.ansi_parse() { - if let Output::Escape(AnsiSequence::SetGraphicsMode(color)) = entity - && color.len() > 1 - { - if color[0] == 38 { - tag.text_tag - .set_foreground_rgba(rgba::default(*color.last().unwrap()).as_ref()); - } else { - tag.text_tag - .set_background_rgba(rgba::default(*color.last().unwrap()).as_ref()); - } - } - if let Output::TextBlock(text) = entity { - buffer.push((tag.text_tag, text.to_string())); - tag = Tag::new(); - } - } - - buffer -} diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tags/code/ansi/rgba.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tags/code/ansi/rgba.rs deleted file mode 100644 index d1398d2f..00000000 --- a/src/app/browser/window/tab/item/page/content/text/markdown/tags/code/ansi/rgba.rs +++ /dev/null @@ -1,256 +0,0 @@ -use gtk::gdk::RGBA; - -/// Default RGBa palette for ANSI terminal emulation -pub fn default(color: u8) -> Option { - match color { - 7 => Some(RGBA::new(0.854, 0.854, 0.854, 1.0)), - 8 => Some(RGBA::new(0.424, 0.424, 0.424, 1.0)), - 10 => Some(RGBA::new(0.0, 1.0, 0.0, 1.0)), - 11 => Some(RGBA::new(1.0, 1.0, 0.0, 1.0)), - 12 => Some(RGBA::new(0.0, 0.0, 1.0, 1.0)), - 13 => Some(RGBA::new(1.0, 0.0, 1.0, 1.0)), - 14 => Some(RGBA::new(0.0, 1.0, 1.0, 1.0)), - 15 => Some(RGBA::new(1.0, 1.0, 1.0, 1.0)), - 16 => Some(RGBA::new(0.0, 0.0, 0.0, 1.0)), - 17 => Some(RGBA::new(0.0, 0.020, 0.373, 1.0)), - 18 => Some(RGBA::new(0.0, 0.031, 0.529, 1.0)), - 19 => Some(RGBA::new(0.0, 0.0, 0.686, 1.0)), - 20 => Some(RGBA::new(0.0, 0.0, 0.823, 1.0)), - 21 => Some(RGBA::new(0.0, 0.0, 1.0, 1.0)), - 22 => Some(RGBA::new(0.0, 0.373, 0.0, 1.0)), - 23 => Some(RGBA::new(0.0, 0.373, 0.373, 1.0)), - 24 => Some(RGBA::new(0.0, 0.373, 0.529, 1.0)), - 25 => Some(RGBA::new(0.0, 0.373, 0.686, 1.0)), - 26 => Some(RGBA::new(0.0, 0.373, 0.823, 1.0)), - 27 => Some(RGBA::new(0.0, 0.373, 1.0, 1.0)), - 28 => Some(RGBA::new(0.0, 0.533, 0.0, 1.0)), - 29 => Some(RGBA::new(0.0, 0.533, 0.373, 1.0)), - 30 => Some(RGBA::new(0.0, 0.533, 0.533, 1.0)), - 31 => Some(RGBA::new(0.0, 0.533, 0.686, 1.0)), - 32 => Some(RGBA::new(0.0, 0.533, 0.823, 1.0)), - 33 => Some(RGBA::new(0.0, 0.533, 1.0, 1.0)), - 34 => Some(RGBA::new(0.039, 0.686, 0.0, 1.0)), - 35 => Some(RGBA::new(0.039, 0.686, 0.373, 1.0)), - 36 => Some(RGBA::new(0.039, 0.686, 0.529, 1.0)), - 37 => Some(RGBA::new(0.039, 0.686, 0.686, 1.0)), - 38 => Some(RGBA::new(0.039, 0.686, 0.823, 1.0)), - 39 => Some(RGBA::new(0.039, 0.686, 1.0, 1.0)), - 40 => Some(RGBA::new(0.0, 0.843, 0.0, 1.0)), - 41 => Some(RGBA::new(0.0, 0.843, 0.373, 1.0)), - 42 => Some(RGBA::new(0.0, 0.843, 0.529, 1.0)), - 43 => Some(RGBA::new(0.0, 0.843, 0.686, 1.0)), - 44 => Some(RGBA::new(0.0, 0.843, 0.843, 1.0)), - 45 => Some(RGBA::new(0.0, 0.843, 1.0, 1.0)), - 46 => Some(RGBA::new(0.0, 1.0, 0.0, 1.0)), - 47 => Some(RGBA::new(0.0, 1.0, 0.373, 1.0)), - 48 => Some(RGBA::new(0.0, 1.0, 0.529, 1.0)), - 49 => Some(RGBA::new(0.0, 1.0, 0.686, 1.0)), - 50 => Some(RGBA::new(0.0, 1.0, 0.843, 1.0)), - 51 => Some(RGBA::new(0.0, 1.0, 1.0, 1.0)), - 52 => Some(RGBA::new(0.373, 0.0, 0.0, 1.0)), - 53 => Some(RGBA::new(0.373, 0.0, 0.373, 1.0)), - 54 => Some(RGBA::new(0.373, 0.0, 0.529, 1.0)), - 55 => Some(RGBA::new(0.373, 0.0, 0.686, 1.0)), - 56 => Some(RGBA::new(0.373, 0.0, 0.843, 1.0)), - 57 => Some(RGBA::new(0.373, 0.0, 1.0, 1.0)), - 58 => Some(RGBA::new(0.373, 0.373, 0.0, 1.0)), - 59 => Some(RGBA::new(0.373, 0.373, 0.373, 1.0)), - 60 => Some(RGBA::new(0.373, 0.373, 0.529, 1.0)), - 61 => Some(RGBA::new(0.373, 0.373, 0.686, 1.0)), - 62 => Some(RGBA::new(0.373, 0.373, 0.843, 1.0)), - 63 => Some(RGBA::new(0.373, 0.373, 1.0, 1.0)), - 64 => Some(RGBA::new(0.373, 0.529, 0.0, 1.0)), - 65 => Some(RGBA::new(0.373, 0.529, 0.373, 1.0)), - 66 => Some(RGBA::new(0.373, 0.529, 0.529, 1.0)), - 67 => Some(RGBA::new(0.373, 0.529, 0.686, 1.0)), - 68 => Some(RGBA::new(0.373, 0.529, 0.843, 1.0)), - 69 => Some(RGBA::new(0.373, 0.529, 1.0, 1.0)), - 70 => Some(RGBA::new(0.373, 0.686, 0.0, 1.0)), - 71 => Some(RGBA::new(0.373, 0.686, 0.373, 1.0)), - 72 => Some(RGBA::new(0.373, 0.686, 0.529, 1.0)), - 73 => Some(RGBA::new(0.373, 0.686, 0.686, 1.0)), - 74 => Some(RGBA::new(0.373, 0.686, 0.843, 1.0)), - 75 => Some(RGBA::new(0.373, 0.686, 1.0, 1.0)), - 76 => Some(RGBA::new(0.373, 0.843, 0.0, 1.0)), - 77 => Some(RGBA::new(0.373, 0.843, 0.373, 1.0)), - 78 => Some(RGBA::new(0.373, 0.843, 0.529, 1.0)), - 79 => Some(RGBA::new(0.373, 0.843, 0.686, 1.0)), - 80 => Some(RGBA::new(0.373, 0.843, 0.843, 1.0)), - 81 => Some(RGBA::new(0.373, 0.843, 1.0, 1.0)), - 82 => Some(RGBA::new(0.373, 1.0, 0.0, 1.0)), - 83 => Some(RGBA::new(0.373, 1.0, 0.373, 1.0)), - 84 => Some(RGBA::new(0.373, 1.0, 0.529, 1.0)), - 85 => Some(RGBA::new(0.373, 1.0, 0.686, 1.0)), - 86 => Some(RGBA::new(0.373, 1.0, 0.843, 1.0)), - 87 => Some(RGBA::new(0.373, 1.0, 1.0, 1.0)), - 88 => Some(RGBA::new(0.529, 0.0, 0.0, 1.0)), - 89 => Some(RGBA::new(0.529, 0.0, 0.373, 1.0)), - 90 => Some(RGBA::new(0.529, 0.0, 0.529, 1.0)), - 91 => Some(RGBA::new(0.529, 0.0, 0.686, 1.0)), - 92 => Some(RGBA::new(0.529, 0.0, 0.843, 1.0)), - 93 => Some(RGBA::new(0.529, 0.0, 1.0, 1.0)), - 94 => Some(RGBA::new(0.529, 0.373, 0.0, 1.0)), - 95 => Some(RGBA::new(0.529, 0.373, 0.373, 1.0)), - 96 => Some(RGBA::new(0.529, 0.373, 0.529, 1.0)), - 97 => Some(RGBA::new(0.529, 0.373, 0.686, 1.0)), - 98 => Some(RGBA::new(0.529, 0.373, 0.843, 1.0)), - 99 => Some(RGBA::new(0.529, 0.373, 1.0, 1.0)), - 100 => Some(RGBA::new(0.529, 0.529, 0.0, 1.0)), - 101 => Some(RGBA::new(0.529, 0.529, 0.373, 1.0)), - 102 => Some(RGBA::new(0.529, 0.529, 0.529, 1.0)), - 103 => Some(RGBA::new(0.529, 0.529, 0.686, 1.0)), - 104 => Some(RGBA::new(0.529, 0.529, 0.843, 1.0)), - 105 => Some(RGBA::new(0.529, 0.529, 1.0, 1.0)), - 106 => Some(RGBA::new(0.533, 0.686, 0.0, 1.0)), - 107 => Some(RGBA::new(0.533, 0.686, 0.373, 1.0)), - 108 => Some(RGBA::new(0.533, 0.686, 0.529, 1.0)), - 109 => Some(RGBA::new(0.533, 0.686, 0.686, 1.0)), - 110 => Some(RGBA::new(0.533, 0.686, 0.843, 1.0)), - 111 => Some(RGBA::new(0.533, 0.686, 1.0, 1.0)), - 112 => Some(RGBA::new(0.533, 0.843, 0.0, 1.0)), - 113 => Some(RGBA::new(0.533, 0.843, 0.373, 1.0)), - 114 => Some(RGBA::new(0.533, 0.843, 0.529, 1.0)), - 115 => Some(RGBA::new(0.533, 0.843, 0.686, 1.0)), - 116 => Some(RGBA::new(0.533, 0.843, 0.843, 1.0)), - 117 => Some(RGBA::new(0.533, 0.843, 1.0, 1.0)), - 118 => Some(RGBA::new(0.533, 1.0, 0.0, 1.0)), - 119 => Some(RGBA::new(0.533, 1.0, 0.373, 1.0)), - 120 => Some(RGBA::new(0.533, 1.0, 0.529, 1.0)), - 121 => Some(RGBA::new(0.533, 1.0, 0.686, 1.0)), - 122 => Some(RGBA::new(0.533, 1.0, 0.843, 1.0)), - 123 => Some(RGBA::new(0.533, 1.0, 1.0, 1.0)), - 124 => Some(RGBA::new(0.686, 0.0, 0.0, 1.0)), - 125 => Some(RGBA::new(0.686, 0.0, 0.373, 1.0)), - 126 => Some(RGBA::new(0.686, 0.0, 0.529, 1.0)), - 127 => Some(RGBA::new(0.686, 0.0, 0.686, 1.0)), - 128 => Some(RGBA::new(0.686, 0.0, 0.843, 1.0)), - 129 => Some(RGBA::new(0.686, 0.0, 1.0, 1.0)), - 130 => Some(RGBA::new(0.686, 0.373, 0.0, 1.0)), - 131 => Some(RGBA::new(0.686, 0.373, 0.373, 1.0)), - 132 => Some(RGBA::new(0.686, 0.373, 0.529, 1.0)), - 133 => Some(RGBA::new(0.686, 0.373, 0.686, 1.0)), - 134 => Some(RGBA::new(0.686, 0.373, 0.843, 1.0)), - 135 => Some(RGBA::new(0.686, 0.373, 1.0, 1.0)), - 136 => Some(RGBA::new(0.686, 0.529, 0.0, 1.0)), - 137 => Some(RGBA::new(0.686, 0.529, 0.373, 1.0)), - 138 => Some(RGBA::new(0.686, 0.529, 0.529, 1.0)), - 139 => Some(RGBA::new(0.686, 0.529, 0.686, 1.0)), - 140 => Some(RGBA::new(0.686, 0.529, 0.843, 1.0)), - 141 => Some(RGBA::new(0.686, 0.529, 1.0, 1.0)), - 142 => Some(RGBA::new(0.686, 0.686, 0.0, 1.0)), - 143 => Some(RGBA::new(0.686, 0.686, 0.373, 1.0)), - 144 => Some(RGBA::new(0.686, 0.686, 0.529, 1.0)), - 145 => Some(RGBA::new(0.686, 0.686, 0.686, 1.0)), - 146 => Some(RGBA::new(0.686, 0.686, 0.843, 1.0)), - 147 => Some(RGBA::new(0.686, 0.686, 1.0, 1.0)), - 148 => Some(RGBA::new(0.686, 0.843, 0.0, 1.0)), - 149 => Some(RGBA::new(0.686, 0.843, 0.373, 1.0)), - 150 => Some(RGBA::new(0.686, 0.843, 0.529, 1.0)), - 151 => Some(RGBA::new(0.686, 0.843, 0.686, 1.0)), - 152 => Some(RGBA::new(0.686, 0.843, 0.843, 1.0)), - 153 => Some(RGBA::new(0.686, 0.843, 1.0, 1.0)), - 154 => Some(RGBA::new(0.686, 1.0, 0.0, 1.0)), - 155 => Some(RGBA::new(0.686, 1.0, 0.373, 1.0)), - 156 => Some(RGBA::new(0.686, 1.0, 0.529, 1.0)), - 157 => Some(RGBA::new(0.686, 1.0, 0.686, 1.0)), - 158 => Some(RGBA::new(0.686, 1.0, 0.843, 1.0)), - 159 => Some(RGBA::new(0.686, 1.0, 1.0, 1.0)), - 160 => Some(RGBA::new(0.847, 0.0, 0.0, 1.0)), - 161 => Some(RGBA::new(0.847, 0.0, 0.373, 1.0)), - 162 => Some(RGBA::new(0.847, 0.0, 0.529, 1.0)), - 163 => Some(RGBA::new(0.847, 0.0, 0.686, 1.0)), - 164 => Some(RGBA::new(0.847, 0.0, 0.843, 1.0)), - 165 => Some(RGBA::new(0.847, 0.0, 1.0, 1.0)), - 166 => Some(RGBA::new(0.847, 0.373, 0.0, 1.0)), - 167 => Some(RGBA::new(0.847, 0.373, 0.373, 1.0)), - 168 => Some(RGBA::new(0.847, 0.373, 0.529, 1.0)), - 169 => Some(RGBA::new(0.847, 0.373, 0.686, 1.0)), - 170 => Some(RGBA::new(0.847, 0.373, 0.843, 1.0)), - 171 => Some(RGBA::new(0.847, 0.373, 1.0, 1.0)), - 172 => Some(RGBA::new(0.847, 0.529, 0.0, 1.0)), - 173 => Some(RGBA::new(0.847, 0.529, 0.373, 1.0)), - 174 => Some(RGBA::new(0.847, 0.529, 0.529, 1.0)), - 175 => Some(RGBA::new(0.847, 0.529, 0.686, 1.0)), - 176 => Some(RGBA::new(0.847, 0.529, 0.843, 1.0)), - 177 => Some(RGBA::new(0.847, 0.529, 1.0, 1.0)), - 178 => Some(RGBA::new(0.847, 0.686, 0.0, 1.0)), - 179 => Some(RGBA::new(0.847, 0.686, 0.373, 1.0)), - 180 => Some(RGBA::new(0.847, 0.686, 0.529, 1.0)), - 181 => Some(RGBA::new(0.847, 0.686, 0.686, 1.0)), - 182 => Some(RGBA::new(0.847, 0.686, 0.843, 1.0)), - 183 => Some(RGBA::new(0.847, 0.686, 1.0, 1.0)), - 184 => Some(RGBA::new(0.847, 0.843, 0.0, 1.0)), - 185 => Some(RGBA::new(0.847, 0.843, 0.373, 1.0)), - 186 => Some(RGBA::new(0.847, 0.843, 0.529, 1.0)), - 187 => Some(RGBA::new(0.847, 0.843, 0.686, 1.0)), - 188 => Some(RGBA::new(0.847, 0.843, 0.843, 1.0)), - 189 => Some(RGBA::new(0.847, 0.843, 1.0, 1.0)), - 190 => Some(RGBA::new(0.847, 1.0, 0.0, 1.0)), - 191 => Some(RGBA::new(0.847, 1.0, 0.373, 1.0)), - 192 => Some(RGBA::new(0.847, 1.0, 0.529, 1.0)), - 193 => Some(RGBA::new(0.847, 1.0, 0.686, 1.0)), - 194 => Some(RGBA::new(0.847, 1.0, 0.843, 1.0)), - 195 => Some(RGBA::new(0.847, 1.0, 1.0, 1.0)), - 196 => Some(RGBA::new(1.0, 0.0, 0.0, 1.0)), - 197 => Some(RGBA::new(1.0, 0.0, 0.373, 1.0)), - 198 => Some(RGBA::new(1.0, 0.0, 0.529, 1.0)), - 199 => Some(RGBA::new(1.0, 0.0, 0.686, 1.0)), - 200 => Some(RGBA::new(1.0, 0.0, 0.843, 1.0)), - 201 => Some(RGBA::new(1.0, 0.0, 1.0, 1.0)), - 202 => Some(RGBA::new(1.0, 0.373, 0.0, 1.0)), - 203 => Some(RGBA::new(1.0, 0.373, 0.373, 1.0)), - 204 => Some(RGBA::new(1.0, 0.373, 0.529, 1.0)), - 205 => Some(RGBA::new(1.0, 0.373, 0.686, 1.0)), - 206 => Some(RGBA::new(1.0, 0.373, 0.843, 1.0)), - 207 => Some(RGBA::new(1.0, 0.373, 1.0, 1.0)), - 208 => Some(RGBA::new(1.0, 0.529, 0.0, 1.0)), - 209 => Some(RGBA::new(1.0, 0.529, 0.373, 1.0)), - 210 => Some(RGBA::new(1.0, 0.529, 0.529, 1.0)), - 211 => Some(RGBA::new(1.0, 0.529, 0.686, 1.0)), - 212 => Some(RGBA::new(1.0, 0.529, 0.843, 1.0)), - 213 => Some(RGBA::new(1.0, 0.529, 1.0, 1.0)), - 214 => Some(RGBA::new(1.0, 0.686, 0.0, 1.0)), - 215 => Some(RGBA::new(1.0, 0.686, 0.373, 1.0)), - 216 => Some(RGBA::new(1.0, 0.686, 0.529, 1.0)), - 217 => Some(RGBA::new(1.0, 0.686, 0.686, 1.0)), - 218 => Some(RGBA::new(1.0, 0.686, 0.843, 1.0)), - 219 => Some(RGBA::new(1.0, 0.686, 1.0, 1.0)), - 220 => Some(RGBA::new(1.0, 0.843, 0.0, 1.0)), - 221 => Some(RGBA::new(1.0, 0.843, 0.373, 1.0)), - 222 => Some(RGBA::new(1.0, 0.843, 0.529, 1.0)), - 223 => Some(RGBA::new(1.0, 0.843, 0.686, 1.0)), - 224 => Some(RGBA::new(1.0, 0.843, 0.843, 1.0)), - 225 => Some(RGBA::new(1.0, 0.843, 1.0, 1.0)), - 226 => Some(RGBA::new(1.0, 1.0, 0.0, 1.0)), - 227 => Some(RGBA::new(1.0, 1.0, 0.373, 1.0)), - 228 => Some(RGBA::new(1.0, 1.0, 0.529, 1.0)), - 229 => Some(RGBA::new(1.0, 1.0, 0.686, 1.0)), - 230 => Some(RGBA::new(1.0, 1.0, 0.843, 1.0)), - 231 => Some(RGBA::new(1.0, 1.0, 1.0, 1.0)), - 232 => Some(RGBA::new(0.031, 0.031, 0.031, 1.0)), - 233 => Some(RGBA::new(0.071, 0.071, 0.071, 1.0)), - 234 => Some(RGBA::new(0.110, 0.110, 0.110, 1.0)), - 235 => Some(RGBA::new(0.149, 0.149, 0.149, 1.0)), - 236 => Some(RGBA::new(0.188, 0.188, 0.188, 1.0)), - 237 => Some(RGBA::new(0.227, 0.227, 0.227, 1.0)), - 238 => Some(RGBA::new(0.267, 0.267, 0.267, 1.0)), - 239 => Some(RGBA::new(0.306, 0.306, 0.306, 1.0)), - 240 => Some(RGBA::new(0.345, 0.345, 0.345, 1.0)), - 241 => Some(RGBA::new(0.384, 0.384, 0.384, 1.0)), - 242 => Some(RGBA::new(0.424, 0.424, 0.424, 1.0)), - 243 => Some(RGBA::new(0.462, 0.462, 0.462, 1.0)), - 244 => Some(RGBA::new(0.502, 0.502, 0.502, 1.0)), - 245 => Some(RGBA::new(0.541, 0.541, 0.541, 1.0)), - 246 => Some(RGBA::new(0.580, 0.580, 0.580, 1.0)), - 247 => Some(RGBA::new(0.620, 0.620, 0.620, 1.0)), - 248 => Some(RGBA::new(0.659, 0.659, 0.659, 1.0)), - 249 => Some(RGBA::new(0.694, 0.694, 0.694, 1.0)), - 250 => Some(RGBA::new(0.733, 0.733, 0.733, 1.0)), - 251 => Some(RGBA::new(0.777, 0.777, 0.777, 1.0)), - 252 => Some(RGBA::new(0.816, 0.816, 0.816, 1.0)), - 253 => Some(RGBA::new(0.855, 0.855, 0.855, 1.0)), - 254 => Some(RGBA::new(0.890, 0.890, 0.890, 1.0)), - 255 => Some(RGBA::new(0.933, 0.933, 0.933, 1.0)), - _ => None, - } -} diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tags/code/ansi/tag.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tags/code/ansi/tag.rs deleted file mode 100644 index 7154b1f3..00000000 --- a/src/app/browser/window/tab/item/page/content/text/markdown/tags/code/ansi/tag.rs +++ /dev/null @@ -1,29 +0,0 @@ -use gtk::{TextTag, WrapMode}; - -/// Default [TextTag](https://docs.gtk.org/gtk4/class.TextTag.html) preset -/// for ANSI buffer -pub struct Tag { - pub text_tag: TextTag, -} - -impl Default for Tag { - fn default() -> Self { - Self::new() - } -} - -impl Tag { - // Constructors - - /// Create new `Self` - pub fn new() -> Self { - Self { - text_tag: TextTag::builder() - .family("monospace") // @TODO - .left_margin(28) - .scale(0.81) // * the rounded `0.8` value crops text for some reason @TODO - .wrap_mode(WrapMode::None) - .build(), - } - } -} diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tags/code/syntax.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tags/code/syntax.rs deleted file mode 100644 index 50de853d..00000000 --- a/src/app/browser/window/tab/item/page/content/text/markdown/tags/code/syntax.rs +++ /dev/null @@ -1,152 +0,0 @@ -pub mod error; -mod tag; - -pub use error::Error; -use tag::Tag; - -use adw::StyleManager; -use gtk::{ - TextTag, - gdk::RGBA, - pango::{Style, Underline}, - prelude::TextTagExt, -}; -use syntect::{ - easy::HighlightLines, - highlighting::{Color, FontStyle, ThemeSet}, - parsing::{SyntaxReference, SyntaxSet}, -}; - -/* Default theme - @TODO make optional - base16-ocean.dark - base16-eighties.dark - base16-mocha.dark - base16-ocean.light - InspiredGitHub - Solarized (dark) - Solarized (light) -*/ -pub const DEFAULT_THEME_DARK: &str = "base16-eighties.dark"; -pub const DEFAULT_THEME_LIGHT: &str = "InspiredGitHub"; - -pub struct Syntax { - syntax_set: SyntaxSet, - theme_set: ThemeSet, -} - -impl Default for Syntax { - fn default() -> Self { - Self::new() - } -} - -impl Syntax { - // Constructors - - /// Create new `Self` - pub fn new() -> Self { - Self { - syntax_set: SyntaxSet::load_defaults_newlines(), - theme_set: ThemeSet::load_defaults(), - } - } - - // Actions - - /// Apply `Syntect` highlight to new buffer returned, - /// according to given `alt` and `source_code` content - pub fn highlight( - &self, - source_code: &str, - alt: Option<&String>, - ) -> Result, Error> { - if let Some(value) = alt { - if let Some(reference) = self.syntax_set.find_syntax_by_name(value) { - return self.buffer(source_code, reference); - } - - if let Some(reference) = self.syntax_set.find_syntax_by_token(value) { - return self.buffer(source_code, reference); - } - - if let Some(reference) = self.syntax_set.find_syntax_by_path(value) { - return self.buffer(source_code, reference); - } - } - - if let Some(reference) = self.syntax_set.find_syntax_by_first_line(source_code) { - return self.buffer(source_code, reference); - } - - Err(Error::Parse) - } - - fn buffer( - &self, - source: &str, - syntax_reference: &SyntaxReference, - ) -> Result, Error> { - // Init new line buffer - let mut buffer = Vec::new(); - - // Apply syntect decorator - let mut ranges = HighlightLines::new( - syntax_reference, - &self.theme_set.themes[if StyleManager::default().is_dark() { - DEFAULT_THEME_DARK - } else { - DEFAULT_THEME_LIGHT - }], // @TODO apply on env change - ); - - match ranges.highlight_line(source, &self.syntax_set) { - Ok(result) => { - // Build tags - for (style, entity) in result { - // Create new tag from default preset - let tag = Tag::new(); - - // Tuneup using syntect conversion - // tag.set_background_rgba(Some(&color_to_rgba(style.background))); - tag.text_tag - .set_foreground_rgba(Some(&color_to_rgba(style.foreground))); - tag.text_tag - .set_style(font_style_to_style(style.font_style)); - tag.text_tag - .set_underline(font_style_to_underline(style.font_style)); - - // Append - buffer.push((tag.text_tag, entity.to_string())); - } - Ok(buffer) - } - Err(e) => Err(Error::Syntect(e)), - } - } -} - -// Tools - -fn color_to_rgba(color: Color) -> RGBA { - RGBA::new( - color.r as f32 / 255.0, - color.g as f32 / 255.0, - color.b as f32 / 255.0, - color.a as f32 / 255.0, - ) -} - -fn font_style_to_style(font_style: FontStyle) -> Style { - match font_style { - FontStyle::ITALIC => Style::Italic, - _ => Style::Normal, - } -} - -fn font_style_to_underline(font_style: FontStyle) -> Underline { - match font_style { - FontStyle::UNDERLINE => Underline::Single, - _ => Underline::None, - } -} diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tags/code/syntax/error.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tags/code/syntax/error.rs deleted file mode 100644 index ae9bfdb6..00000000 --- a/src/app/browser/window/tab/item/page/content/text/markdown/tags/code/syntax/error.rs +++ /dev/null @@ -1,18 +0,0 @@ -use std::fmt::{Display, Formatter, Result}; - -#[derive(Debug)] -pub enum Error { - Parse, - Syntect(syntect::Error), -} - -impl Display for Error { - fn fmt(&self, f: &mut Formatter) -> Result { - match self { - Self::Parse => write!(f, "Parse error"), - Self::Syntect(e) => { - write!(f, "Syntect error: {e}") - } - } - } -} diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tags/code/syntax/tag.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tags/code/syntax/tag.rs deleted file mode 100644 index 4b2011b8..00000000 --- a/src/app/browser/window/tab/item/page/content/text/markdown/tags/code/syntax/tag.rs +++ /dev/null @@ -1,29 +0,0 @@ -use gtk::{TextTag, WrapMode}; - -/// Default [TextTag](https://docs.gtk.org/gtk4/class.TextTag.html) preset -/// for syntax highlight buffer -pub struct Tag { - pub text_tag: TextTag, -} - -impl Default for Tag { - fn default() -> Self { - Self::new() - } -} - -impl Tag { - // Constructors - - /// Create new `Self` - pub fn new() -> Self { - Self { - text_tag: TextTag::builder() - .family("monospace") // @TODO - .left_margin(28) - .scale(0.81) // * the rounded `0.8` value crops text for some reason @TODO - .wrap_mode(WrapMode::None) - .build(), - } - } -} diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tags/header.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tags/header.rs deleted file mode 100644 index 681d73ae..00000000 --- a/src/app/browser/window/tab/item/page/content/text/markdown/tags/header.rs +++ /dev/null @@ -1,170 +0,0 @@ -use gtk::{ - TextBuffer, TextTag, WrapMode, - glib::Uri, - prelude::{TextBufferExt, TextBufferExtManual}, -}; -use regex::Regex; -use std::collections::HashMap; - -const REGEX_HEADER: &str = r"(?m)^(?P#{1,6})\s+(?P.*)$"; - -pub struct Header { - h1: TextTag, - h2: TextTag, - h3: TextTag, - h4: TextTag, - h5: TextTag, - h6: TextTag, -} - -impl Header { - pub fn new() -> Self { - // * important to give the tag name here as used in the fragment search - Self { - h1: TextTag::builder() - .foreground("#2190a4") // @TODO optional - .name("h1") - .scale(1.6) - .sentence(true) - .weight(500) - .wrap_mode(WrapMode::Word) - .build(), - h2: TextTag::builder() - .foreground("#d56199") // @TODO optional - .name("h2") - .scale(1.4) - .sentence(true) - .weight(400) - .wrap_mode(WrapMode::Word) - .build(), - h3: TextTag::builder() - .foreground("#c88800") // @TODO optional - .name("h3") - .scale(1.2) - .sentence(true) - .weight(400) - .wrap_mode(WrapMode::Word) - .build(), - h4: TextTag::builder() - .foreground("#c88800") // @TODO optional - .name("h4") - .scale(1.1) - .sentence(true) - .weight(400) - .wrap_mode(WrapMode::Word) - .build(), - h5: TextTag::builder() - .foreground("#c88800") // @TODO optional - .name("h5") - .scale(1.0) - .sentence(true) - .weight(400) - .wrap_mode(WrapMode::Word) - .build(), - h6: TextTag::builder() - .foreground("#c88800") // @TODO optional - .name("h6") - .scale(1.0) - .sentence(true) - .weight(300) - .wrap_mode(WrapMode::Word) - .build(), - } - } - - /// Apply title `Tag` to given `TextBuffer` - pub fn render( - &self, - buffer: &TextBuffer, - base: &Uri, - headers: &mut HashMap<TextTag, (String, Uri)>, - ) -> Option<String> { - let mut raw_title = None; - - let table = buffer.tag_table(); - - assert!(table.add(&self.h1)); - assert!(table.add(&self.h2)); - assert!(table.add(&self.h3)); - assert!(table.add(&self.h4)); - assert!(table.add(&self.h5)); - - let (start, end) = buffer.bounds(); - let full_content = buffer.text(&start, &end, true).to_string(); - - let matches: Vec<_> = Regex::new(REGEX_HEADER) - .unwrap() - .captures_iter(&full_content) - .collect(); - - for cap in matches.iter() { - if raw_title.is_none() && !cap["title"].trim().is_empty() { - raw_title = Some(cap["title"].into()) - } - } - - for cap in matches.into_iter().rev() { - let full_match = cap.get(0).unwrap(); - - let start_char_offset = full_content[..full_match.start()].chars().count() as i32; - let end_char_offset = full_content[..full_match.end()].chars().count() as i32; - - let mut start_iter = buffer.iter_at_offset(start_char_offset); - let mut end_iter = buffer.iter_at_offset(end_char_offset); - - // Create unique phantom tag for each header - // * for the #fragment references implementation - let h = TextTag::new(Some(&format!("h{}", gtk::glib::uuid_string_random()))); - assert!(table.add(&h)); - - // Render header in text buffer - buffer.delete(&mut start_iter, &mut end_iter); - - match cap["level"].chars().count() { - 1 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&h, &self.h1]), - 2 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&h, &self.h2]), - 3 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&h, &self.h3]), - 4 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&h, &self.h4]), - 5 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&h, &self.h5]), - 6 => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&h, &self.h6]), - _ => buffer.insert_with_tags(&mut start_iter, &cap["title"], &[&h]), // unexpected - } - - // Register fragment reference - assert!( - headers - .insert( - h, - ( - cap["title"].into(), - Uri::build( - base.flags(), - &base.scheme(), - base.userinfo().as_deref(), - base.host().as_deref(), - base.port(), - &base.path(), - base.query().as_deref(), - Some(&super::format_header_fragment(&cap["title"])), - ) - ), - ) - .is_none() - ) - } - raw_title - } -} - -#[test] -fn test_regex_title() { - let cap: Vec<_> = Regex::new(REGEX_HEADER) - .unwrap() - .captures_iter(r"## Header ![alt](https://link.com)") - .collect(); - - let first = cap.first().unwrap(); - assert_eq!(&first[0], "## Header ![alt](https://link.com)"); - assert_eq!(&first["level"], "##"); - assert_eq!(&first["title"], "Header ![alt](https://link.com)"); -} diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tags/hr.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tags/hr.rs deleted file mode 100644 index 8cfcc683..00000000 --- a/src/app/browser/window/tab/item/page/content/text/markdown/tags/hr.rs +++ /dev/null @@ -1,93 +0,0 @@ -use gtk::{ - Orientation, Separator, TextView, - glib::{ControlFlow, idle_add_local}, - prelude::*, -}; -use regex::Regex; - -const REGEX_HR: &str = r"(?m)^(?P<hr>\\?[-]{3,})$"; - -/// Apply --- `Tag` to given `TextBuffer` -pub fn render(text_view: &TextView) { - let separator = Separator::builder() - .orientation(Orientation::Horizontal) - .build(); - idle_add_local({ - let text_view = text_view.clone(); - let separator = separator.clone(); - move || { - separator.set_width_request(text_view.width() - 18); - ControlFlow::Break - } - }); - - let buffer = text_view.buffer(); - - let (start, end) = buffer.bounds(); - let full_content = buffer.text(&start, &end, true).to_string(); - - let matches: Vec<_> = Regex::new(REGEX_HR) - .unwrap() - .captures_iter(&full_content) - .collect(); - - for cap in matches.into_iter().rev() { - let full_match = cap.get(0).unwrap(); - - let start_char_offset = full_content[..full_match.start()].chars().count() as i32; - let end_char_offset = full_content[..full_match.end()].chars().count() as i32; - - let mut start_iter = buffer.iter_at_offset(start_char_offset); - let mut end_iter = buffer.iter_at_offset(end_char_offset); - - if start_char_offset > 0 - && buffer - .text( - &buffer.iter_at_offset(start_char_offset - 1), - &end_iter, - false, - ) - .contains("\\") - { - continue; - } - - buffer.delete(&mut start_iter, &mut end_iter); - text_view.add_child_at_anchor(&separator, &buffer.create_child_anchor(&mut end_iter)); - } -} - -pub fn strip_tags(value: &str) -> String { - let mut result = String::from(value); - for cap in Regex::new(REGEX_HR).unwrap().captures_iter(value) { - if let Some(m) = cap.get(0) { - result = result.replace(m.as_str(), &cap["hr"]); - } - } - result -} - -#[test] -fn test_strip_tags() { - const VALUE: &str = "Some line\n---\nSome another-line with ![img](https://link.com)"; - let mut result = String::from(VALUE); - for cap in Regex::new(REGEX_HR).unwrap().captures_iter(VALUE) { - if let Some(m) = cap.get(0) { - result = result.replace(m.as_str(), ""); - } - } - assert_eq!( - result, - "Some line\n\nSome another-line with ![img](https://link.com)" - ) -} - -#[test] -fn test_regex() { - let cap: Vec<_> = Regex::new(REGEX_HR) - .unwrap() - .captures_iter("Some line\n---\nSome another-line with ![img](https://link.com)") - .collect(); - - assert_eq!(&cap.first().unwrap()["hr"], "---"); -} diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tags/italic.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tags/italic.rs deleted file mode 100644 index 9c485ad8..00000000 --- a/src/app/browser/window/tab/item/page/content/text/markdown/tags/italic.rs +++ /dev/null @@ -1,141 +0,0 @@ -use gtk::{ - TextBuffer, TextTag, - WrapMode::Word, - pango::Style, - prelude::{TextBufferExt, TextBufferExtManual}, -}; -use regex::Regex; - -const REGEX_ITALIC_1: &str = r"\*(?P<text>[^\*]*)\*"; -const REGEX_ITALIC_2: &str = r"\b_(?P<text>[^_]*)_\b"; - -pub struct Italic(TextTag); - -impl Italic { - pub fn new() -> Self { - Self( - TextTag::builder() - .style(Style::Italic) - .wrap_mode(Word) - .build(), - ) - } - - /// Apply *italic*/_italic_ `Tag` to given `TextBuffer` - /// * run after `Bold` tag! - pub fn render(&self, buffer: &TextBuffer) { - assert!(buffer.tag_table().add(&self.0)); - - render(self, buffer, REGEX_ITALIC_1); - render(self, buffer, REGEX_ITALIC_2); - } -} - -fn render(this: &Italic, buffer: &TextBuffer, regex: &str) { - let (start, end) = buffer.bounds(); - let full_content = buffer.text(&start, &end, true).to_string(); - - let matches: Vec<_> = Regex::new(regex) - .unwrap() - .captures_iter(&full_content) - .collect(); - - for cap in matches.into_iter().rev() { - let full_match = cap.get(0).unwrap(); - - let start_char_offset = full_content[..full_match.start()].chars().count() as i32; - let end_char_offset = full_content[..full_match.end()].chars().count() as i32; - - let mut start_iter = buffer.iter_at_offset(start_char_offset); - let mut end_iter = buffer.iter_at_offset(end_char_offset); - - if start_char_offset > 0 - && buffer - .text( - &buffer.iter_at_offset(start_char_offset - 1), - &end_iter, - false, - ) - .contains("\\") - { - continue; - } - - let mut tags = start_iter.tags(); - tags.push(this.0.clone()); - - buffer.delete(&mut start_iter, &mut end_iter); - buffer.insert_with_tags( - &mut start_iter, - &cap["text"], - &tags.iter().collect::<Vec<&TextTag>>(), - ) - } -} - -/// * run after `Bold` tag! -pub fn strip_tags(value: &str) -> String { - let mut s = String::from(value); - for cap in Regex::new(REGEX_ITALIC_1).unwrap().captures_iter(value) { - if let Some(m) = cap.get(0) { - s = s.replace(m.as_str(), &cap["text"]); - } - } - for cap in Regex::new(REGEX_ITALIC_2).unwrap().captures_iter(value) { - if let Some(m) = cap.get(0) { - s = s.replace(m.as_str(), &cap["text"]); - } - } - s -} - -#[test] -fn test_strip_tags() { - const S: &str = "Some *italic 1*\nand *italic 2* and _italic 3_"; - { - let mut result = String::from(S); - for cap in Regex::new(REGEX_ITALIC_1).unwrap().captures_iter(S) { - if let Some(m) = cap.get(0) { - result = result.replace(m.as_str(), &cap["text"]); - } - } - assert_eq!(result, "Some italic 1\nand italic 2 and _italic 3_") - } - { - let mut result = String::from(S); - for cap in Regex::new(REGEX_ITALIC_2).unwrap().captures_iter(S) { - if let Some(m) = cap.get(0) { - result = result.replace(m.as_str(), &cap["text"]); - } - } - assert_eq!(result, "Some *italic 1*\nand *italic 2* and italic 3") - } -} - -#[test] -fn test_regex() { - const S: &str = "Some *italic 1*\nand *italic 2* and _italic 3_"; - { - let cap: Vec<_> = Regex::new(REGEX_ITALIC_1) - .unwrap() - .captures_iter(S) - .collect(); - - assert_eq!(cap.len(), 2); - - let mut c = cap.into_iter(); - assert_eq!(&c.next().unwrap()["text"], "italic 1"); - assert_eq!(&c.next().unwrap()["text"], "italic 2"); - } - { - let cap: Vec<_> = Regex::new(REGEX_ITALIC_2) - .unwrap() - .captures_iter(S) - .collect(); - - assert_eq!(cap.len(), 1); - - let mut c = cap.into_iter(); - assert_eq!(&c.next().unwrap()["text"], "italic 3"); - } -} diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tags/list.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tags/list.rs deleted file mode 100644 index fc142275..00000000 --- a/src/app/browser/window/tab/item/page/content/text/markdown/tags/list.rs +++ /dev/null @@ -1,150 +0,0 @@ -use gtk::{ - TextBuffer, TextTag, - prelude::{TextBufferExt, TextBufferExtManual}, -}; -use regex::Regex; - -const REGEX_LIST: &str = - r"(?m)^(?P<level>[ \t]*)\*[ \t]+(?:(?P<state>\[[ xX]\])[ \t]+)?(?P<text>.*)"; - -struct State(bool); - -impl State { - fn parse(value: Option<&str>) -> Option<Self> { - if let Some(state) = value - && (state.starts_with("[ ]") || state.starts_with("[x]")) - { - return Some(Self(state.starts_with("[x]"))); - } - None - } - fn is_checked(&self) -> bool { - self.0 - } -} - -struct Item { - pub level: usize, - pub state: Option<State>, - pub text: String, -} - -impl Item { - fn parse(level: &str, state: Option<&str>, text: String) -> Self { - Self { - level: level.chars().count(), - state: State::parse(state), - text, - } - } -} - -/// Apply * list item `Tag` to given `TextBuffer` -pub fn render(buffer: &TextBuffer) { - let state_tag = TextTag::builder().family("monospace").build(); - assert!(buffer.tag_table().add(&state_tag)); - - let (start, end) = buffer.bounds(); - let full_content = buffer.text(&start, &end, true).to_string(); - - let matches: Vec<_> = Regex::new(REGEX_LIST) - .unwrap() - .captures_iter(&full_content) - .collect(); - - for cap in matches.into_iter().rev() { - let full_match = cap.get(0).unwrap(); - - let start_char_offset = full_content[..full_match.start()].chars().count() as i32; - let end_char_offset = full_content[..full_match.end()].chars().count() as i32; - - let mut start_iter = buffer.iter_at_offset(start_char_offset); - let mut end_iter = buffer.iter_at_offset(end_char_offset); - - buffer.delete(&mut start_iter, &mut end_iter); - - let item = Item::parse( - &cap["level"], - cap.name("state").map(|m| m.as_str()), - cap["text"].into(), - ); - - buffer.insert_with_tags( - &mut start_iter, - &format!("{}• ", " ".repeat(item.level)), - &[], - ); - if let Some(state) = item.state { - buffer.insert_with_tags( - &mut start_iter, - if state.is_checked() { "[x] " } else { "[ ] " }, - &[&state_tag], - ); - } - buffer.insert_with_tags(&mut start_iter, &item.text, &[]); - } -} - -#[test] -fn test_regex() { - fn item(cap: &Vec<regex::Captures<'_>>, n: usize) -> Item { - let c = cap.get(n).unwrap(); - Item::parse( - &c["level"], - c.name("state").map(|m| m.as_str()), - c["text"].into(), - ) - } - let cap: Vec<_> = Regex::new(REGEX_LIST) - .unwrap() - .captures_iter("Some\n* list item 1\n * list item 1.1\n * list item 1.2\n* list item 2\nand\n* list item 3\n * [x] list item 3.1\n * [ ] list item 3.2\n* list item 4\n") - .collect(); - { - let item = item(&cap, 0); - assert_eq!(item.level, 0); - assert!(item.state.is_none()); - assert_eq!(item.text, "list item 1"); - } - { - let item = item(&cap, 1); - assert_eq!(item.level, 2); - assert!(item.state.is_none()); - assert_eq!(item.text, "list item 1.1"); - } - { - let item = item(&cap, 2); - assert_eq!(item.level, 2); - assert!(item.state.is_none()); - assert_eq!(item.text, "list item 1.2"); - } - { - let item = item(&cap, 3); - assert_eq!(item.level, 0); - assert!(item.state.is_none()); - assert_eq!(item.text, "list item 2"); - } - { - let item = item(&cap, 4); - assert_eq!(item.level, 0); - assert!(item.state.is_none()); - assert_eq!(item.text, "list item 3"); - } - { - let item = item(&cap, 5); - assert_eq!(item.level, 2); - assert!(item.state.is_some_and(|this| this.is_checked())); - assert_eq!(item.text, "list item 3.1"); - } - { - let item = item(&cap, 6); - assert_eq!(item.level, 2); - assert!(item.state.is_some_and(|this| !this.is_checked())); - assert_eq!(item.text, "list item 3.2"); - } - { - let item = item(&cap, 7); - assert_eq!(item.level, 0); - assert!(item.state.is_none()); - assert_eq!(item.text, "list item 4"); - } -} diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tags/pre.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tags/pre.rs deleted file mode 100644 index 0ff09dc0..00000000 --- a/src/app/browser/window/tab/item/page/content/text/markdown/tags/pre.rs +++ /dev/null @@ -1,105 +0,0 @@ -use gtk::{ - TextBuffer, TextTag, - WrapMode::Word, - gdk::RGBA, - prelude::{TextBufferExt, TextBufferExtManual}, -}; -use regex::Regex; - -const REGEX_PRE: &str = r"`(?P<text>[^`]*)`"; -const TAG_FONT: &str = "monospace"; // @TODO -const TAG_SCALE: f64 = 0.9; - -pub struct Pre(TextTag); - -impl Pre { - pub fn new() -> Self { - Self(if adw::StyleManager::default().is_dark() { - TextTag::builder() - .background_rgba(&RGBA::new(255., 255., 255., 0.05)) - .family(TAG_FONT) - .foreground("#e8e8e8") - .scale(TAG_SCALE) - .wrap_mode(Word) - .build() - } else { - TextTag::builder() - .background_rgba(&RGBA::new(0., 0., 0., 0.06)) - .family(TAG_FONT) - .scale(TAG_SCALE) - .wrap_mode(Word) - .build() - }) - } - - /// Apply preformatted `Tag` to given `TextBuffer` - pub fn render(&self, buffer: &TextBuffer) { - assert!(buffer.tag_table().add(&self.0)); - - let (start, end) = buffer.bounds(); - let full_content = buffer.text(&start, &end, true).to_string(); - - let matches: Vec<_> = Regex::new(REGEX_PRE) - .unwrap() - .captures_iter(&full_content) - .collect(); - - for cap in matches.into_iter().rev() { - let full_match = cap.get(0).unwrap(); - - let start_char_offset = full_content[..full_match.start()].chars().count() as i32; - let end_char_offset = full_content[..full_match.end()].chars().count() as i32; - - let mut start_iter = buffer.iter_at_offset(start_char_offset); - let mut end_iter = buffer.iter_at_offset(end_char_offset); - - if start_char_offset > 0 - && buffer - .text( - &buffer.iter_at_offset(start_char_offset - 1), - &end_iter, - false, - ) - .contains("\\") - { - continue; - } - - buffer.delete(&mut start_iter, &mut end_iter); - buffer.insert_with_tags(&mut start_iter, &cap["text"], &[&self.0]) - } - } -} - -pub fn strip_tags(value: &str) -> String { - let mut result = String::from(value); - for cap in Regex::new(REGEX_PRE).unwrap().captures_iter(value) { - if let Some(m) = cap.get(0) { - result = result.replace(m.as_str(), &cap["text"]); - } - } - result -} - -#[test] -fn test_strip_tags() { - const VALUE: &str = r"Some `pre 1` and `pre 2` with ![img](https://link.com)"; - let mut result = String::from(VALUE); - for cap in Regex::new(REGEX_PRE).unwrap().captures_iter(VALUE) { - if let Some(m) = cap.get(0) { - result = result.replace(m.as_str(), &cap["text"]); - } - } - assert_eq!(result, "Some pre 1 and pre 2 with ![img](https://link.com)") -} - -#[test] -fn test_regex() { - let cap: Vec<_> = Regex::new(REGEX_PRE) - .unwrap() - .captures_iter(r"Some `pre 1` and `pre 2` with ![img](https://link.com)") - .collect(); - - assert_eq!(&cap.first().unwrap()["text"], "pre 1"); - assert_eq!(&cap.get(1).unwrap()["text"], "pre 2"); -} diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tags/quote.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tags/quote.rs deleted file mode 100644 index 6b7a8b74..00000000 --- a/src/app/browser/window/tab/item/page/content/text/markdown/tags/quote.rs +++ /dev/null @@ -1,66 +0,0 @@ -use gtk::{ - TextBuffer, TextTag, - WrapMode::Word, - pango::Style::Italic, - prelude::{TextBufferExt, TextBufferExtManual}, -}; -use regex::Regex; - -const REGEX_QUOTE: &str = r"(?m)^>(?:[ \t]*(?P<text>.*))?$"; - -pub struct Quote(TextTag); - -impl Quote { - pub fn new() -> Self { - Self( - TextTag::builder() - .left_margin(28) - .wrap_mode(Word) - .style(Italic) // conflicts the italic tags decoration @TODO - .build(), - ) - } - - /// Apply quote `Tag` to given `TextBuffer` - pub fn render(&self, buffer: &TextBuffer) { - assert!(buffer.tag_table().add(&self.0)); - - let (start, end) = buffer.bounds(); - let full_content = buffer.text(&start, &end, true).to_string(); - - let matches: Vec<_> = Regex::new(REGEX_QUOTE) - .unwrap() - .captures_iter(&full_content) - .collect(); - - for cap in matches.into_iter().rev() { - let full_match = cap.get(0).unwrap(); - - let start_char_offset = full_content[..full_match.start()].chars().count() as i32; - let end_char_offset = full_content[..full_match.end()].chars().count() as i32; - - let mut start_iter = buffer.iter_at_offset(start_char_offset); - let mut end_iter = buffer.iter_at_offset(end_char_offset); - - buffer.delete(&mut start_iter, &mut end_iter); - buffer.insert_with_tags(&mut start_iter, &cap["text"], &[&self.0]) - } - } -} - -#[test] -fn test_regex() { - let cap: Vec<_> = Regex::new(REGEX_QUOTE).unwrap().captures_iter( - "> Some quote 1 with ![img](https://link.com)\n>\n> 2\\)Some quote 2 with text\nplain text\n> Some quote 3" - ).collect(); - - let mut i = cap.into_iter(); - - assert_eq!( - &i.next().unwrap()["text"], - "Some quote 1 with ![img](https://link.com)" - ); - assert!(&i.next().unwrap()["text"].is_empty()); - assert_eq!(&i.next().unwrap()["text"], "2\\)Some quote 2 with text"); - assert_eq!(&i.next().unwrap()["text"], "Some quote 3"); -} diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tags/reference.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tags/reference.rs deleted file mode 100644 index 0ce45980..00000000 --- a/src/app/browser/window/tab/item/page/content/text/markdown/tags/reference.rs +++ /dev/null @@ -1,361 +0,0 @@ -use gtk::{ - TextBuffer, TextIter, TextTag, WrapMode, - gdk::RGBA, - glib::{Uri, UriFlags}, - prelude::{TextBufferExt, TextBufferExtManual}, -}; -use regex::Regex; -use std::collections::HashMap; - -const REGEX_LINK: &str = r"\[(?P<text>[^\]]*)\]\((?P<url>[^\)]+)\)"; -const REGEX_IMAGE: &str = r"!\[(?P<alt>[^\]]*)\]\((?P<url>[^\)]+)\)"; -const REGEX_IMAGE_LINK: &str = - r"\[(?P<is_img>!)\[(?P<alt>[^\]]*)\]\((?P<img_url>[^\)]+)\)\]\((?P<link_url>[^\)]+)\)"; - -struct Reference { - uri: Uri, - alt: String, -} - -impl Reference { - /// Try construct new `Self` with given options - fn parse(address: &str, alt: Option<&str>, base: &Uri) -> Option<Self> { - // Convert address to the valid URI, - // resolve to absolute URL format if the target is relative - match Uri::resolve_relative( - Some(&base.to_string()), - // Relative scheme patch - // https://datatracker.ietf.org/doc/html/rfc3986#section-4.2 - &match address.strip_prefix("//") { - Some(p) => { - let s = p.trim_start_matches(":"); - format!( - "{}://{}", - base.scheme(), - if s.is_empty() { - format!("{}/", base.host().unwrap_or_default()) - } else { - s.into() - } - ) - } - None => address.into(), - }, - UriFlags::NONE, - ) { - Ok(ref url) => match Uri::parse(url, UriFlags::NONE) { - Ok(uri) => { - let mut a: Vec<&str> = Vec::with_capacity(2); - if uri.scheme() != base.scheme() { - a.push("⇖"); - } - match alt { - Some(text) => a.push(text), - None => a.push(url), - } - Some(Self { - uri, - alt: a.join(" "), - }) - } - Err(_) => todo!(), - }, - Err(_) => None, - } - } - - /// Insert `Self` into the given `TextBuffer` by registering new `TextTag` created - fn into_buffer( - self, - buffer: &TextBuffer, - position: &mut TextIter, - link_color: &RGBA, - is_annotation: bool, - links: &mut HashMap<TextTag, Uri>, - ) { - let a = if is_annotation { - buffer.insert_with_tags(position, " ", &[]); - TextTag::builder() - .foreground_rgba(link_color) - // .foreground_rgba(&adw::StyleManager::default().accent_color_rgba()) - // @TODO adw 1.6 / ubuntu 24.10+ - .pixels_above_lines(4) - .pixels_below_lines(4) - .rise(5000) - .scale(0.8) - .wrap_mode(WrapMode::Word) - .build() - } else { - TextTag::builder() - .foreground_rgba(link_color) - // .foreground_rgba(&adw::StyleManager::default().accent_color_rgba()) - // @TODO adw 1.6 / ubuntu 24.10+ - .sentence(true) - .wrap_mode(WrapMode::Word) - .build() - }; - assert!(buffer.tag_table().add(&a)); - - let mut tags = position.tags(); // @TODO seems does not work :) - tags.push(a.clone()); - - buffer.insert_with_tags(position, &self.alt, &tags.iter().collect::<Vec<&TextTag>>()); - - links.insert(a, self.uri); - } -} - -/// Image links `[![]()]()` -fn render_images_links( - buffer: &TextBuffer, - base: &Uri, - link_color: &RGBA, - links: &mut HashMap<TextTag, Uri>, -) { - let (start, end) = buffer.bounds(); - let full_content = buffer.text(&start, &end, true).to_string(); - - let matches: Vec<_> = Regex::new(REGEX_IMAGE_LINK) - .unwrap() - .captures_iter(&full_content) - .collect(); - - for cap in matches.into_iter().rev() { - let full_match = cap.get(0).unwrap(); - - let start_char_offset = full_content[..full_match.start()].chars().count() as i32; - let end_char_offset = full_content[..full_match.end()].chars().count() as i32; - - let mut start_iter = buffer.iter_at_offset(start_char_offset); - let mut end_iter = buffer.iter_at_offset(end_char_offset); - - if start_char_offset > 0 - && buffer - .text( - &buffer.iter_at_offset(start_char_offset - 1), - &end_iter, - false, - ) - .contains("\\") - { - continue; - } - - buffer.delete(&mut start_iter, &mut end_iter); - - if let Some(this) = Reference::parse( - &cap["img_url"], - if cap["alt"].is_empty() { - None - } else { - Some(&cap["alt"]) - }, - base, - ) { - this.into_buffer(buffer, &mut start_iter, link_color, false, links) - } - if let Some(this) = Reference::parse(&cap["link_url"], Some("1"), base) { - this.into_buffer(buffer, &mut start_iter, link_color, true, links) - } - } -} - -pub fn render( - buffer: &TextBuffer, - base: &Uri, - link_color: &RGBA, - links: &mut HashMap<TextTag, Uri>, -) { - render_images_links(buffer, base, link_color, links); - render_images(buffer, base, link_color, links); - render_links(buffer, base, link_color, links) -} - -/// Image tags `![]()` -fn render_images( - buffer: &TextBuffer, - base: &Uri, - link_color: &RGBA, - links: &mut HashMap<TextTag, Uri>, -) { - let (start, end) = buffer.bounds(); - let full_content = buffer.text(&start, &end, true).to_string(); - - let matches: Vec<_> = Regex::new(REGEX_IMAGE) - .unwrap() - .captures_iter(&full_content) - .collect(); - - for cap in matches.into_iter().rev() { - let full_match = cap.get(0).unwrap(); - - let start_char_offset = full_content[..full_match.start()].chars().count() as i32; - let end_char_offset = full_content[..full_match.end()].chars().count() as i32; - - let mut start_iter = buffer.iter_at_offset(start_char_offset); - let mut end_iter = buffer.iter_at_offset(end_char_offset); - - if start_char_offset > 0 - && buffer - .text( - &buffer.iter_at_offset(start_char_offset - 1), - &end_iter, - false, - ) - .contains("\\") - { - continue; - } - - buffer.delete(&mut start_iter, &mut end_iter); - - if let Some(this) = Reference::parse( - &cap["url"], - if cap["alt"].is_empty() { - None - } else { - Some(&cap["alt"]) - }, - base, - ) { - this.into_buffer(buffer, &mut start_iter, link_color, false, links) - } - } -} -/// Links `[]()` -fn render_links( - buffer: &TextBuffer, - base: &Uri, - link_color: &RGBA, - links: &mut HashMap<TextTag, Uri>, -) { - let (start, end) = buffer.bounds(); - let full_content = buffer.text(&start, &end, true).to_string(); - - let matches: Vec<_> = Regex::new(REGEX_LINK) - .unwrap() - .captures_iter(&full_content) - .collect(); - - for cap in matches.into_iter().rev() { - let full_match = cap.get(0).unwrap(); - - let start_char_offset = full_content[..full_match.start()].chars().count() as i32; - let end_char_offset = full_content[..full_match.end()].chars().count() as i32; - - let mut start_iter = buffer.iter_at_offset(start_char_offset); - let mut end_iter = buffer.iter_at_offset(end_char_offset); - - if start_char_offset > 0 - && buffer - .text( - &buffer.iter_at_offset(start_char_offset - 1), - &end_iter, - false, - ) - .contains("\\") - { - continue; - } - - buffer.delete(&mut start_iter, &mut end_iter); - - if let Some(this) = Reference::parse( - &cap["url"], - if cap["text"].is_empty() { - None - } else { - Some(&cap["text"]) - }, - base, - ) { - this.into_buffer(buffer, &mut start_iter, link_color, false, links) - } - } -} - -pub fn strip_tags(value: &str) -> String { - let mut result = String::from(value); - for cap in Regex::new(REGEX_LINK).unwrap().captures_iter(value) { - if let Some(m) = cap.get(0) { - result = result.replace(m.as_str(), &cap["text"]); - } - } - result -} - -#[test] -fn test_strip_tags() { - const VALUE: &str = r"Some text [link1](https://link1.com) [link2](https://link2.com)"; - let mut result = String::from(VALUE); - for cap in Regex::new(REGEX_LINK).unwrap().captures_iter(VALUE) { - if let Some(m) = cap.get(0) { - result = result.replace(m.as_str(), &cap["text"]); - } - } - assert_eq!(result, "Some text link1 link2") -} - -#[test] -fn test_regex_link() { - let cap: Vec<_> = Regex::new(REGEX_LINK) - .unwrap() - .captures_iter(r"[link1](https://link1.com) [link2](https://link2.com)") - .collect(); - - let first = cap.first().unwrap(); - assert_eq!(&first[0], "[link1](https://link1.com)"); - assert_eq!(&first["text"], "link1"); - assert_eq!(&first["url"], "https://link1.com"); - - let second = cap.get(1).unwrap(); - assert_eq!(&second[0], "[link2](https://link2.com)"); - assert_eq!(&second["text"], "link2"); - assert_eq!(&second["url"], "https://link2.com"); -} - -#[test] -fn test_regex_image_link() { - let cap: Vec<_> = Regex::new( - REGEX_IMAGE_LINK, - ) - .unwrap().captures_iter( - r"[![image1](https://image1.com)](https://image2.com) [![image3](https://image3.com)](https://image4.com)" - ).collect(); - - let first = cap.first().unwrap(); - assert_eq!( - &first[0], - "[![image1](https://image1.com)](https://image2.com)" - ); - assert_eq!(&first["alt"], "image1"); - assert_eq!(&first["img_url"], "https://image1.com"); - assert_eq!(&first["link_url"], "https://image2.com"); - - let second = cap.get(1).unwrap(); - assert_eq!( - &second[0], - "[![image3](https://image3.com)](https://image4.com)" - ); - assert_eq!(&second["alt"], "image3"); - assert_eq!(&second["img_url"], "https://image3.com"); - assert_eq!(&second["link_url"], "https://image4.com"); -} - -#[test] -fn test_regex_image() { - let cap: Vec<_> = Regex::new(REGEX_IMAGE) - .unwrap() - .captures_iter(r"![image1](https://image1.com) ![image2](https://image2.com)") - .collect(); - - let first = cap.first().unwrap(); - assert_eq!(&first[0], "![image1](https://image1.com)"); - assert_eq!(&first["alt"], "image1"); - assert_eq!(&first["url"], "https://image1.com"); - - let second = cap.get(1).unwrap(); - assert_eq!(&second[0], "![image2](https://image2.com)"); - assert_eq!(&second["alt"], "image2"); - assert_eq!(&second["url"], "https://image2.com"); -} diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tags/strike.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tags/strike.rs deleted file mode 100644 index 7c0efb71..00000000 --- a/src/app/browser/window/tab/item/page/content/text/markdown/tags/strike.rs +++ /dev/null @@ -1,102 +0,0 @@ -use gtk::{ - TextBuffer, TextTag, - WrapMode::Word, - prelude::{TextBufferExt, TextBufferExtManual}, -}; -use regex::Regex; - -const REGEX_STRIKE: &str = r"~~(?P<text>[^~]*)~~"; - -pub struct Strike(TextTag); - -impl Strike { - pub fn new() -> Self { - Self( - TextTag::builder() - .strikethrough(true) - .wrap_mode(Word) - .build(), - ) - } - - /// Apply ~~strike~~ `Tag` to given `TextBuffer` - pub fn render(&self, buffer: &TextBuffer) { - assert!(buffer.tag_table().add(&self.0)); - - let (start, end) = buffer.bounds(); - let full_content = buffer.text(&start, &end, true).to_string(); - - let matches: Vec<_> = Regex::new(REGEX_STRIKE) - .unwrap() - .captures_iter(&full_content) - .collect(); - - for cap in matches.into_iter().rev() { - let full_match = cap.get(0).unwrap(); - - let start_char_offset = full_content[..full_match.start()].chars().count() as i32; - let end_char_offset = full_content[..full_match.end()].chars().count() as i32; - - let mut start_iter = buffer.iter_at_offset(start_char_offset); - let mut end_iter = buffer.iter_at_offset(end_char_offset); - - if start_char_offset > 0 - && buffer - .text( - &buffer.iter_at_offset(start_char_offset - 1), - &end_iter, - false, - ) - .contains("\\") - { - continue; - } - - let mut tags = start_iter.tags(); - tags.push(self.0.clone()); - - buffer.delete(&mut start_iter, &mut end_iter); - buffer.insert_with_tags( - &mut start_iter, - &cap["text"], - &tags.iter().collect::<Vec<&TextTag>>(), - ) - } - } -} - -pub fn strip_tags(value: &str) -> String { - let mut result = String::from(value); - for cap in Regex::new(REGEX_STRIKE).unwrap().captures_iter(value) { - if let Some(m) = cap.get(0) { - result = result.replace(m.as_str(), &cap["text"]); - } - } - result -} - -#[test] -fn test_strip_tags() { - const VALUE: &str = r"Some ~~strike 1~~ and ~~strike 2~~ with ![img](https://link.com)"; - let mut result = String::from(VALUE); - for cap in Regex::new(REGEX_STRIKE).unwrap().captures_iter(VALUE) { - if let Some(m) = cap.get(0) { - result = result.replace(m.as_str(), &cap["text"]); - } - } - assert_eq!( - result, - "Some strike 1 and strike 2 with ![img](https://link.com)" - ) -} - -#[test] -fn test_regex() { - let cap: Vec<_> = Regex::new(REGEX_STRIKE) - .unwrap() - .captures_iter(r"Some ~~strike 1~~ and ~~strike 2~~ with ![img](https://link.com)") - .collect(); - - assert_eq!(&cap.first().unwrap()["text"], "strike 1"); - assert_eq!(&cap.get(1).unwrap()["text"], "strike 2"); -} diff --git a/src/app/browser/window/tab/item/page/content/text/markdown/tags/underline.rs b/src/app/browser/window/tab/item/page/content/text/markdown/tags/underline.rs deleted file mode 100644 index 9357208a..00000000 --- a/src/app/browser/window/tab/item/page/content/text/markdown/tags/underline.rs +++ /dev/null @@ -1,98 +0,0 @@ -use gtk::{ - TextBuffer, TextTag, - WrapMode::Word, - pango::Underline::Single, - prelude::{TextBufferExt, TextBufferExtManual}, -}; -use regex::Regex; - -const REGEX_UNDERLINE: &str = r"\b_(?P<text>[^_]*)_\b"; - -pub struct Underline(TextTag); - -impl Underline { - pub fn new() -> Self { - Self(TextTag::builder().underline(Single).wrap_mode(Word).build()) - } - - /// Apply _underline_ `Tag` to given `TextBuffer` - pub fn render(&self, buffer: &TextBuffer) { - assert!(buffer.tag_table().add(&self.0)); - - let (start, end) = buffer.bounds(); - let full_content = buffer.text(&start, &end, true).to_string(); - - let matches: Vec<_> = Regex::new(REGEX_UNDERLINE) - .unwrap() - .captures_iter(&full_content) - .collect(); - - for cap in matches.into_iter().rev() { - let full_match = cap.get(0).unwrap(); - - let start_char_offset = full_content[..full_match.start()].chars().count() as i32; - let end_char_offset = full_content[..full_match.end()].chars().count() as i32; - - let mut start_iter = buffer.iter_at_offset(start_char_offset); - let mut end_iter = buffer.iter_at_offset(end_char_offset); - - if start_char_offset > 0 - && buffer - .text( - &buffer.iter_at_offset(start_char_offset - 1), - &end_iter, - false, - ) - .contains("\\") - { - continue; - } - - let mut tags = start_iter.tags(); - tags.push(self.0.clone()); - - buffer.delete(&mut start_iter, &mut end_iter); - buffer.insert_with_tags( - &mut start_iter, - &cap["text"], - &tags.iter().collect::<Vec<&TextTag>>(), - ) - } - } -} - -pub fn strip_tags(value: &str) -> String { - let mut result = String::from(value); - for cap in Regex::new(REGEX_UNDERLINE).unwrap().captures_iter(value) { - if let Some(m) = cap.get(0) { - result = result.replace(m.as_str(), &cap["text"]); - } - } - result -} - -#[test] -fn test_strip_tags() { - const VALUE: &str = r"Some _underline 1_ and _underline 2_ with ![img](https://link.com)"; - let mut result = String::from(VALUE); - for cap in Regex::new(REGEX_UNDERLINE).unwrap().captures_iter(VALUE) { - if let Some(m) = cap.get(0) { - result = result.replace(m.as_str(), &cap["text"]); - } - } - assert_eq!( - result, - "Some underline 1 and underline 2 with ![img](https://link.com)" - ) -} - -#[test] -fn test_regex() { - let cap: Vec<_> = Regex::new(REGEX_UNDERLINE) - .unwrap() - .captures_iter(r"Some _underline 1_ and _underline 2_ with ![img](https://link.com)") - .collect(); - - assert_eq!(&cap.first().unwrap()["text"], "underline 1"); - assert_eq!(&cap.get(1).unwrap()["text"], "underline 2"); -} diff --git a/src/app/browser/window/tab/item/page/navigation/request/info/dialog.rs b/src/app/browser/window/tab/item/page/navigation/request/info/dialog.rs index e3d56d8a..89ee25db 100644 --- a/src/app/browser/window/tab/item/page/navigation/request/info/dialog.rs +++ b/src/app/browser/window/tab/item/page/navigation/request/info/dialog.rs @@ -121,7 +121,7 @@ impl Dialog for PreferencesDialog { /// Lookup [MaxMind](https://www.maxmind.com) database fn l(profile: &Profile, socket_address: &SocketAddress) -> Option<String> { use maxminddb::{ - Reader, + MaxMindDbError, Reader, geoip2::{/*City,*/ Country}, }; if !matches!( @@ -136,16 +136,26 @@ impl Dialog for PreferencesDialog { Reader::open_readfile(c) } .ok()?; - let a: std::net::SocketAddr = socket_address.to_string().parse().unwrap(); - let c: Country = db.lookup(a.ip()).ok()?.decode().ok()??; - let mut b = Vec::new(); - if let Some(iso_code) = c.country.iso_code { - b.push(iso_code); + let lookup = { + let a: std::net::SocketAddr = socket_address.to_string().parse().unwrap(); + let lookup: std::result::Result<Option<Country>, MaxMindDbError> = + db.lookup(a.ip()); + lookup } - if let Some(name_en) = c.country.names.english { - b.push(name_en); - } - b.join(", ").into() + .ok()??; + lookup.country.map(|c| { + let mut b = Vec::new(); + if let Some(iso_code) = c.iso_code { + b.push(iso_code) + } + if let Some(n) = c.names + && let Some(s) = n.get("en") + { + b.push(s) + } // @TODO multi-lang + // @TODO city DB + b.join(", ") + }) } p.add(&{ let g = PreferencesGroup::builder().title("Remote").build(); diff --git a/src/profile/history/database.rs b/src/profile/history/database.rs index ed6fd1ea..ec77a840 100644 --- a/src/profile/history/database.rs +++ b/src/profile/history/database.rs @@ -151,7 +151,7 @@ pub fn select( //profile_id: row.get(1)?, opened: Event { time: DateTime::from_unix_local(row.get(2)?).unwrap(), - count: row.get::<_, i64>(3)? as usize, + count: row.get(3)?, }, closed: closed(row.get(4)?, row.get(5)?), request: row.get::<_, String>(6)?.into(),