#
mdman/
lib.rs

    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
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133

    //! mdman markdown to man converter.
    //!
    //! > This crate is maintained by the Cargo team, primarily for use by Cargo
    //! > and not intended for external use (except as a transitive dependency). This
    //! > crate may make major changes to its APIs or be deprecated without warning.

    use anyhow::{bail, Context, Error};
    use pulldown_cmark::{CowStr, Event, LinkType, Options, Parser, Tag, TagEnd};
    use std::collections::HashMap;
    use std::fs;
    use std::io::{self, BufRead};
    use std::ops::Range;
    use std::path::Path;
    use url::Url;

    mod format;
    mod hbs;
    mod util;

    use format::Formatter;

    /// Mapping of `(name, section)` of a man page to a URL.
    pub type ManMap = HashMap<(String, u8), String>;

    /// A man section.
    pub type Section = u8;

    /// The output formats supported by mdman.
    #[derive(Copy, Clone)]
    pub enum Format {
        Man,
        Md,
        Text,
    }

    impl Format {
        /// The filename extension for the format.
        pub fn extension(&self, section: Section) -> String {
            match self {
                Format::Man => section.to_string(),
                Format::Md => "md".to_string(),
                Format::Text => "txt".to_string(),
            }
        }
    }

    /// Converts the handlebars markdown file at the given path into the given
    /// format, returning the translated result.
    pub fn convert(
        file: &Path,
        format: Format,
        url: Option<Url>,
        man_map: ManMap,
    ) -> Result<String, Error> {
        let formatter: Box<dyn Formatter + Send + Sync> = match format {
            Format::Man => Box::new(format::man::ManFormatter::new(url)),
            Format::Md => Box::new(format::md::MdFormatter::new(man_map)),
            Format::Text => Box::new(format::text::TextFormatter::new(url)),
        };
        let expanded = hbs::expand(file, &*formatter)?;
        // pulldown-cmark can behave a little differently with Windows newlines,
        // just normalize it.
        let expanded = expanded.replace("\r\n", "\n");
        formatter.render(&expanded)
    }

    /// Pulldown-cmark iterator yielding an `(event, range)` tuple.
    type EventIter<'a> = Box<dyn Iterator<Item = (Event<'a>, Range<usize>)> + 'a>;

    /// Creates a new markdown parser with the given input.
    pub(crate) fn md_parser(input: &str, url: Option<Url>) -> EventIter<'_> {
        let mut options = Options::empty();
        options.insert(Options::ENABLE_TABLES);
        options.insert(Options::ENABLE_FOOTNOTES);
        options.insert(Options::ENABLE_STRIKETHROUGH);
        options.insert(Options::ENABLE_SMART_PUNCTUATION);
        let parser = Parser::new_ext(input, options);
        let parser = parser.into_offset_iter();
        // Translate all links to include the base url.
        let parser = parser.map(move |(event, range)| match event {
            Event::Start(Tag::Link {
                link_type,
                dest_url,
                title,
                id,
            }) if !matches!(link_type, LinkType::Email) => (
                Event::Start(Tag::Link {
                    link_type,
                    dest_url: join_url(url.as_ref(), dest_url),
                    title,
                    id,
                }),
                range,
            ),
            Event::End(TagEnd::Link) => (Event::End(TagEnd::Link), range),
            _ => (event, range),
        });
        Box::new(parser)
    }

    fn join_url<'a>(base: Option<&Url>, dest: CowStr<'a>) -> CowStr<'a> {
        match base {
            Some(base_url) => {
                // Absolute URL or page-relative anchor doesn't need to be translated.
                if dest.contains(':') || dest.starts_with('#') {
                    dest
                } else {
                    let joined = base_url.join(&dest).unwrap_or_else(|e| {
                        panic!("failed to join URL `{}` to `{}`: {}", dest, base_url, e)
                    });
                    String::from(joined).into()
                }
            }
            None => dest,
        }
    }

    pub fn extract_section(file: &Path) -> Result<Section, Error> {
        let f = fs::File::open(file).with_context(|| format!("could not open `{}`", file.display()))?;
        let mut f = io::BufReader::new(f);
        let mut line = String::new();
        f.read_line(&mut line)?;
        if !line.starts_with("# ") {
            bail!("expected input file to start with # header");
        }
        let (_name, section) = util::parse_name_and_section(&line[2..].trim()).with_context(|| {
            format!(
                "expected input file to have header with the format `# command-name(1)`, found: `{}`",
                line
            )
        })?;
        Ok(section)
    }