Skip to content

Commit 197753f

Browse files
authored
Merge pull request #619 from multiplex55/codex/update-link-extraction-and-rendering
Parse markdown links with labels
2 parents 0cce6f6 + f6c08b5 commit 197753f

3 files changed

Lines changed: 95 additions & 28 deletions

File tree

src/gui/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ pub use fav_dialog::FavDialog;
3636
pub use image_panel::ImagePanel;
3737
pub use macro_dialog::MacroDialog;
3838
pub use note_panel::{
39-
build_nvim_command, build_wezterm_command, extract_links, show_wiki_link, spawn_external,
39+
build_nvim_command,
40+
build_wezterm_command,
41+
extract_links,
42+
show_wiki_link,
43+
spawn_external,
4044
NotePanel,
4145
};
4246
pub use notes_dialog::NotesDialog;

src/gui/note_panel.rs

Lines changed: 80 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ fn preprocess_note_links(content: &str, current_slug: &str) -> String {
4343
}
4444

4545
fn handle_markdown_links(ui: &egui::Ui, app: &mut LauncherApp) {
46-
if let Some(open_url) = ui.ctx().output_mut(|o| o.open_url.take()) {
46+
if let Some(mut open_url) = ui.ctx().output_mut(|o| o.open_url.take()) {
4747
if let Ok(url) = Url::parse(&open_url.url) {
4848
if url.scheme() == "note" {
4949
if let Some(slug) = url.host_str() {
@@ -53,6 +53,9 @@ fn handle_markdown_links(ui: &egui::Ui, app: &mut LauncherApp) {
5353
ui.ctx().open_url(open_url);
5454
}
5555
} else {
56+
if open_url.url.starts_with("www.") {
57+
open_url.url = format!("https://{}", open_url.url);
58+
}
5659
ui.ctx().open_url(open_url);
5760
}
5861
}
@@ -237,11 +240,15 @@ impl NotePanel {
237240
let links = extract_links(&self.note.content);
238241
enum LinkKind {
239242
Wiki(String),
240-
Url(String),
243+
Url(String, String),
241244
}
242245
let mut all_links: Vec<LinkKind> = Vec::new();
243246
all_links.extend(wiki.into_iter().map(LinkKind::Wiki));
244-
all_links.extend(links.into_iter().map(LinkKind::Url));
247+
all_links.extend(
248+
links
249+
.into_iter()
250+
.map(|(label, url)| LinkKind::Url(label, url)),
251+
);
245252
if !all_links.is_empty() {
246253
let was_focused = ui.ctx().memory(|m| m.has_focus(content_id));
247254
ui.horizontal_wrapped(|ui| {
@@ -255,8 +262,8 @@ impl NotePanel {
255262
LinkKind::Wiki(s) => {
256263
let _ = show_wiki_link(ui, app, s);
257264
}
258-
LinkKind::Url(s) => {
259-
let _ = ui.hyperlink(s);
265+
LinkKind::Url(label, url) => {
266+
let _ = ui.hyperlink_to(label, url);
260267
}
261268
}
262269
}
@@ -1151,24 +1158,44 @@ fn extract_tags(content: &str) -> Vec<String> {
11511158
tags
11521159
}
11531160

1154-
pub fn extract_links(content: &str) -> Vec<String> {
1161+
pub fn extract_links(content: &str) -> Vec<(String, String)> {
1162+
static MARKDOWN_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\[([^\]]+)\]\(([^)]+)\)").unwrap());
11551163
static LINK_RE: Lazy<Regex> =
11561164
Lazy::new(|| Regex::new(r"([a-zA-Z][a-zA-Z0-9+.-]*://\S+|www\.\S+)").unwrap());
1157-
let mut links: Vec<String> = LINK_RE
1158-
.find_iter(content)
1159-
.filter_map(|m| {
1160-
let raw = m.as_str();
1161-
let url = if raw.starts_with("www.") {
1162-
format!("https://{raw}")
1163-
} else {
1164-
raw.to_string()
1165-
};
1166-
Url::parse(&url)
1167-
.ok()
1168-
.filter(|u| u.scheme() == "https")
1169-
.map(|_| raw.to_string())
1170-
})
1171-
.collect();
1165+
1166+
let mut links: Vec<(String, String)> = Vec::new();
1167+
1168+
for cap in MARKDOWN_RE.captures_iter(content) {
1169+
let label = cap[1].to_string();
1170+
let raw = cap[2].to_string();
1171+
let url = if raw.starts_with("www.") {
1172+
format!("https://{raw}")
1173+
} else {
1174+
raw.clone()
1175+
};
1176+
if Url::parse(&url)
1177+
.ok()
1178+
.filter(|u| u.scheme() == "https")
1179+
.is_some()
1180+
{
1181+
links.push((label, url));
1182+
}
1183+
}
1184+
1185+
let stripped = MARKDOWN_RE.replace_all(content, "");
1186+
links.extend(LINK_RE.find_iter(&stripped).filter_map(|m| {
1187+
let raw = m.as_str();
1188+
let url = if raw.starts_with("www.") {
1189+
format!("https://{raw}")
1190+
} else {
1191+
raw.to_string()
1192+
};
1193+
Url::parse(&url)
1194+
.ok()
1195+
.filter(|u| u.scheme() == "https")
1196+
.map(|_| (raw.to_string(), url))
1197+
}));
1198+
11721199
links.sort();
11731200
links.dedup();
11741201
links
@@ -1411,17 +1438,46 @@ mod tests {
14111438

14121439
#[test]
14131440
fn extract_links_filters_invalid() {
1414-
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";
1441+
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";
14151442
let links = extract_links(content);
14161443
assert_eq!(
14171444
links,
14181445
vec![
1419-
"https://rust-lang.org".to_string(),
1420-
"www.example.com".to_string(),
1446+
("Rust".to_string(), "https://rust-lang.org".to_string()),
1447+
(
1448+
"https://rust-lang.org".to_string(),
1449+
"https://rust-lang.org".to_string(),
1450+
),
1451+
(
1452+
"www.example.com".to_string(),
1453+
"https://www.example.com".to_string(),
1454+
),
14211455
]
14221456
);
14231457
}
14241458

1459+
#[test]
1460+
fn handle_markdown_links_promotes_www() {
1461+
let ctx = egui::Context::default();
1462+
let mut app = new_app(&ctx);
1463+
let output = ctx.run(Default::default(), |ctx| {
1464+
egui::CentralPanel::default().show(ctx, |ui| {
1465+
ctx.output_mut(|o| {
1466+
o.open_url = Some(egui::OpenUrl::same_tab("www.example.com"));
1467+
});
1468+
handle_markdown_links(ui, &mut app);
1469+
});
1470+
});
1471+
assert_eq!(
1472+
output
1473+
.platform_output
1474+
.open_url
1475+
.unwrap()
1476+
.url,
1477+
"https://www.example.com"
1478+
);
1479+
}
1480+
14251481
#[test]
14261482
fn extract_wiki_links_dedupes() {
14271483
let content = "links [[alpha]] and [[alpha]] and [[beta]]";

tests/notes_plugin.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -292,13 +292,20 @@ fn missing_link_colored_red() {
292292

293293
#[test]
294294
fn link_validation_rejects_invalid_urls() {
295-
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";
295+
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";
296296
let links = extract_links(content);
297297
assert_eq!(
298298
links,
299299
vec![
300-
"https://rust-lang.org".to_string(),
301-
"www.example.com".to_string(),
300+
("Rust".to_string(), "https://rust-lang.org".to_string()),
301+
(
302+
"https://rust-lang.org".to_string(),
303+
"https://rust-lang.org".to_string(),
304+
),
305+
(
306+
"www.example.com".to_string(),
307+
"https://www.example.com".to_string(),
308+
),
302309
]
303310
);
304311
}

0 commit comments

Comments
 (0)