c6o_obsidian_export/
references.rs

1use regex::Regex;
2use std::fmt;
3
4lazy_static! {
5    static ref OBSIDIAN_NOTE_LINK_RE: Regex =
6        Regex::new(r"^(?P<file>[^#|]+)??(#(?P<section>.+?))??(\|(?P<label>.+?))??$").unwrap();
7}
8
9#[derive(Debug, Clone, PartialEq)]
10/// ObsidianNoteReference represents the structure of a `[[note]]` or `![[embed]]` reference.
11pub struct ObsidianNoteReference<'a> {
12    /// The file (note name or partial path) being referenced.
13    /// This will be None in the case that the reference is to a section within the same document
14    pub file: Option<&'a str>,
15    /// If specific, a specific section/heading being referenced.
16    pub section: Option<&'a str>,
17    /// If specific, the custom label/text which was specified.
18    pub label: Option<&'a str>,
19}
20
21#[derive(PartialEq)]
22/// RefParserState enumerates all the possible parsing states [RefParser] may enter.
23pub enum RefParserState {
24    NoState,
25    ExpectSecondOpenBracket,
26    ExpectRefText,
27    ExpectRefTextOrCloseBracket,
28    ExpectFinalCloseBracket,
29    Resetting,
30}
31
32/// RefType indicates whether a note reference is a link (`[[note]]`) or embed (`![[embed]]`).
33pub enum RefType {
34    Link,
35    Embed,
36}
37
38/// RefParser holds state which is used to parse Obsidian WikiLinks (`[[note]]`, `![[embed]]`).
39pub struct RefParser {
40    pub state: RefParserState,
41    pub ref_type: Option<RefType>,
42    // References sometimes come in through multiple events. One example of this is when notes
43    // start with an underscore (_), presumably because this is also the literal which starts
44    // italic and bold text.
45    //
46    // ref_text concatenates the values from these partial events so that there's a fully-formed
47    // string to work with by the time the final `]]` is encountered.
48    pub ref_text: String,
49}
50
51impl RefParser {
52    pub fn new() -> RefParser {
53        RefParser {
54            state: RefParserState::NoState,
55            ref_type: None,
56            ref_text: String::new(),
57        }
58    }
59
60    pub fn transition(&mut self, new_state: RefParserState) {
61        self.state = new_state;
62    }
63
64    pub fn reset(&mut self) {
65        self.state = RefParserState::NoState;
66        self.ref_type = None;
67        self.ref_text.clear();
68    }
69}
70
71impl<'a> ObsidianNoteReference<'a> {
72    pub fn from_str(text: &str) -> ObsidianNoteReference {
73        let captures = OBSIDIAN_NOTE_LINK_RE
74            .captures(text)
75            .expect("note link regex didn't match - bad input?");
76        let file = captures.name("file").map(|v| v.as_str());
77        let label = captures.name("label").map(|v| v.as_str());
78        let section = captures.name("section").map(|v| v.as_str());
79
80        ObsidianNoteReference {
81            file,
82            section,
83            label,
84        }
85    }
86
87    pub fn display(&self) -> String {
88        format!("{}", self)
89    }
90}
91
92impl<'a> fmt::Display for ObsidianNoteReference<'a> {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        let label =
95            self.label
96                .map(|v| v.to_string())
97                .unwrap_or_else(|| match (self.file, self.section) {
98                    (Some(file), Some(section)) => format!("{} > {}", file, section),
99                    (Some(file), None) => file.to_string(),
100                    (None, Some(section)) => section.to_string(),
101
102                    _ => panic!("Reference exists without file or section!"),
103                });
104        write!(f, "{}", label)
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn parse_note_refs_from_strings() {
114        assert_eq!(
115            ObsidianNoteReference::from_str("Just a note"),
116            ObsidianNoteReference {
117                file: Some("Just a note"),
118                label: None,
119                section: None,
120            }
121        );
122        assert_eq!(
123            ObsidianNoteReference::from_str("A note?"),
124            ObsidianNoteReference {
125                file: Some("A note?"),
126                label: None,
127                section: None,
128            }
129        );
130        assert_eq!(
131            ObsidianNoteReference::from_str("Note#with heading"),
132            ObsidianNoteReference {
133                file: Some("Note"),
134                label: None,
135                section: Some("with heading"),
136            }
137        );
138        assert_eq!(
139            ObsidianNoteReference::from_str("Note#Heading|Label"),
140            ObsidianNoteReference {
141                file: Some("Note"),
142                label: Some("Label"),
143                section: Some("Heading"),
144            }
145        );
146        assert_eq!(
147            ObsidianNoteReference::from_str("#Heading|Label"),
148            ObsidianNoteReference {
149                file: None,
150                label: Some("Label"),
151                section: Some("Heading"),
152            }
153        );
154    }
155
156    #[test]
157    fn test_display_of_note_refs() {
158        assert_eq!(
159            "Note",
160            ObsidianNoteReference {
161                file: Some("Note"),
162                label: None,
163                section: None,
164            }
165            .display()
166        );
167        assert_eq!(
168            "Note > Heading",
169            ObsidianNoteReference {
170                file: Some("Note"),
171                label: None,
172                section: Some("Heading"),
173            }
174            .display()
175        );
176        assert_eq!(
177            "Heading",
178            ObsidianNoteReference {
179                file: None,
180                label: None,
181                section: Some("Heading"),
182            }
183            .display()
184        );
185        assert_eq!(
186            "Label",
187            ObsidianNoteReference {
188                file: Some("Note"),
189                label: Some("Label"),
190                section: Some("Heading"),
191            }
192            .display()
193        );
194        assert_eq!(
195            "Label",
196            ObsidianNoteReference {
197                file: None,
198                label: Some("Label"),
199                section: Some("Heading"),
200            }
201            .display()
202        );
203    }
204}