doc-writer 0.2.0

Generate documentation in multiple formats.
Documentation
use crate::DocumentationWriter;
use std::borrow::Cow;
use std::io::Write;
use std::{io, mem};

// see MAN-PAGES(7) and GROFF_MAN(7)

macro_rules! setter {
    ($(#[$doc:meta])* $var:ident, $with:ident, $set:ident, $typ:ty) => {
        $(#[$doc])*
        ///
        /// Also see
        #[doc=concat!("[", stringify!($set), "](Self::", stringify!($set), ")")]
        #[inline]
        pub fn $with(mut self, $var: $typ) -> Self {
            self.$var = $var;
            self
        }

        $(#[$doc])*
        ///
        /// Also see
        #[doc=concat!("[", stringify!($with), "](Self::", stringify!($with), ")")]
        #[inline]
        pub fn set_section(&mut self, $var: $typ) {
            self.$var = $var;
        }
    };
    ($var:ident, $with:ident, $set:ident) => {
        setter!($var, $with, $set, Cow<'static, str>);
    };
}

/// A [`DocumentationWriter`] that generates man pages.
///
/// # Examples
/// See [`DocumentationWriter`]
pub struct ManWriter<W: Write> {
    writer: W,

    section: &'static str,
    title: Cow<'static, str>,
    subtitle: Cow<'static, str>,
    license: Cow<'static, str>,

    wrote_header: bool,
    see_also: Vec<(String, String)>,
}

impl<W: Write> ManWriter<W> {
    /// Create a new instance.
    pub fn new(w: W) -> Self {
        Self {
            writer: w,
            section: "",
            title: "".into(),
            subtitle: "".into(),
            license: "".into(),
            wrote_header: false,
            see_also: vec![],
        }
    }

    setter!(
        /// Set the manual section.
        ///
        /// Section | Contents
        /// --------|---------
        /// 1 | Executable programs or shell commands
        /// 2 | System calls (functions provided by the kernel)
        /// 3 | Library calls (functions within program libraries)
        /// 4 | Special files (usually found in /dev)
        /// 5 | File formats and conventions, e.g. /etc/passwd
        /// 6 | Games
        /// 7 | Miscellaneous (including macro packages and conventions), e.g. man(7), groff(7)
        /// 8 | System administration commands (usually only for root)
        /// 9 | Kernel routines [Non standard]
        section,
        with_section,
        set_section,
        &'static str
    );
}

impl<W: Write> DocumentationWriter for ManWriter<W> {
    type Error = io::Error;

    fn set_title(&mut self, title: Cow<'static, str>) {
        self.title = title;
    }

    fn set_subtitle(&mut self, subtitle: Cow<'static, str>) {
        self.subtitle = subtitle;
    }

    fn set_license(&mut self, license: Cow<'static, str>) {
        self.license = license;
    }

    fn usage(&mut self, usage: &str) -> Result<(), Self::Error> {
        self.write_header_once()?;
        self.write_raw(b".SH \"SYNOPSIS\"\n")?;
        let mut args_iter = usage.splitn(2, ' ');
        if let Some(command) = args_iter.next() {
            self.emphasis(command)?;
            if let Some(args) = args_iter.next() {
                self.write_escaped_para(args)?;
                self.write_raw(b"\n")?;
            }
        }
        Ok(())
    }

    fn start_description(&mut self) -> Result<(), Self::Error> {
        self.write_header_once()?;
        self.write_raw(b".SH \"DESCRIPTION\"\n")
    }

    fn start_section(&mut self, name: &str) -> Result<(), Self::Error> {
        self.write_header_once()?;
        self.write_raw(b".SH ")?;
        self.write_escaped_str(name.to_uppercase())?;
        self.write_raw(b"\n")
    }

    fn plain(&mut self, s: &str) -> Result<(), Self::Error> {
        self.write_escaped_para(s.trim())?;
        self.write_raw(b"\n")
    }

    fn paragraph_break(&mut self) -> Result<(), Self::Error> {
        self.write_raw(b".P\n")
    }

    fn emphasis(&mut self, text: &str) -> Result<(), Self::Error> {
        self.write_raw(b".I ")?;
        self.write_escaped_para(text)?;
        self.write_raw(b"\n")
    }

    fn strong(&mut self, text: &str) -> Result<(), Self::Error> {
        self.write_raw(b".B ")?;
        self.write_escaped_para(text)?;
        self.write_raw(b"\n")
    }

    fn link(&mut self, text: &str, to: &str) -> Result<(), Self::Error> {
        self.see_also.push((text.to_owned(), to.to_owned()));
        if let Some(man) = to.strip_prefix("man:") {
            if text != "" {
                self.plain(text)?;
                self.write_raw(b" (")?;
            }
            self.write_raw(b".BR ")?;
            self.write_escaped_para(man.replace("(", " ("))?;
            if text != "" {
                self.write_raw(b" )")?;
            }
            self.write_raw(b"\n")?;
        } else if let Some(mail) = to.strip_prefix("mailto:") {
            self.write_raw(b".MT ")?;
            self.write_escaped_para(mail)?;
            self.write_raw(b"\n")?;
            if text != "" {
                self.write_escaped_para(text)?;
            } else {
                self.write_escaped_para(mail)?;
            }
            self.write_raw(b"\n.ME\n")?;
        } else {
            self.write_raw(b".UR ")?;
            self.write_escaped_para(to)?;
            self.write_raw(b"\n")?;
            if text != "" {
                self.write_escaped_para(text)?;
            } else {
                self.write_escaped_para(to)?;
            }
            self.write_raw(b"\n.UE\n")?;
        }
        Ok(())
    }

    fn start_options(&mut self) -> Result<(), Self::Error> {
        self.write_header_once()?;
        self.write_raw(b".SH \"OPTIONS\"\n")
    }

    fn option(&mut self, name: &str, default: &str) -> Result<(), Self::Error> {
        self.write_raw(br#".IP "\fI"#)?;
        self.write_escaped_para(name)?;
        if default != "" {
            self.write_raw(b"=")?;
            self.write_escaped_para(default)?;
        }
        self.write_raw(b"\\fP\" 10\n")
    }

    fn start_environment(&mut self) -> Result<(), Self::Error> {
        self.write_header_once()?;
        self.write_raw(b".SH \"ENVIRONMENT\"\n")
    }

    fn variable(&mut self, name: &str, default: &str) -> Result<(), Self::Error> {
        self.option(name, default)
    }

    fn start_enum(&mut self, name: &str) -> Result<(), Self::Error> {
        self.write_header_once()?;
        self.write_raw(b".SH ")?;
        self.write_escaped_str(name)?;
        self.write_raw(b"\n")
    }

    fn variant(&mut self, name: &str) -> Result<(), Self::Error> {
        self.option(name, "")
    }

    fn finish(mut self) -> Result<(), Self::Error> {
        self.write_header_once()?;
        if !self.see_also.is_empty() {
            self.write_raw(b".SH \"SEE ALSO\"\n")?;
            let see_also = mem::take(&mut self.see_also);
            for also in see_also {
                self.link(&also.0, &also.1)?;
            }
        }

        if self.license != "" {
            self.write_raw(b"\n.SH \"COPYRIGHT\"")?;
            let license = mem::take(&mut self.license);
            let mut lines = license.split('\n').peekable();
            while let Some(line) = lines.next() {
                self.write_raw(b"\n")?;
                self.write_escaped_para(line)?;
                match lines.peek() {
                    Some(&"") => {
                        self.write_raw(b"\n.P")?;
                        lines.next();
                    }
                    Some(_) => self.write_raw(b"\n.br")?,
                    None => {
                        lines.next();
                    }
                }
            }
        }
        Ok(())
    }
}

impl<W: Write> ManWriter<W> {
    fn write_header_once(&mut self) -> Result<(), io::Error> {
        if self.wrote_header {
            return Ok(());
        }
        self.wrote_header = true;

        if self.title != "" {
            let title = mem::take(&mut self.title);

            self.write_raw(b".TH ")?;
            self.write_escaped_str(title.to_uppercase())?;
            if self.section != "" {
                self.write_raw(b" ")?;
                self.write_escaped_str(self.section)?;
            } else {
                self.write_raw(b" \"1\"")?;
            }
            self.write_raw(b"\n")?;

            self.write_raw(b".SH \"NAME\"\n")?;
            self.write_escaped_para(title)?;

            if self.subtitle != "" {
                self.write_raw(b"\n\\(em ")?;
                let subtitle = mem::take(&mut self.subtitle);
                self.write_escaped_para(subtitle)?;
            }
            self.write_raw(b"\n")?;
        }

        Ok(())
    }

    fn write_raw(&mut self, s: &[u8]) -> Result<(), io::Error> {
        self.writer.write_all(s)
    }

    fn write_escaped_para<'a>(&mut self, s: impl Into<Cow<'a, str>>) -> Result<(), io::Error> {
        let s = s.into();
        if s.starts_with(&['.', '\''][..]) {
            self.write_raw(b"\\&")?;
        }
        self.writer
            .write_all(s.replace('"', "\\(dq").replace('\\', "\\e").as_bytes())
    }

    fn write_escaped_str<'a>(&mut self, s: impl Into<Cow<'a, str>>) -> Result<(), io::Error> {
        self.write_raw(b"\"")?;
        self.write_escaped_para(s.into())?;
        self.write_raw(b"\"")
    }
}