rst_parser/conversion/
block.rs

1use anyhow::{Error, bail};
2use pest::iterators::Pair;
3
4use document_tree::{
5    Element, ExtraAttributes, HasChildren, attribute_types as at, element_categories as c,
6    elements as e, extra_attributes as a,
7};
8use uuid::Uuid;
9
10use super::{inline::convert_inlines, whitespace_normalize_name};
11use crate::{pair_ext_parse::PairExt, pest_rst::Rule};
12
13#[derive(PartialEq)]
14pub(super) enum TitleKind {
15    Double(char),
16    Single(char),
17}
18
19pub(super) enum TitleOrSsubel {
20    Title(e::Title, TitleKind),
21    Ssubel(c::StructuralSubElement),
22}
23
24pub(super) fn convert_ssubel(pair: Pair<Rule>) -> Result<Option<TitleOrSsubel>, Error> {
25    use self::TitleOrSsubel::{Ssubel, Title};
26    Ok(Some(match pair.as_rule() {
27        Rule::title => {
28            let (t, k) = convert_title(pair)?;
29            Title(t, k)
30        }
31        //TODO: subtitle, decoration, docinfo
32        Rule::EOI => return Ok(None),
33        _ => Ssubel(convert_substructure(pair)?.into()),
34    }))
35}
36
37fn convert_substructure(pair: Pair<Rule>) -> Result<c::SubStructure, Error> {
38    #[allow(clippy::match_single_binding)]
39    Ok(match pair.as_rule() {
40        // TODO: Topic, Sidebar, Transition
41        // no section here, as it’s constructed from titles
42        _ => convert_body_elem(pair)?.into(),
43    })
44}
45
46fn convert_body_elem(pair: Pair<Rule>) -> Result<c::BodyElement, Error> {
47    Ok(match pair.as_rule() {
48        Rule::paragraph => convert_paragraph(pair)?.into(),
49        Rule::target => convert_target(pair)?.into(),
50        Rule::footnote => convert_footnote(pair)?.into(),
51        Rule::substitution_def => convert_substitution_def(pair)?.into(),
52        Rule::block_quote_directive => convert_block_quote_directive(pair)?.into(),
53        Rule::admonition_gen => convert_admonition_gen(pair),
54        Rule::image => convert_image::<e::Image>(pair)?.into(),
55        Rule::bullet_list => convert_bullet_list(pair)?.into(),
56        Rule::block_quote => convert_block_quote(pair)?.into(),
57        Rule::literal_block => convert_literal_block(pair).into(),
58        Rule::code_directive => convert_code_directive(pair).into(),
59        Rule::raw_directive => convert_raw_directive(pair).into(),
60        Rule::block_comment => convert_comment(pair).into(),
61        rule => unimplemented!("unhandled rule {:?}", rule),
62    })
63}
64
65fn convert_title(pair: Pair<Rule>) -> Result<(e::Title, TitleKind), Error> {
66    let mut title: Option<String> = None;
67    let mut title_inlines: Option<Vec<c::TextOrInlineElement>> = None;
68    let mut adornment_char: Option<char> = None;
69    // title_double or title_single. Extract kind before consuming
70    let inner_pair = pair.into_inner().next().unwrap();
71    let kind = inner_pair.as_rule();
72    for p in inner_pair.into_inner() {
73        match p.as_rule() {
74            Rule::line => {
75                title = Some(p.as_str().to_owned());
76                title_inlines = Some(convert_inlines(p)?);
77            }
78            Rule::adornments => {
79                adornment_char = Some(p.as_str().chars().next().expect("Empty adornment?"));
80            }
81            rule => unimplemented!("Unexpected rule in title: {:?}", rule),
82        }
83    }
84    // now we encountered one line of text and one of adornments
85    // TODO: emit error if the adornment line is too short (has to match title length)
86    let mut elem = e::Title::with_children(title_inlines.expect("No text in title"));
87    if let Some(title) = title {
88        //TODO: slugify properly
89        let slug = title.to_lowercase().replace('\n', "").replace(' ', "-");
90        elem.names_mut().push(at::NameToken(slug));
91    }
92    let title_kind = match kind {
93        Rule::title_double => TitleKind::Double(adornment_char.unwrap()),
94        Rule::title_single => TitleKind::Single(adornment_char.unwrap()),
95        _ => unreachable!(),
96    };
97    Ok((elem, title_kind))
98}
99
100fn convert_paragraph(pair: Pair<Rule>) -> Result<e::Paragraph, Error> {
101    Ok(e::Paragraph::with_children(convert_inlines(pair)?))
102}
103
104fn convert_target(pair: Pair<Rule>) -> Result<e::Target, Error> {
105    let mut elem = e::Target::default();
106    elem.extra_mut().anonymous = false;
107    for p in pair.into_inner() {
108        match p.as_rule() {
109            Rule::target_name_uq | Rule::target_name_qu => {
110                elem.ids_mut().push(p.as_str().into());
111                elem.names_mut().push(p.as_str().into());
112            }
113            // TODO: also handle non-urls
114            Rule::link_target => elem.extra_mut().refuri = Some(p.parse()?),
115            rule => panic!("Unexpected rule in target: {rule:?}"),
116        }
117    }
118    Ok(elem)
119}
120
121fn convert_footnote(pair: Pair<Rule>) -> Result<e::Footnote, Error> {
122    let mut pairs = pair.into_inner();
123    let label = pairs.next().unwrap().as_str();
124    let mut children: Vec<c::SubFootnote> = vec![];
125    // turn `line` into paragraph
126    children.push(convert_paragraph(pairs.next().unwrap())?.into());
127    for p in pairs {
128        children.push(convert_body_elem(p)?.into());
129    }
130    let mut footnote = e::Footnote::with_children(children);
131    match label.chars().next().unwrap() {
132        '#' => {
133            if label.len() >= 2 {
134                footnote.names_mut().push(label[1..].into());
135            }
136            footnote.extra_mut().auto = Some(at::AutoFootnoteType::Number);
137        }
138        '*' => {
139            footnote.extra_mut().auto = Some(at::AutoFootnoteType::Symbol);
140        }
141        _ => {
142            footnote
143                .children_mut()
144                .insert(0, e::Label::with_children(vec![label.into()]).into());
145        }
146    }
147    footnote.ids_mut().push(at::ID(Uuid::new_v4().to_string()));
148    Ok(footnote)
149}
150
151fn convert_substitution_def(pair: Pair<Rule>) -> Result<e::SubstitutionDefinition, Error> {
152    let mut pairs = pair.into_inner();
153    let name = whitespace_normalize_name(pairs.next().unwrap().as_str()); // Rule::substitution_name
154    let inner_pair = pairs.next().unwrap();
155    let inner: Vec<c::TextOrInlineElement> = match inner_pair.as_rule() {
156        Rule::replace => convert_replace(inner_pair)?,
157        Rule::image => vec![convert_image::<e::ImageInline>(inner_pair)?.into()],
158        rule => panic!("Unknown substitution rule {rule:?}"),
159    };
160    let mut subst_def = e::SubstitutionDefinition::with_children(inner);
161    subst_def.names_mut().push(at::NameToken(name));
162    Ok(subst_def)
163}
164
165fn convert_replace(pair: Pair<Rule>) -> Result<Vec<c::TextOrInlineElement>, Error> {
166    let mut pairs = pair.into_inner();
167    let paragraph = pairs.next().unwrap();
168    convert_inlines(paragraph)
169}
170
171fn convert_image<I>(pair: Pair<Rule>) -> Result<I, Error>
172where
173    I: Element + ExtraAttributes<a::Image>,
174{
175    let mut pairs = pair.into_inner();
176    let mut image = I::with_extra(a::Image::new(
177        pairs.next().unwrap().as_str().trim().parse()?, // line
178    ));
179    for opt in pairs {
180        let mut opt_iter = opt.into_inner();
181        let opt_name = opt_iter.next().unwrap();
182        let opt_val = opt_iter.next().unwrap();
183        match opt_name.as_str() {
184            "class" => image.classes_mut().push(opt_val.as_str().to_owned()),
185            "name" => image.names_mut().push(opt_val.as_str().into()),
186            "alt" => image.extra_mut().alt = Some(opt_val.as_str().to_owned()),
187            "height" => image.extra_mut().height = Some(opt_val.parse()?),
188            "width" => image.extra_mut().width = Some(opt_val.parse()?),
189            "scale" => image.extra_mut().scale = Some(parse_scale(&opt_val)?),
190            "align" => image.extra_mut().align = Some(opt_val.parse()?),
191            "target" => image.extra_mut().target = Some(opt_val.parse()?),
192            name => bail!("Unknown Image option {}", name),
193        }
194    }
195    Ok(image)
196}
197
198fn parse_scale(pair: &Pair<Rule>) -> Result<u8, Error> {
199    use pest::error::{Error, ErrorVariant};
200
201    let input = pair.as_str().trim();
202    let input = if let Some(percentage) = input.strip_suffix('%') {
203        percentage.trim_end()
204    } else {
205        input
206    };
207    Ok(input.parse().map_err(|e: std::num::ParseIntError| {
208        let var: ErrorVariant<Rule> = ErrorVariant::CustomError {
209            message: e.to_string(),
210        };
211        Error::new_from_span(var, pair.as_span())
212    })?)
213}
214
215fn convert_admonition_gen(pair: Pair<Rule>) -> document_tree::element_categories::BodyElement {
216    let mut iter = pair.into_inner();
217    let typ = iter.next().unwrap().as_str();
218    // TODO: in reality it contains body elements.
219    let children: Vec<c::BodyElement> = iter
220        .map(|p| e::Paragraph::with_children(vec![p.as_str().into()]).into())
221        .collect();
222    match typ {
223        "attention" => e::Attention::with_children(children).into(),
224        "hint" => e::Hint::with_children(children).into(),
225        "note" => e::Note::with_children(children).into(),
226        "caution" => e::Caution::with_children(children).into(),
227        "danger" => e::Danger::with_children(children).into(),
228        "error" => e::Error::with_children(children).into(),
229        "important" => e::Important::with_children(children).into(),
230        "tip" => e::Tip::with_children(children).into(),
231        "warning" => e::Warning::with_children(children).into(),
232        typ => panic!("Unknown admontion type {typ}!"),
233    }
234}
235
236fn convert_bullet_list(pair: Pair<Rule>) -> Result<e::BulletList, Error> {
237    Ok(e::BulletList::with_children(
238        pair.into_inner()
239            .map(convert_bullet_item)
240            .collect::<Result<_, _>>()?,
241    ))
242}
243
244fn convert_bullet_item(pair: Pair<Rule>) -> Result<e::ListItem, Error> {
245    let mut iter = pair.into_inner();
246    let mut children: Vec<c::BodyElement> = vec![convert_paragraph(iter.next().unwrap())?.into()];
247    for p in iter {
248        children.push(convert_body_elem(p)?);
249    }
250    Ok(e::ListItem::with_children(children))
251}
252
253fn convert_block_quote(pair: Pair<Rule>) -> Result<e::BlockQuote, Error> {
254    Ok(e::BlockQuote::with_children(
255        pair.into_inner()
256            .map(convert_block_quote_inner)
257            .collect::<Result<_, _>>()?,
258    ))
259}
260
261fn convert_block_quote_directive(pair: Pair<Rule>) -> Result<e::BlockQuote, Error> {
262    let mut iter = pair.into_inner();
263    let typ = iter.next().unwrap().as_str();
264    let children: Vec<c::SubBlockQuote> = iter
265        .map(convert_block_quote_inner)
266        .collect::<Result<_, _>>()?;
267    let mut bq = e::BlockQuote::with_children(children);
268    bq.classes_mut().push(typ.to_owned());
269    Ok(bq)
270}
271
272fn convert_block_quote_inner(pair: Pair<Rule>) -> Result<c::SubBlockQuote, Error> {
273    Ok(if pair.as_rule() == Rule::attribution {
274        e::Attribution::with_children(convert_inlines(pair)?).into()
275    } else {
276        convert_body_elem(pair)?.into()
277    })
278}
279
280fn convert_literal_block(pair: Pair<Rule>) -> e::LiteralBlock {
281    convert_literal_lines(pair.into_inner().next().unwrap())
282}
283
284fn convert_literal_lines(pair: Pair<Rule>) -> e::LiteralBlock {
285    let children = pair
286        .into_inner()
287        .map(|l| {
288            match l.as_rule() {
289                Rule::literal_line => l.as_str(),
290                Rule::literal_line_blank => "\n",
291                _ => unreachable!(),
292            }
293            .into()
294        })
295        .collect();
296    e::LiteralBlock::with_children(children)
297}
298
299fn convert_code_directive(pair: Pair<Rule>) -> e::LiteralBlock {
300    let mut iter = pair.into_inner();
301    let (lang, code) = match (iter.next().unwrap(), iter.next()) {
302        (lang, Some(code)) => (Some(lang), code),
303        (code, None) => (None, code),
304    };
305    let mut code_block = convert_literal_lines(code);
306    code_block.classes_mut().push("code".to_owned());
307    if let Some(lang) = lang {
308        code_block.classes_mut().push(lang.as_str().to_owned());
309    }
310    code_block
311}
312
313fn convert_raw_directive(pair: Pair<Rule>) -> e::Raw {
314    let mut iter = pair.into_inner();
315    let format = iter.next().unwrap();
316    let block = iter.next().unwrap();
317    let children = block
318        .into_inner()
319        .map(|l| {
320            match l.as_rule() {
321                Rule::raw_line => l.as_str(),
322                Rule::raw_line_blank => "\n",
323                _ => unreachable!(),
324            }
325            .into()
326        })
327        .collect();
328    let mut raw_block = e::Raw::with_children(children);
329    raw_block
330        .extra_mut()
331        .format
332        .push(at::NameToken(format.as_str().to_owned()));
333    raw_block
334}
335
336fn convert_comment(pair: Pair<Rule>) -> e::Comment {
337    let lines = pair
338        .into_inner()
339        .map(|l| {
340            match l.as_rule() {
341                Rule::comment_line_blank => "\n",
342                Rule::comment_line => l.as_str(),
343                _ => unreachable!(),
344            }
345            .into()
346        })
347        .collect();
348    e::Comment::with_children(lines)
349}