Skip to main content

obsidian_cli_inspector/parser/
wikilink.rs

1use super::{normalize_note_identifier, Link, LinkType};
2
3pub fn extract_wikilinks(content: &str) -> Vec<Link> {
4    let mut links = Vec::new();
5    let content_chars: Vec<char> = content.chars().collect();
6    let mut pos = 0;
7
8    while pos < content_chars.len() {
9        // Check for wikilink or embed
10        if pos + 1 < content_chars.len() {
11            let is_embed = content_chars[pos] == '!' && content_chars[pos + 1] == '[';
12            let is_wikilink = content_chars[pos] == '[' && content_chars[pos + 1] == '[';
13
14            if is_embed {
15                // ![[...]]
16                if let Some(link) = parse_wikilink(&content_chars, pos + 1, true) {
17                    links.push(link);
18                }
19                pos += 1;
20            } else if is_wikilink {
21                // [[...]]
22                if let Some(link) = parse_wikilink(&content_chars, pos, false) {
23                    links.push(link);
24                }
25                pos += 1;
26            }
27        }
28        pos += 1;
29    }
30
31    links
32}
33
34pub fn parse_wikilink(chars: &[char], start: usize, is_embed: bool) -> Option<Link> {
35    if start + 3 >= chars.len() || chars[start] != '[' || chars[start + 1] != '[' {
36        return None;
37    }
38
39    let mut end = start + 2;
40    while end + 1 < chars.len() && !(chars[end] == ']' && chars[end + 1] == ']') {
41        end += 1;
42    }
43
44    if end + 1 >= chars.len() {
45        return None;
46    }
47
48    let content: String = chars[start + 2..end].iter().collect();
49    let (text, alias, heading_ref, block_ref) = parse_link_content(&content);
50    let text = normalize_note_identifier(&text);
51
52    if text.is_empty() {
53        return None;
54    }
55
56    Some(Link {
57        text,
58        alias,
59        heading_ref,
60        block_ref,
61        is_embed,
62        link_type: LinkType::Wiki,
63    })
64}
65
66fn parse_link_content(content: &str) -> (String, Option<String>, Option<String>, Option<String>) {
67    let mut alias = None;
68    let mut heading_ref = None;
69    let mut block_ref = None;
70
71    // Check for pipe (alias)
72    let text = if let Some(pipe_pos) = content.find('|') {
73        let t = content[..pipe_pos].trim().to_string();
74        alias = Some(content[pipe_pos + 1..].trim().to_string());
75        t
76    } else {
77        content.to_string()
78    };
79
80    // Check for heading reference
81    let text = if let Some(hash_pos) = text.find('#') {
82        let heading = text[hash_pos + 1..].trim().to_string();
83        let t = text[..hash_pos].trim().to_string();
84
85        if let Some(stripped) = heading.strip_prefix('^') {
86            block_ref = Some(stripped.to_string());
87        } else {
88            heading_ref = Some(heading);
89        }
90        t
91    } else {
92        text
93    };
94
95    (text, alias, heading_ref, block_ref)
96}