Skip to main content

lychee_lib/checker/wikilink/
mod.rs

1//! `WikiLink` Module
2//!
3//! This module contains a Indexer and a Resolver for `WikiLinks`
4//! The Indexer recursively indexes the subdirectories and files in a given base-directory mapping
5//! the filename to the full path
6//! The Resolver looks for found `WikiLinks` in the Index thus resolving the `WikiLink` to a full
7//! filepath
8
9pub(crate) mod index;
10pub(crate) mod resolver;
11
12use crate::ErrorKind;
13use pulldown_cmark::CowStr;
14
15/// In Markdown Links both '#' and '|' act as modifiers
16/// '#' links to a specific Header in a file
17/// '|' is used to modify the link name, a so called "pothole"
18const MARKDOWN_FRAGMENT_MARKER: char = '#';
19const MARKDOWN_POTHOLE_MARKER: char = '|';
20
21/// Clean a `WikiLink` by removing potholes and fragments from a `&str`
22pub(crate) fn wikilink(input: &str, has_pothole: bool) -> Result<CowStr<'_>, ErrorKind> {
23    // Strip pothole marker (|) and pothole (text after marker) from wikilinks
24    let mut stripped_input = if has_pothole {
25        pulldown_cmark::CowStr::Borrowed(
26            &input[0..input.find(MARKDOWN_POTHOLE_MARKER).unwrap_or(input.len())],
27        )
28    } else {
29        CowStr::Borrowed(input)
30    };
31
32    // Strip fragments (#) from wikilinks, according to the obsidian spec
33    // fragments always come before potholes
34    // https://help.obsidian.md/links#Change+the+link+display+text
35    if stripped_input.contains(MARKDOWN_FRAGMENT_MARKER) {
36        stripped_input = pulldown_cmark::CowStr::Borrowed(
37            // In theory a second '#' could be inserted into the pothole, so searching for the
38            // first occurrence from the left should yield the correct result
39            &input[0..input.find(MARKDOWN_FRAGMENT_MARKER).unwrap_or(input.len())],
40        );
41    }
42    if stripped_input.is_empty() {
43        return Err(ErrorKind::EmptyUrl);
44    }
45    Ok(stripped_input)
46}
47
48#[cfg(test)]
49mod tests {
50    use pulldown_cmark::CowStr;
51    use rstest::rstest;
52
53    use crate::checker::wikilink::wikilink;
54
55    // All these Links are missing the targetname itself but contain valid fragment- and
56    // pothole-modifications. They would be parsed as an empty Link
57    #[rstest]
58    #[case("|foo", true)]
59    #[case("|foo#bar", true)]
60    #[case("|foo#bar|foo#bar", true)]
61    #[case("#baz", false)]
62    #[case("#baz#baz|foo", false)]
63    fn test_empty_wikilinks_are_detected(#[case] input: &str, #[case] has_pothole: bool) {
64        let result = wikilink(input, has_pothole);
65        assert!(result.is_err());
66    }
67
68    #[rstest]
69    #[case("link with spaces", true, "link with spaces")]
70    #[case("foo.fileextension", true, "foo.fileextension")]
71    #[case("specialcharacters !_@$&(){}", true, "specialcharacters !_@$&(){}")]
72    fn test_valid_wikilinks(#[case] input: &str, #[case] has_pothole: bool, #[case] actual: &str) {
73        let result = wikilink(input, has_pothole).unwrap();
74        let actual = CowStr::Borrowed(actual);
75        assert_eq!(result, actual);
76    }
77
78    #[rstest]
79    #[case("foo|bar", true, "foo")]
80    #[case("foo#bar", true, "foo")]
81    #[case("foo#bar|baz", false, "foo")]
82    #[case("foo#bar|baz#hashtag_in_pothole", false, "foo")]
83    #[case("foo with spaces#bar|baz#hashtag_in_pothole", false, "foo with spaces")]
84    #[case(
85        "specialcharacters !_@$&(){}#bar|baz#hashtag_in_pothole",
86        true,
87        "specialcharacters !_@$&(){}"
88    )]
89    fn test_fragment_and_pothole_removal(
90        #[case] input: &str,
91        #[case] has_pothole: bool,
92        #[case] actual: &str,
93    ) {
94        let result = wikilink(input, has_pothole).unwrap();
95        let actual = CowStr::Borrowed(actual);
96        assert_eq!(result, actual);
97    }
98}