From 3e5b89946d344051a9e63a2bf6e86ed3402aebee Mon Sep 17 00:00:00 2001 From: multiplex55 <6619098+multiplex55@users.noreply.github.com> Date: Wed, 3 Sep 2025 18:34:38 -0400 Subject: [PATCH 1/4] Parse markdown links with labels --- src/gui/mod.rs | 6 +++- src/gui/note_panel.rs | 74 +++++++++++++++++++++++++++++-------------- tests/notes_plugin.rs | 10 ++++-- 3 files changed, 63 insertions(+), 27 deletions(-) diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 2545b3dc..e4a2a1fa 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -36,7 +36,11 @@ pub use fav_dialog::FavDialog; pub use image_panel::ImagePanel; pub use macro_dialog::MacroDialog; pub use note_panel::{ - build_nvim_command, build_wezterm_command, extract_links, show_wiki_link, spawn_external, + build_nvim_command, + build_wezterm_command, + extract_links, + show_wiki_link, + spawn_external, NotePanel, }; pub use notes_dialog::NotesDialog; diff --git a/src/gui/note_panel.rs b/src/gui/note_panel.rs index 333b0979..c7e2bcad 100644 --- a/src/gui/note_panel.rs +++ b/src/gui/note_panel.rs @@ -237,11 +237,15 @@ impl NotePanel { let links = extract_links(&self.note.content); enum LinkKind { Wiki(String), - Url(String), + Url(String, String), } let mut all_links: Vec = Vec::new(); all_links.extend(wiki.into_iter().map(LinkKind::Wiki)); - all_links.extend(links.into_iter().map(LinkKind::Url)); + all_links.extend( + links + .into_iter() + .map(|(label, url)| LinkKind::Url(label, url)), + ); if !all_links.is_empty() { let was_focused = ui.ctx().memory(|m| m.has_focus(content_id)); ui.horizontal_wrapped(|ui| { @@ -255,8 +259,8 @@ impl NotePanel { LinkKind::Wiki(s) => { let _ = show_wiki_link(ui, app, s); } - LinkKind::Url(s) => { - let _ = ui.hyperlink(s); + LinkKind::Url(label, url) => { + let _ = ui.hyperlink_to(label, url); } } } @@ -1151,24 +1155,44 @@ fn extract_tags(content: &str) -> Vec { tags } -pub fn extract_links(content: &str) -> Vec { +pub fn extract_links(content: &str) -> Vec<(String, String)> { + static MARKDOWN_RE: Lazy = Lazy::new(|| Regex::new(r"\[([^\]]+)\]\(([^)]+)\)").unwrap()); static LINK_RE: Lazy = Lazy::new(|| Regex::new(r"([a-zA-Z][a-zA-Z0-9+.-]*://\S+|www\.\S+)").unwrap()); - let mut links: Vec = LINK_RE - .find_iter(content) - .filter_map(|m| { - let raw = m.as_str(); - let url = if raw.starts_with("www.") { - format!("https://{raw}") - } else { - raw.to_string() - }; - Url::parse(&url) - .ok() - .filter(|u| u.scheme() == "https") - .map(|_| raw.to_string()) - }) - .collect(); + + let mut links: Vec<(String, String)> = Vec::new(); + + for cap in MARKDOWN_RE.captures_iter(content) { + let label = cap[1].to_string(); + let raw = cap[2].to_string(); + let url = if raw.starts_with("www.") { + format!("https://{raw}") + } else { + raw.clone() + }; + if Url::parse(&url) + .ok() + .filter(|u| u.scheme() == "https") + .is_some() + { + links.push((label, raw)); + } + } + + let stripped = MARKDOWN_RE.replace_all(content, ""); + links.extend(LINK_RE.find_iter(&stripped).filter_map(|m| { + let raw = m.as_str(); + let url = if raw.starts_with("www.") { + format!("https://{raw}") + } else { + raw.to_string() + }; + Url::parse(&url) + .ok() + .filter(|u| u.scheme() == "https") + .map(|_| (raw.to_string(), raw.to_string())) + })); + links.sort(); links.dedup(); links @@ -1411,13 +1435,17 @@ mod tests { #[test] fn extract_links_filters_invalid() { - let content = "visit http://example.com and http://exa%mple.com also https://rust-lang.org and https://rust-lang.org and www.example.com and www.example.com and www.exa%mple.com"; + let content = "visit http://example.com and http://exa%mple.com also [Rust](https://rust-lang.org) and https://rust-lang.org and https://rust-lang.org and www.example.com and www.example.com and www.exa%mple.com"; let links = extract_links(content); assert_eq!( links, vec![ - "https://rust-lang.org".to_string(), - "www.example.com".to_string(), + ("Rust".to_string(), "https://rust-lang.org".to_string()), + ( + "https://rust-lang.org".to_string(), + "https://rust-lang.org".to_string(), + ), + ("www.example.com".to_string(), "www.example.com".to_string()), ] ); } diff --git a/tests/notes_plugin.rs b/tests/notes_plugin.rs index de84935c..98afe795 100644 --- a/tests/notes_plugin.rs +++ b/tests/notes_plugin.rs @@ -292,13 +292,17 @@ fn missing_link_colored_red() { #[test] fn link_validation_rejects_invalid_urls() { - let content = "visit http://example.com and http://exa%mple.com also https://rust-lang.org and https://rust-lang.org and www.example.com and www.example.com and www.exa%mple.com"; + let content = "visit http://example.com and http://exa%mple.com also [Rust](https://rust-lang.org) and https://rust-lang.org and https://rust-lang.org and www.example.com and www.example.com and www.exa%mple.com"; let links = extract_links(content); assert_eq!( links, vec![ - "https://rust-lang.org".to_string(), - "www.example.com".to_string(), + ("Rust".to_string(), "https://rust-lang.org".to_string()), + ( + "https://rust-lang.org".to_string(), + "https://rust-lang.org".to_string(), + ), + ("www.example.com".to_string(), "www.example.com".to_string()), ] ); } From 6fec3287b7f4ff0a8753bf6b06acabceab324ebf Mon Sep 17 00:00:00 2001 From: multiplex55 <6619098+multiplex55@users.noreply.github.com> Date: Wed, 3 Sep 2025 19:16:20 -0400 Subject: [PATCH 2/4] Sanitize markdown link URLs --- src/gui/note_panel.rs | 9 ++++++--- tests/notes_plugin.rs | 5 ++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/gui/note_panel.rs b/src/gui/note_panel.rs index c7e2bcad..c73ec31d 100644 --- a/src/gui/note_panel.rs +++ b/src/gui/note_panel.rs @@ -1175,7 +1175,7 @@ pub fn extract_links(content: &str) -> Vec<(String, String)> { .filter(|u| u.scheme() == "https") .is_some() { - links.push((label, raw)); + links.push((label, url)); } } @@ -1190,7 +1190,7 @@ pub fn extract_links(content: &str) -> Vec<(String, String)> { Url::parse(&url) .ok() .filter(|u| u.scheme() == "https") - .map(|_| (raw.to_string(), raw.to_string())) + .map(|_| (raw.to_string(), url)) })); links.sort(); @@ -1445,7 +1445,10 @@ mod tests { "https://rust-lang.org".to_string(), "https://rust-lang.org".to_string(), ), - ("www.example.com".to_string(), "www.example.com".to_string()), + ( + "www.example.com".to_string(), + "https://www.example.com".to_string(), + ), ] ); } diff --git a/tests/notes_plugin.rs b/tests/notes_plugin.rs index 98afe795..f501cc94 100644 --- a/tests/notes_plugin.rs +++ b/tests/notes_plugin.rs @@ -302,7 +302,10 @@ fn link_validation_rejects_invalid_urls() { "https://rust-lang.org".to_string(), "https://rust-lang.org".to_string(), ), - ("www.example.com".to_string(), "www.example.com".to_string()), + ( + "www.example.com".to_string(), + "https://www.example.com".to_string(), + ), ] ); } From cfd70b8e7c9f73e2e42a1c91832f89be233bf8cc Mon Sep 17 00:00:00 2001 From: multiplex55 <6619098+multiplex55@users.noreply.github.com> Date: Wed, 3 Sep 2025 20:24:50 -0400 Subject: [PATCH 3/4] Normalize bare www links in markdown handler --- src/gui/note_panel.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/gui/note_panel.rs b/src/gui/note_panel.rs index c73ec31d..f8f93d85 100644 --- a/src/gui/note_panel.rs +++ b/src/gui/note_panel.rs @@ -43,7 +43,7 @@ fn preprocess_note_links(content: &str, current_slug: &str) -> String { } fn handle_markdown_links(ui: &egui::Ui, app: &mut LauncherApp) { - if let Some(open_url) = ui.ctx().output_mut(|o| o.open_url.take()) { + if let Some(mut open_url) = ui.ctx().output_mut(|o| o.open_url.take()) { if let Ok(url) = Url::parse(&open_url.url) { if url.scheme() == "note" { if let Some(slug) = url.host_str() { @@ -53,6 +53,9 @@ fn handle_markdown_links(ui: &egui::Ui, app: &mut LauncherApp) { ui.ctx().open_url(open_url); } } else { + if open_url.url.starts_with("www.") { + open_url.url = format!("https://{}", open_url.url); + } ui.ctx().open_url(open_url); } } @@ -1453,6 +1456,24 @@ mod tests { ); } + #[test] + fn handle_markdown_links_promotes_www() { + let ctx = egui::Context::default(); + let mut app = new_app(&ctx); + let output = ctx.run(Default::default(), |ctx| { + egui::CentralPanel::default().show(ctx, |ui| { + ctx.output_mut(|o| { + o.open_url = Some(egui::OpenUrl::same_tab("www.example.com")); + }); + handle_markdown_links(ui, &mut app); + }); + }); + assert_eq!( + output.open_url.unwrap().url, + "https://www.example.com" + ); + } + #[test] fn extract_wiki_links_dedupes() { let content = "links [[alpha]] and [[alpha]] and [[beta]]"; From f6c08b5e5cb43f22fa0848a3d6ff3792627edf7c Mon Sep 17 00:00:00 2001 From: multiplex55 <6619098+multiplex55@users.noreply.github.com> Date: Wed, 3 Sep 2025 20:42:39 -0400 Subject: [PATCH 4/4] fix open_url field access for FullOutput --- src/gui/note_panel.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/gui/note_panel.rs b/src/gui/note_panel.rs index f8f93d85..1ba35928 100644 --- a/src/gui/note_panel.rs +++ b/src/gui/note_panel.rs @@ -1469,7 +1469,11 @@ mod tests { }); }); assert_eq!( - output.open_url.unwrap().url, + output + .platform_output + .open_url + .unwrap() + .url, "https://www.example.com" ); }