obsidian_parser/note/
parser.rs

1//! impl parser for Obsidian notes
2
3use thiserror::Error;
4
5/// Parses Obsidian-style links in note content
6///
7/// Handles all link formats:
8/// - `[[Note]]`
9/// - `[[Note|Alias]]`
10/// - `[[Note^block]]`
11/// - `[[Note#heading]]`
12/// - `[[Note#heading|Alias]]`
13///
14/// # Example
15/// ```
16/// # use obsidian_parser::note::parser::parse_links;
17/// let content = "[[Physics]] and [[Math|Mathematics]]";
18/// let links: Vec<_> = parse_links(content).collect();
19/// assert_eq!(links, vec!["Physics", "Math"]);
20/// ```
21pub fn parse_links(text: &str) -> impl Iterator<Item = &str> {
22    text.match_indices("[[").filter_map(move |(start_pos, _)| {
23        let end_pos = text[start_pos + 2..].find("]]")?;
24        let inner = &text[start_pos + 2..start_pos + 2 + end_pos];
25
26        let note_name = inner
27            .split('#')
28            .next()?
29            .split('^')
30            .next()?
31            .split('|')
32            .next()?
33            .trim();
34
35        Some(note_name)
36    })
37}
38
39#[derive(Debug, PartialEq, Eq)]
40#[allow(missing_docs)]
41pub enum ResultParse<'a> {
42    WithProperties {
43        content: &'a str,
44        properties: &'a str,
45    },
46    WithoutProperties,
47}
48
49/// Errors for [`parse_note`]
50#[derive(Debug, Error)]
51pub enum Error {
52    /// Not found closer in yanl like `---`
53    #[error("Not found closer in yaml like `---`")]
54    NotFoundCloser,
55}
56
57/// Parse obsidian note
58pub fn parse_note(raw_text: &str) -> Result<ResultParse<'_>, Error> {
59    let have_start_properties = raw_text
60        .lines()
61        .next()
62        .is_some_and(|line| line.trim_end() == "---");
63
64    if have_start_properties {
65        let closed = raw_text["---".len()..]
66            .find("---")
67            .ok_or(Error::NotFoundCloser)?;
68
69        return Ok(ResultParse::WithProperties {
70            content: raw_text[(closed + 2 * "...".len())..].trim(),
71            properties: raw_text["...".len()..(closed + "...".len())].trim(),
72        });
73    }
74
75    Ok(ResultParse::WithoutProperties)
76}
77
78#[cfg(test)]
79mod tests {
80    use super::{ResultParse, parse_note};
81
82    #[cfg_attr(feature = "tracing", tracing_test::traced_test)]
83    #[test]
84    fn parse_note_without_properties() {
85        let test_data = "test_data";
86        let result = parse_note(test_data).unwrap();
87
88        assert_eq!(result, ResultParse::WithoutProperties);
89    }
90
91    #[cfg_attr(feature = "tracing", tracing_test::traced_test)]
92    #[test]
93    fn parse_note_with_properties() {
94        let test_data = "---\nproperties data\n---\ntest data";
95        let result = parse_note(test_data).unwrap();
96
97        assert_eq!(
98            result,
99            ResultParse::WithProperties {
100                content: "test data",
101                properties: "properties data"
102            }
103        );
104    }
105
106    #[cfg_attr(feature = "tracing", tracing_test::traced_test)]
107    #[test]
108    fn parse_note_without_properties_but_with_closed() {
109        let test_data1 = "test_data---";
110        let test_data2 = "test_data\n---\n";
111
112        let result1 = parse_note(test_data1).unwrap();
113        let result2 = parse_note(test_data2).unwrap();
114
115        assert_eq!(result1, ResultParse::WithoutProperties);
116        assert_eq!(result2, ResultParse::WithoutProperties);
117    }
118
119    #[cfg_attr(feature = "tracing", tracing_test::traced_test)]
120    #[test]
121    #[should_panic]
122    fn parse_note_with_properties_but_without_closed() {
123        let test_data = "---\nproperties data\ntest data";
124        let _ = parse_note(test_data).unwrap();
125    }
126
127    #[cfg_attr(feature = "tracing", tracing_test::traced_test)]
128    #[test]
129    fn parse_note_with_() {
130        let test_data = "---properties data";
131
132        let result = parse_note(test_data).unwrap();
133        assert_eq!(result, ResultParse::WithoutProperties);
134    }
135
136    #[cfg_attr(feature = "tracing", tracing_test::traced_test)]
137    #[test]
138    fn parse_note_without_properties_but_with_spaces() {
139        let test_data = "   ---\ndata";
140
141        let result = parse_note(test_data).unwrap();
142        assert_eq!(result, ResultParse::WithoutProperties);
143    }
144
145    #[cfg_attr(feature = "tracing", tracing_test::traced_test)]
146    #[test]
147    fn parse_note_with_properties_but_check_trim_end() {
148        let test_data = "---\r\nproperties data\r\n---\r   \ntest data";
149        let result = parse_note(test_data).unwrap();
150
151        assert_eq!(
152            result,
153            ResultParse::WithProperties {
154                content: "test data",
155                properties: "properties data"
156            }
157        );
158    }
159
160    #[cfg_attr(feature = "tracing", tracing_test::traced_test)]
161    #[test]
162    fn test_parse_links() {
163        let test_data =
164            "[[Note]] [[Note|Alias]] [[Note^block]] [[Note#Heading|Alias]] [[Note^block|Alias]]";
165
166        let ds: Vec<_> = super::parse_links(test_data).collect();
167
168        assert!(ds.iter().all(|x| *x == "Note"))
169    }
170}