diff --git a/Cargo.lock b/Cargo.lock index 8a467b6f..56732b8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "Yoda" -version = "0.12.10" +version = "0.12.7" dependencies = [ "ansi-parser", "anyhow", @@ -20,10 +20,8 @@ dependencies = [ "plurify", "r2d2", "r2d2_sqlite", - "regex", "rusqlite", "sourceview5", - "strip-tags", "syntect", ] @@ -33,15 +31,6 @@ 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" @@ -122,9 +111,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.57" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "shlex", @@ -132,9 +121,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.20.7" +version = "0.20.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6b04e07d8080154ed4ac03546d9a2b303cc2fe1901ba0b35b301516e289368" +checksum = "78cef5b5a1a6827c7322ae2a636368a573006b27cfa76c7ebd53e834daeaab6a" dependencies = [ "smallvec", "target-lexicon", @@ -146,26 +135,6 @@ 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" @@ -386,6 +355,18 @@ dependencies = [ "system-deps", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + [[package]] name = "getrandom" version = "0.4.2" @@ -394,8 +375,7 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", - "rand_core", + "r-efi 6.0.0", "wasip2", "wasip3", ] @@ -697,9 +677,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.18" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" @@ -750,9 +730,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.183" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libspelling" @@ -876,9 +856,9 @@ checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "once_cell" -version = "1.21.4" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "onig" @@ -904,9 +884,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.76" +version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ "bitflags", "cfg-if", @@ -930,9 +910,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.112" +version = "0.9.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", @@ -1024,6 +1004,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -1036,9 +1025,9 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.5.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ "toml_edit", ] @@ -1063,13 +1052,19 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.45" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -1100,20 +1095,32 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "chacha20", - "getrandom", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" -version = "0.10.0" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] [[package]] name = "redox_syscall" @@ -1124,29 +1131,6 @@ 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" @@ -1352,12 +1336,6 @@ 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" @@ -1469,10 +1447,10 @@ dependencies = [ "indexmap", "serde_core", "serde_spanned", - "toml_datetime 0.7.5+spec-1.1.0", + "toml_datetime", "toml_parser", "toml_writer", - "winnow 0.7.15", + "winnow", ] [[package]] @@ -1484,41 +1462,32 @@ 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" +version = "0.23.10+spec-1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ "indexmap", - "toml_datetime 1.0.1+spec-1.1.0", + "toml_datetime", "toml_parser", - "winnow 1.0.0", + "winnow", ] [[package]] name = "toml_parser" -version = "1.0.10+spec-1.1.0" +version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ - "winnow 1.0.0", + "winnow", ] [[package]] name = "toml_writer" -version = "1.0.7+spec-1.1.0" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" [[package]] name = "unicode-ident" @@ -1534,11 +1503,11 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "uuid" -version = "1.22.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" dependencies = [ - "getrandom", + "getrandom 0.4.2", "js-sys", "rand", "wasm-bindgen", @@ -1689,15 +1658,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.15" +version = "0.7.14" 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" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -1799,6 +1762,26 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "zerocopy" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index 75c5b126..5ad72c3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "Yoda" -version = "0.12.10" +version = "0.12.7" edition = "2024" license = "MIT" readme = "README.md" @@ -41,8 +41,6 @@ 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" 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/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..a89e2fff 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 @@ -6,13 +6,13 @@ mod syntax; mod tag; use super::{ItemAction, WindowAction}; -use crate::{app::browser::window::action::Position, profile::Profile}; +use crate::app::browser::window::action::Position; pub use error::Error; 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}, + gdk::{BUTTON_MIDDLE, BUTTON_PRIMARY, BUTTON_SECONDARY, RGBA}, + gio::{Cancellable, SimpleAction, SimpleActionGroup}, glib::{Uri, uuid_string_random}, prelude::{PopoverExt, TextBufferExt, TextBufferExtManual, TextTagExt, TextViewExt, WidgetExt}, }; @@ -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); @@ -287,39 +296,14 @@ impl Gemini { ) } }); - let action_link_copy_url = + let action_link_copy = SimpleAction::new_stateful(&uuid_string_random(), None, &String::new().to_variant()); - action_link_copy_url.connect_activate(|this, _| { - Display::default() + action_link_copy.connect_activate(|this, _| { + gtk::gdk::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({ @@ -354,17 +338,14 @@ impl Gemini { 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_copy); 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(); + let m = gtk::gio::Menu::new(); m.append( Some("Open Link in New Tab"), Some(&format!( @@ -372,56 +353,27 @@ impl Gemini { 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.append( + Some("Copy Link"), + Some(&format!( + "{link_context_group_id}.{}", + action_link_copy.name() + )), + ); + m.append( + Some("Download Link"), + Some(&format!( + "{link_context_group_id}.{}", + action_link_download.name() + )), + ); + m.append( + Some("View Link as Source"), + Some(&format!( + "{link_context_group_id}.{}", + action_link_source.name() + )), + ); m })); link_context.set_parent(&text_view); @@ -482,65 +434,19 @@ impl Gemini { 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()); + action_link_copy.set_state(&request_var); - // 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); + action_link_download.set_enabled(is_prefixable_link(&request_str)); - // View as Source (new tab) action_link_source.set_state(&request_var); - action_link_source.set_enabled(is_prefix_link); + action_link_source.set_enabled(is_prefixable_link(&request_str)); - // Toggle link_context .set_pointing_to(Some(>k::gdk::Rectangle::new(x, y, 1, 1))); - link_context.popup() + link_context.popup(); } } } @@ -633,7 +539,7 @@ fn is_internal_link(request: &str) -> bool { || request.starts_with("source:") } -fn is_prefix_link(request: &str) -> bool { +fn is_prefixable_link(request: &str) -> bool { request.starts_with("gemini://") || request.starts_with("nex://") || request.starts_with("file://") @@ -674,6 +580,5 @@ 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"); -}