rst_parser/conversion/
inline.rs

1use anyhow::Error;
2use pest::iterators::Pair;
3
4use document_tree::{
5    CommonAttributes, HasChildren, attribute_types as at, element_categories as c, elements as e,
6    extra_attributes as a, url::Url,
7};
8
9use super::whitespace_normalize_name;
10use crate::pest_rst::Rule;
11
12pub fn convert_inline(pair: Pair<Rule>) -> Result<c::TextOrInlineElement, Error> {
13    Ok(match pair.as_rule() {
14        Rule::str | Rule::str_nested => pair.as_str().into(),
15        Rule::ws_newline => " ".to_owned().into(),
16        Rule::reference => convert_reference(pair)?,
17        Rule::substitution_name => convert_substitution_ref(&pair).into(),
18        Rule::emph => e::Emphasis::with_children(convert_inlines(pair)?).into(),
19        Rule::strong => e::Strong::with_children(convert_inlines(pair)?).into(),
20        Rule::literal => e::Literal::with_children(vec![pair.as_str().to_owned()]).into(),
21        rule => unimplemented!("unknown rule {:?}", rule),
22    })
23}
24
25pub fn convert_inlines(pair: Pair<Rule>) -> Result<Vec<c::TextOrInlineElement>, Error> {
26    pair.into_inner().map(convert_inline).collect()
27}
28
29fn convert_reference(pair: Pair<Rule>) -> Result<c::TextOrInlineElement, Error> {
30    let concrete = pair.into_inner().next().unwrap();
31    match concrete.as_rule() {
32        Rule::reference_target => convert_reference_target(concrete).map(Into::into),
33        Rule::reference_explicit => unimplemented!("explicit reference"),
34        Rule::reference_auto => Ok(convert_reference_auto(concrete)),
35        _ => unreachable!(),
36    }
37}
38
39fn convert_reference_target(concrete: Pair<'_, Rule>) -> Result<e::Reference, Error> {
40    let rt_inner = concrete.into_inner().next().unwrap();
41    Ok(match rt_inner.as_rule() {
42        Rule::reference_target_uq => e::Reference::new(
43            CommonAttributes::default(),
44            a::Reference {
45                name: Some(rt_inner.as_str().into()),
46                refuri: None,
47                refid: None,
48                refname: vec![rt_inner.as_str().into()],
49            },
50            vec![rt_inner.as_str().into()],
51        ),
52        Rule::reference_target_qu => {
53            let (text, reference) = {
54                let mut text = None;
55                let mut reference = None;
56                for inner in rt_inner.clone().into_inner() {
57                    match inner.as_rule() {
58                        Rule::reference_text => text = Some(inner),
59                        Rule::reference_bracketed => reference = Some(inner),
60                        _ => unreachable!(),
61                    }
62                }
63                (text, reference)
64            };
65            let trimmed_text = match (&text, &reference) {
66                (Some(text), None) => text.as_str(),
67                (_, Some(reference)) => text
68                    .map(|text| text.as_str().trim_end_matches(|ch| " \n\r".contains(ch)))
69                    .filter(|text| !text.is_empty())
70                    .unwrap_or_else(|| reference.clone().into_inner().next().unwrap().as_str()),
71                (None, None) => unreachable!(),
72            };
73            let (refuri, refname): (Option<Url>, Vec<at::NameToken>) =
74                if let Some(reference) = reference {
75                    let inner = reference.into_inner().next().unwrap();
76                    match inner.as_rule() {
77                        // The URL rules in our parser accept a narrow superset of
78                        // valid URLs, so we need to handle false positives.
79                        Rule::url => {
80                            if let Ok(target) = Url::parse_absolute(inner.as_str()) {
81                                (Some(target), Vec::new())
82                            } else if inner.as_str().ends_with('_') {
83                                // like target_name_qu (minus the final underscore)
84                                let full_str = inner.as_str();
85                                (None, vec![full_str[0..full_str.len() - 1].into()])
86                            } else {
87                                // like relative_reference
88                                (Some(Url::parse_relative(inner.as_str())?), Vec::new())
89                            }
90                        }
91                        Rule::target_name_qu => (None, vec![inner.as_str().into()]),
92                        Rule::relative_reference => {
93                            (Some(Url::parse_relative(inner.as_str())?), Vec::new())
94                        }
95                        _ => unreachable!(),
96                    }
97                } else {
98                    (None, vec![trimmed_text.into()])
99                };
100            e::Reference::new(
101                CommonAttributes::default(),
102                a::Reference {
103                    name: Some(trimmed_text.into()),
104                    refuri,
105                    refid: None,
106                    refname,
107                },
108                vec![trimmed_text.into()],
109            )
110        }
111        _ => unreachable!(),
112    })
113}
114
115fn convert_reference_auto(concrete: Pair<'_, Rule>) -> c::TextOrInlineElement {
116    let rt_inner = concrete.into_inner().next().unwrap();
117    let str: c::TextOrInlineElement = rt_inner.as_str().into();
118    let Ok(target) = (match rt_inner.as_rule() {
119        Rule::url_auto => Url::parse_absolute(rt_inner.as_str()),
120        Rule::email => Url::parse_absolute(&format!("mailto:{}", rt_inner.as_str())),
121        _ => unreachable!(),
122    }) else {
123        // if our parser got a URL wrong, return it as a string
124        return str;
125    };
126    e::Reference::new(
127        CommonAttributes::default(),
128        a::Reference {
129            name: None,
130            refuri: Some(target),
131            refid: None,
132            refname: Vec::new(),
133        },
134        vec![str],
135    )
136    .into()
137}
138
139fn convert_substitution_ref(pair: &Pair<Rule>) -> e::SubstitutionReference {
140    let name = whitespace_normalize_name(pair.as_str());
141    a::ExtraAttributes::with_extra(a::SubstitutionReference {
142        refname: vec![at::NameToken(name)],
143    })
144}