    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112

    //! Markdown formatter.

    use crate::util::unwrap;
    use crate::ManMap;
    use anyhow::{bail, format_err, Error};
    use std::fmt::Write;

    pub struct MdFormatter {
        man_map: ManMap,
    }

    impl MdFormatter {
        pub fn new(man_map: ManMap) -> MdFormatter {
            MdFormatter { man_map }
        }
    }

    impl MdFormatter {
        fn render_html(&self, input: &str) -> Result<String, Error> {
            let parser = crate::md_parser(input, None);
            let mut html_output: String = String::with_capacity(input.len() * 3 / 2);
            pulldown_cmark::html::push_html(&mut html_output, parser.map(|(e, _r)| e));
            Ok(html_output)
        }
    }

    impl super::Formatter for MdFormatter {
        fn render(&self, input: &str) -> Result<String, Error> {
            Ok(input.replace("\r\n", "\n"))
        }

        fn render_options_start(&self) -> &'static str {
            "<dl>"
        }

        fn render_options_end(&self) -> &'static str {
            "</dl>"
        }

        fn render_option(&self, params: &[&str], block: &str, man_name: &str) -> Result<String, Error> {
            let mut result = String::new();
            fn unwrap_p(t: &str) -> &str {
                unwrap(t, "<p>", "</p>")
            }

            for param in params {
                let rendered = self.render_html(param)?;
                let no_p = unwrap_p(&rendered);
                // split out first term to use as the id.
                let first = no_p
                    .split_whitespace()
                    .next()
                    .ok_or_else(|| format_err!("did not expect option `{}` to be empty", param))?;
                let no_tags = trim_tags(first);
                if no_tags.is_empty() {
                    bail!("unexpected empty option with no tags `{}`", param);
                }
                let id = format!("option-{}-{}", man_name, no_tags);
                write!(
                    result,
                    "<dt class=\"option-term\" id=\"{ID}\">\
                    <a class=\"option-anchor\" href=\"#{ID}\"></a>{OPTION}</dt>\n",
                    ID = id,
                    OPTION = no_p
                )?;
            }
            let rendered_block = self.render_html(block)?;
            write!(
                result,
                "<dd class=\"option-desc\">{}</dd>\n",
                unwrap_p(&rendered_block)
            )?;
            Ok(result)
        }

        fn linkify_man_to_md(&self, name: &str, section: u8) -> Result<String, Error> {
            let s = match self.man_map.get(&(name.to_string(), section)) {
                Some(link) => format!("[{}({})]({})", name, section, link),
                None => format!("[{}({})]({}.html)", name, section, name),
            };
            Ok(s)
        }
    }

    fn trim_tags(s: &str) -> String {
        // This is a hack. It removes all HTML tags.
        let mut in_tag = false;
        let mut in_char_ref = false;
        s.chars()
            .filter(|&ch| match ch {
                '<' if in_tag => panic!("unexpected nested tag"),
                '&' if in_char_ref => panic!("unexpected nested char ref"),
                '<' => {
                    in_tag = true;
                    false
                }
                '&' => {
                    in_char_ref = true;
                    false
                }
                '>' if in_tag => {
                    in_tag = false;
                    false
                }
                ';' if in_char_ref => {
                    in_char_ref = false;
                    false
                }
                _ => !in_tag && !in_char_ref,
            })
            .collect()
    }