Skip to main content

tatuin_core/
raw_link_transformer.rs

1// SPDX-License-Identifier: MIT
2
3use crate::RichStringTransformerTrait;
4use regex::Regex;
5use std::sync::LazyLock;
6
7static URL_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\w+://[^\s$)]+").unwrap());
8
9#[derive(Debug)]
10pub struct RawLinkTransformer {}
11
12impl RichStringTransformerTrait for RawLinkTransformer {
13    fn transform(&self, s: &str) -> String {
14        fix_raw_links(s)
15    }
16}
17
18fn fix_raw_links(text: &str) -> String {
19    let mut result = String::new();
20
21    let mut last_end: usize = 0;
22
23    for m in URL_RE.find_iter(text) {
24        let start = m.start();
25        if start > 3 /*^[](http://) case*/ && text.get(start-2..start).is_some_and(|s| s == "](") {
26            // detect []() link
27            continue;
28        }
29        let mut s = m.as_str();
30        let mut end = m.end();
31        // check correctness of the url because the regexp is very simple
32        if url::Url::parse(s).is_err() {
33            // try one more time without the last symbol
34            if url::Url::parse(&s[..s.len() - 1]).is_ok() {
35                end -= 1;
36                s = &s[..s.len() - 1];
37            } else {
38                continue;
39            }
40        }
41
42        result.push_str(&text[last_end..start]);
43        result.push('[');
44        result.push_str(s);
45        result.push_str("](");
46        result.push_str(s);
47        result.push(')');
48        last_end = end;
49    }
50    result.push_str(&text[last_end..]);
51    result
52}
53
54#[cfg(test)]
55mod test {
56    use super::fix_raw_links;
57
58    #[test]
59    fn fix_raw_links_test() {
60        const FIXTURE:&str = "
61obsidian://open?vault=personal&file=%D0%92%D0%B5%D1%80%D0%B8%D0%BD%D0%B0%20%D0%BA%D1%80%D0%B0%D1%81%D0%BA%D0%B0%20%D0%B4%D0%BB%D1%8F%20%D0%B2%D0%BE%D0%BB%D0%BE%D1%81
62[link](obsidian://open?vault=personal&file=%D0%92%D0%B5%D1%80%D0%B8%D0%BD%D0%B0%20%D0%BA%D1%80%D0%B0%D1%81%D0%BA%D0%B0%20%D0%B4%D0%BB%D1%8F%20%D0%B2%D0%BE%D0%BB%D0%BE%D1%81)
63Some http://some another http://hello/: and one more (http://yeeeee)
64- [ ] Some task http://yandex.ru/some/uri 📅 2025-09-18
65    First line https://login:password@host:1243/path/inside/file.txt: here
66    Second line [link](http://ya.ru)
67    Third line
68";
69        const RESULT:&str = "
70[obsidian://open?vault=personal&file=%D0%92%D0%B5%D1%80%D0%B8%D0%BD%D0%B0%20%D0%BA%D1%80%D0%B0%D1%81%D0%BA%D0%B0%20%D0%B4%D0%BB%D1%8F%20%D0%B2%D0%BE%D0%BB%D0%BE%D1%81](obsidian://open?vault=personal&file=%D0%92%D0%B5%D1%80%D0%B8%D0%BD%D0%B0%20%D0%BA%D1%80%D0%B0%D1%81%D0%BA%D0%B0%20%D0%B4%D0%BB%D1%8F%20%D0%B2%D0%BE%D0%BB%D0%BE%D1%81)
71[link](obsidian://open?vault=personal&file=%D0%92%D0%B5%D1%80%D0%B8%D0%BD%D0%B0%20%D0%BA%D1%80%D0%B0%D1%81%D0%BA%D0%B0%20%D0%B4%D0%BB%D1%8F%20%D0%B2%D0%BE%D0%BB%D0%BE%D1%81)
72Some [http://some](http://some) another [http://hello/:](http://hello/:) and one more ([http://yeeeee](http://yeeeee))
73- [ ] Some task [http://yandex.ru/some/uri](http://yandex.ru/some/uri) 📅 2025-09-18
74    First line [https://login:password@host:1243/path/inside/file.txt:](https://login:password@host:1243/path/inside/file.txt:) here
75    Second line [link](http://ya.ru)
76    Third line
77";
78
79        let t = fix_raw_links(FIXTURE);
80        assert_eq!(RESULT, t);
81    }
82}