valis_core/modules/notes/
markdown.rs

1use std::error::Error;
2use std::fs;
3use std::fs::File;
4use std::io::Write;
5use std::path::{Path, PathBuf};
6use std::str::FromStr;
7
8use globmatch::Matcher;
9use lazy_static::lazy_static;
10use regex::Regex;
11use rlua::{Context, Lua, Table, ToLua, ToLuaMulti};
12
13use crate::modules::core::get_files;
14use crate::modules::notes::markdown::WikilinkType::TEXT;
15
16lazy_static! {
17    static ref WIKILINK_REGEX: Regex = Regex::new(r"(!)?\[\[(.*?)\]\]").unwrap();
18    static ref MEDIA_REGEX: Regex = Regex::new(r"!\[(.*)?\]\((.*)\)").unwrap();
19    static ref IMAGE_EXTENSIONS: Vec<String> = vec![
20        "jpg".to_string(),
21        "png".to_string(),
22        "svg".to_string(),
23        "jpeg".to_string(),
24        "gif".to_string()
25    ];
26}
27
28#[derive(Debug)]
29pub struct Page {
30    pub title: String,
31    pub path: PathBuf,
32    pub contents: String,
33    pub wikilinks: Vec<WikiLink>,
34}
35
36impl Clone for Page {
37    fn clone(&self) -> Self {
38        Page {
39            title: self.title.clone(),
40            path: self.path.clone(),
41            contents: self.contents.clone(),
42            wikilinks: self.wikilinks.clone(),
43        }
44    }
45}
46
47fn convert_wikilinks_to_hugo(contents: &str) -> String {
48    // Regular expression to match wikilinks
49    let re = Regex::new(r"(!?)\[\[(.*?)\]\]").unwrap();
50
51    // Function to convert a matched wikilink to Hugo format
52    let replace_func = |caps: &regex::Captures| {
53        let is_image = &caps[1] == "!";
54        let link_and_name = &caps[2];
55
56        // Check if the matched link is an image
57        if is_image {
58            format!(
59                "{{{{< figure src=\"/assets/{}\" alt=\"{}\" >}}}}",
60                link_and_name, link_and_name
61            )
62        }
63        // Check if the matched link contains a '|'
64        else if let Some(pipe_index) = link_and_name.find('|') {
65            // If it does, split the string on this character to get the link and the alternate name
66            let (link, name) = link_and_name.split_at(pipe_index);
67            let name = &name[1..]; // Remove the leading '|'
68            format!("[{}]({{{{< ref \"{}\" >}}}})", name, link)
69        } else {
70            // If there's no '|', use the entire match as both the link and the name
71            format!("[{}]({{{{< ref \"{}\" >}}}})", link_and_name, link_and_name)
72        }
73    };
74
75    // Replace all matches in the contents
76    re.replace_all(contents, replace_func).into_owned()
77}
78
79impl Page {
80    pub fn save_to_file(&self, directory: &PathBuf) -> std::io::Result<()> {
81        // Construct the full file path
82        let mut file_path = directory.clone();
83        file_path.push(format!("{}.md", &self.title));
84
85        // Open a file in write-only mode
86        let mut file = File::create(&file_path)?;
87
88        // Convert wikilinks to Hugo format
89        let contents = convert_wikilinks_to_hugo(&self.contents);
90
91        // Write the contents to file
92        file.write_all(contents.as_bytes())
93    }
94}
95
96pub trait PageLoader {
97    fn from_path(path: &PathBuf) -> Self;
98    fn title_from_path(path: &PathBuf) -> String;
99}
100
101impl PageLoader for Page {
102    fn from_path(path: &PathBuf) -> Self {
103        // Read the contents, extract wikilinks, or perform other initializations here...
104        let contents = fs::read_to_string(path).ok().unwrap();
105        Self {
106            path: path.clone(),
107            title: Self::title_from_path(path),
108            contents: contents.clone(),
109            wikilinks: extract_links(&contents),
110        }
111    }
112
113    fn title_from_path(path: &PathBuf) -> String {
114        path.file_stem()
115            .and_then(|stem| stem.to_str())
116            .unwrap_or("")
117            .to_string()
118    }
119}
120
121#[derive(Debug, PartialEq, Copy, Clone)]
122pub enum WikilinkType {
123    IMAGE,
124    TEXT,
125}
126
127#[derive(Debug)]
128pub struct WikiLink {
129    pub name: String,
130    pub link: String,
131    pub anchor: String,
132    pub link_type: WikilinkType,
133    pub original: String,
134}
135
136impl Clone for WikiLink {
137    fn clone(&self) -> Self {
138        WikiLink {
139            name: self.name.to_string(),
140            link: self.link.to_string(),
141            anchor: self.anchor.to_string(),
142            link_type: self.link_type.clone(),
143            original: self.original.to_string(),
144        }
145    }
146}
147
148pub fn get_markdown_files<'a>(root: PathBuf) -> Result<Matcher<'a, PathBuf>, String> {
149    return get_files(root, &"**/*.md");
150}
151
152pub fn remove_code_blocks(s: &str) -> String {
153// Matches code blocks with or without language identifiers
154    let re = Regex::new(r"```.*?```").unwrap();
155    re.replace_all(s, "").to_string()
156}
157
158
159pub fn extract_links(contents: &str) -> Vec<WikiLink> {
160    return WIKILINK_REGEX
161        .captures_iter(&remove_code_blocks(contents))
162        .map(|captures| parse_wikilink(captures.get(2).unwrap().as_str()))
163        .filter(|wikilink| wikilink.as_ref().ok().is_some())
164        .map(|wikilink| wikilink.unwrap())
165        .collect::<Vec<WikiLink>>();
166}
167
168impl FromStr for WikiLink {
169    type Err = Box<dyn Error>;
170
171    fn from_str(s: &str) -> Result<Self, Self::Err> {
172        let link_contents = s.trim_start_matches("[[").trim_end_matches("]]");
173
174        let (link, name) = if let Some(pipe_pos) = link_contents.find('|') {
175            let (link, name) = link_contents.split_at(pipe_pos);
176            (link.trim(), name.trim_start_matches('|'))
177        } else {
178            (link_contents, link_contents)
179        };
180
181        let (link_without_anchor, anchor) = if let Some(anchor_pos) = link.find('#') {
182            let (link, anchor) = link.split_at(anchor_pos);
183            (link.trim(), anchor.trim_start_matches('#'))
184        } else {
185            (link, "")
186        };
187
188        let is_just_anchor = link.starts_with('#');
189        let final_link = if is_just_anchor {
190            ""
191        } else {
192            link_without_anchor
193        };
194        let final_anchor = if is_just_anchor {
195            link_contents.trim_start_matches('#')
196        } else {
197            anchor
198        };
199        let final_name = if name == link {
200            final_link.to_string()
201        } else {
202            name.to_string()
203        };
204
205        let link_type = filename_type(final_link);
206
207        Ok(WikiLink {
208            name: final_name,
209            link: String::from(final_link),
210            anchor: String::from(final_anchor),
211            link_type,
212            original: "".to_string(),
213        })
214    }
215}
216
217pub fn parse_wikilink(s: &str) -> Result<WikiLink, Box<dyn Error>> {
218    WikiLink::from_str(s)
219}
220
221pub fn filename_type(filename: &str) -> WikilinkType {
222    let lower_ext = filename.split('.').last().unwrap_or("").to_lowercase();
223    match lower_ext.as_str() {
224        "jpg" | "jpeg" | "png" | "gif" => WikilinkType::IMAGE,
225        _ => WikilinkType::TEXT,
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use crate::modules::notes::markdown::WikilinkType::IMAGE;
232
233    // Note this useful idiom: importing names from outer (for mod tests) scope.
234    use super::*;
235
236    #[test]
237    fn test_simple() {
238        let wikilink = parse_wikilink("[[Test]]").unwrap();
239        assert_eq!(wikilink.name, "Test");
240        assert_eq!(wikilink.link, "Test");
241        assert_eq!(wikilink.anchor, "");
242        assert_eq!(wikilink.link_type, TEXT);
243    }
244
245    #[test]
246    fn test_alternate_name() {
247        let wikilink = parse_wikilink("[[Test|Another]]").unwrap();
248        assert_eq!(wikilink.name, "Another");
249        assert_eq!(wikilink.link, "Test");
250        assert_eq!(wikilink.anchor, "");
251        assert_eq!(wikilink.link_type, TEXT);
252    }
253
254    #[test]
255    fn test_anchor() {
256        let wikilink = parse_wikilink("[[Test#Anchor]]").unwrap();
257        assert_eq!(wikilink.name, "Test");
258        assert_eq!(wikilink.link, "Test");
259        assert_eq!(wikilink.anchor, "Anchor");
260        assert_eq!(wikilink.link_type, TEXT);
261    }
262
263    #[test]
264    fn test_just_anchor() {
265        // Just the anchor
266        let wikilink = parse_wikilink("[[#Anchor Test]]").unwrap();
267        assert_eq!(wikilink.name, "");
268        assert_eq!(wikilink.link, "");
269        assert_eq!(wikilink.anchor, "Anchor Test");
270        assert_eq!(wikilink.link_type, TEXT);
271    }
272
273    #[test]
274    fn test_anchor_and_name() {
275        let wikilink = parse_wikilink("[[Test#Anchor2|Another]]").unwrap();
276        assert_eq!(wikilink.name, "Another");
277        assert_eq!(wikilink.link, "Test");
278        assert_eq!(wikilink.anchor, "Anchor2");
279        assert_eq!(wikilink.link_type, TEXT);
280    }
281
282    #[test]
283    fn test_image() {
284        let wikilink = parse_wikilink("[[Test.jpg]]").unwrap();
285        assert_eq!(wikilink.name, "Test.jpg");
286        assert_eq!(wikilink.link, "Test.jpg");
287        assert_eq!(wikilink.anchor, "");
288        assert_eq!(wikilink.link_type, IMAGE);
289    }
290
291    #[test]
292    fn test_image_name() {
293        let wikilink = parse_wikilink("[[Test.png|Another]]").unwrap();
294        assert_eq!(wikilink.name, "Another");
295        assert_eq!(wikilink.link, "Test.png");
296        assert_eq!(wikilink.anchor, "");
297        assert_eq!(wikilink.link_type, IMAGE);
298    }
299
300    #[test]
301    fn test_extension_jpg() {
302        let filename = "foo.jpg";
303        assert_eq!(filename_type(filename), IMAGE);
304        let filename2 = "FOO.JPG";
305        assert_eq!(filename_type(filename2), IMAGE);
306    }
307
308    #[test]
309    fn test_extension_png() {
310        let filename = "foo.png";
311        assert_eq!(filename_type(filename), IMAGE);
312        let filename2 = "FOO.PNG";
313        assert_eq!(filename_type(filename2), IMAGE);
314    }
315
316    #[test]
317    fn test_extension_gif() {
318        let filename = "foo.gif";
319        assert_eq!(filename_type(filename), IMAGE);
320        let filename2 = "FOO.GIF";
321        assert_eq!(filename_type(filename), IMAGE);
322    }
323}