wayver's git archive


an obsidian renderer
git clone https://git.wayver.dev/sable

sable-markdown/src/parser/link_util.rs@2b84405277e54ab809e328cf0237374d4b4dbd0c

raw
Date Commit Message Author Files + -
2026-02-23 01:55 initial mvp wayverd 139 17808 0
...

1use nom::{
2    IResult, Parser,
3    branch::alt,
4    bytes::complete::tag,
5    character::complete::{anychar, char, none_of, one_of, satisfy},
6    combinator::{map, not, peek, recognize, value, verify},
7    multi::{fold_many0, many0, many1},
8    sequence::{delimited, preceded},
9};
10
11pub(super) fn link_label<'a>() -> impl FnMut(&'a str) -> IResult<&'a str, Vec<crate::ast::Inline>> {
12    move |input: &'a str| delimited(tag("["), link_label_inner(), tag("]")).parse(input)
13}
14
15fn link_label_inner<'a>() -> impl FnMut(&'a str) -> IResult<&'a str, Vec<crate::ast::Inline>> {
16    move |input: &'a str| {
17        let (input, label_chars) = verify(
18            many1(preceded(
19                peek(not(char(']'))),
20                alt((value(']', tag("\\]")), anychar)),
21            )),
22            |chars: &[char]| chars.iter().any(|&c| c != ' ' && c != '\n') && chars.len() < 1000,
23        )
24        .parse(input)?;
25
26        let label = label_chars.iter().collect::<String>();
27
28        let (_, label) = crate::parser::inline::inline_many1()
29            .parse(label.as_str())
30            .map_err(|err| err.map_input(|_| input))?;
31
32        Ok((input, label))
33    }
34}
35
36pub(super) fn link_title(input: &str) -> IResult<&str, String> {
37    alt((
38        link_title_double_quoted,
39        link_title_single_quoted,
40        link_title_parenthesized,
41    ))
42    .parse(input)
43}
44
45fn link_title_parenthesized(input: &str) -> IResult<&str, String> {
46    delimited(char('('), link_title_inner(')'), char(')')).parse(input)
47}
48
49fn link_title_single_quoted(input: &str) -> IResult<&str, String> {
50    delimited(char('\''), link_title_inner('\''), char('\'')).parse(input)
51}
52
53fn link_title_double_quoted(input: &str) -> IResult<&str, String> {
54    delimited(tag("\""), link_title_inner('"'), tag("\"")).parse(input)
55}
56
57fn link_title_inner(end_delim: char) -> impl FnMut(&str) -> IResult<&str, String> {
58    move |input: &str| {
59        fold_many0(
60            alt((
61                map(escaped_char, |c| c.to_string()),
62                map(none_of(&[end_delim, '\\'][..]), |c| c.to_string()),
63            )),
64            String::new,
65            |mut acc, s| {
66                acc.push_str(&s);
67                acc
68            },
69        )
70        .parse(input)
71    }
72}
73
74fn escaped_char(input: &str) -> IResult<&str, char> {
75    preceded(tag("\\"), anychar).parse(input)
76}
77
78pub(super) fn link_destination(input: &str) -> IResult<&str, String> {
79    alt((link_destination1, link_destination2)).parse(input)
80}
81
82fn link_destination1(input: &str) -> IResult<&str, String> {
83    let (input, _) = char('<').parse(input)?;
84
85    let (input, chars) = many0(alt((
86        preceded(char('\\'), one_of("<>")),
87        preceded(peek(not(one_of("\n<>"))), anychar),
88    )))
89    .parse(input)?;
90    let (input, _) = char('>').parse(input)?;
91
92    let v: String = chars.iter().collect();
93
94    Ok((input, v))
95}
96
97fn link_destination2(input: &str) -> IResult<&str, String> {
98    let (input, _) = peek(satisfy(|c| is_valid_char(c) && c != '<')).parse(input)?;
99
100    map(
101        recognize(many1(alt((
102            value((), escaped_char),
103            value((), balanced_parens),
104            value((), satisfy(|c| is_valid_char(c) && c != '(' && c != ')')),
105        )))),
106        |s: &str| s.to_string(),
107    )
108    .parse(input)
109}
110
111fn balanced_parens(input: &str) -> IResult<&str, String> {
112    delimited(
113        tag("("),
114        map(
115            fold_many0(
116                alt((
117                    map(escaped_char, |c| c.to_string()),
118                    map(balanced_parens, |s| format!("({s})")),
119                    map(satisfy(|c| is_valid_char(c) && c != '(' && c != ')'), |c| {
120                        c.to_string()
121                    }),
122                )),
123                String::new,
124                |mut acc, item| {
125                    acc.push_str(&item);
126                    acc
127                },
128            ),
129            |s| s,
130        ),
131        tag(")"),
132    )
133    .parse(input)
134}
135
136const fn is_valid_char(c: char) -> bool {
137    !c.is_ascii_control() && c != ' ' && c != '<'
138}
139