Skip to main content

twrite_core/batteries/markdown/
links.rs

1use std::ops::Range;
2
3use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd};
4
5/// Extracts all markdown hyperlink destinations and their label character ranges from a single line.
6pub fn extract_markdown_links(line_text: &str) -> Vec<(Range<usize>, String)> {
7    let mut options = Options::empty();
8    options.insert(Options::ENABLE_STRIKETHROUGH);
9    options.insert(Options::ENABLE_TASKLISTS);
10    let parser = Parser::new_ext(line_text, options).into_offset_iter();
11    let mut links = Vec::new();
12    let mut current_link: Option<(usize, String)> = None;
13
14    for (event, range) in parser {
15        match event {
16            Event::Start(Tag::Link { dest_url, .. }) => {
17                current_link = Some((range.start, dest_url.to_string()));
18            }
19            Event::End(TagEnd::Link) => {
20                if let Some((start, dest_url)) = current_link.take() {
21                    let end = range.end.min(line_text.len());
22                    if let Some(bracket_idx) = line_text[start..end].find("](") {
23                        let label_start = start + 1;
24                        let label_end = start + bracket_idx;
25                        links.push((label_start..label_end, dest_url));
26                    } else if let Some(bracket_idx) = line_text[start..end].find("][") {
27                        let label_start = start + 1;
28                        let label_end = start + bracket_idx;
29                        links.push((label_start..label_end, dest_url));
30                    } else if line_text[start..end].starts_with('<')
31                        && line_text[start..end].ends_with('>')
32                    {
33                        links.push((start + 1..end - 1, dest_url));
34                    } else {
35                        links.push((start..end, dest_url));
36                    }
37                }
38            }
39            _ => {}
40        }
41    }
42    links
43}
44
45#[cfg(test)]
46mod tests {
47    use super::super::highlight::MarkdownHighlighter;
48    use crate::{EditorBuffer, SyntaxHighlighter};
49
50    #[test]
51    fn test_markdown_highlighter_extract_links_trait() {
52        let buffer = EditorBuffer::new("[Google](https://google.com)");
53        let highlighter = MarkdownHighlighter::new();
54        let links = highlighter.extract_links(&buffer, 0, "[Google](https://google.com)");
55        assert_eq!(links.len(), 1);
56        assert_eq!(links[0].0, 1..7);
57        assert_eq!(links[0].1, "https://google.com");
58
59        let none = highlighter.extract_links(&buffer, 0, "plain text");
60        assert!(none.is_empty());
61    }
62}