markdown_ppp/parser/inline/
emphasis.rs1use 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}