@@ -43,7 +43,7 @@ fn preprocess_note_links(content: &str, current_slug: &str) -> String {
4343}
4444
4545fn 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]]" ;
0 commit comments