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