markdown_ppp/parser/inline/
emphasis.rs

1use crate::ast::Inline;
2use crate::parser::MarkdownParserState;
3use nom::{
4    branch::alt,
5    bytes::complete::tag,
6    character::complete::anychar,
7    combinator::{map, map_opt, not, peek, recognize, value, verify},
8    multi::many1,
9    sequence::{delimited, preceded},
10    IResult, Parser,
11};
12use std::rc::Rc;
13
14pub(crate) fn emphasis(
15    state: Rc<MarkdownParserState>,
16) -> impl FnMut(&str) -> IResult<&str, Inline> {
17    move |input: &str| {
18        alt((
19            map(
20                alt((
21                    delimited(
22                        open_tag("***"),
23                        emphasis_content(state.clone(), close_tag("***")),
24                        close_tag("***"),
25                    ),
26                    delimited(
27                        open_tag("___"),
28                        emphasis_content(state.clone(), close_tag("___")),
29                        close_tag("___"),
30                    ),
31                )),
32                |inner| Inline::Strong(vec![Inline::Emphasis(inner)]),
33            ),
34            map(
35                alt((
36                    delimited(
37                        open_tag("**"),
38                        emphasis_content(state.clone(), close_tag("**")),
39                        close_tag("**"),
40                    ),
41                    delimited(
42                        open_tag("__"),
43                        emphasis_content(state.clone(), close_tag("__")),
44                        close_tag("__"),
45                    ),
46                )),
47                Inline::Strong,
48            ),
49            map(
50                alt((
51                    delimited(
52                        open_tag("*"),
53                        emphasis_content(state.clone(), close_tag("*")),
54                        close_tag("*"),
55                    ),
56                    delimited(
57                        open_tag("_"),
58                        emphasis_content(state.clone(), close_tag("_")),
59                        close_tag("_"),
60                    ),
61                )),
62                Inline::Emphasis,
63            ),
64        ))
65        .parse(input)
66    }
67}
68
69fn emphasis_content<'a, P>(
70    state: Rc<MarkdownParserState>,
71    mut close_tag: P,
72) -> impl FnMut(&'a str) -> IResult<&'a str, Vec<Inline>>
73where
74    P: Parser<&'a str, Output = (), Error = nom::error::Error<&'a str>>,
75{
76    move |input: &str| {
77        let not_end = |i: &'a str| close_tag.parse(i);
78        map_opt(
79            recognize(many1(preceded(
80                peek(not(not_end)),
81                alt((value((), tag("\\*")), value((), anychar))),
82            ))),
83            |content: &str| {
84                crate::parser::inline::inline_many1(state.clone())
85                    .parse(content)
86                    .map(|(_, content)| content)
87                    .ok()
88            },
89        )
90        .parse(input)
91    }
92}
93
94fn open_tag(tag_value: &'static str) -> impl FnMut(&str) -> IResult<&str, ()> {
95    move |input: &str| {
96        value(
97            (),
98            verify(tag(tag_value), |v: &str| {
99                can_open(v.chars().next().unwrap(), input.chars().nth(v.len()))
100            }),
101        )
102        .parse(input)
103    }
104}
105
106fn can_open(marker: char, next: Option<char>) -> bool {
107    let left_flanking = next.is_some_and(|c| !c.is_whitespace())
108        && (next.is_some_and(|c| !is_punctuation(c)) || (next.is_some_and(is_punctuation)));
109    if !left_flanking {
110        return false;
111    }
112    if marker == '_' {
113        let right_flanking = next.is_none_or(|c| c.is_whitespace() || is_punctuation(c));
114        return !right_flanking;
115    }
116    true
117}
118
119fn close_tag(tag_value: &'static str) -> impl FnMut(&str) -> IResult<&str, ()> {
120    move |input: &str| {
121        value(
122            (),
123            verify(tag(tag_value), |v: &str| {
124                can_close(v.chars().next().unwrap(), input.chars().nth(v.len()))
125            }),
126        )
127        .parse(input)
128    }
129}
130
131fn can_close(marker: char, next: Option<char>) -> bool {
132    let right_flanking = next.is_none_or(|c| c.is_whitespace() || is_punctuation(c));
133    if !right_flanking {
134        return false;
135    }
136
137    if marker == '_' {
138        let left_flanking = next.is_some_and(|c| !c.is_whitespace())
139            && (next.is_some_and(|c| !is_punctuation(c)))
140            || (next.is_some_and(is_punctuation));
141        return !left_flanking || next.is_some_and(is_punctuation);
142    }
143    true
144}
145
146fn is_punctuation(c: char) -> bool {
147    use unicode_categories::UnicodeCategories;
148    c.is_ascii_punctuation() || c.is_punctuation()
149}