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};

/// A [`DocumentationWriter`] that generates markdown documents.
///
/// # Examples
/// See [`DocumentationWriter`].
pub struct MarkdownWriter<W: Write> {
    writer: W,

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

    wrote_header: bool,
}

impl<W: Write> MarkdownWriter<W> {
    /// Create a new instance.
    pub fn new(w: W) -> Self {
        Self {
            writer: w,
            title: "".into(),
            subtitle: "".into(),
            license: "".into(),
            wrote_header: false,
        }
    }
}

impl<W: Write> DocumentationWriter for MarkdownWriter<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"usage: ")?;
        self.write_code_literal(usage)?;
        self.write_raw(b"\n")?;
        Ok(())
    }

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

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

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

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

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

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

    fn link(&mut self, text: &str, to: &str) -> Result<(), Self::Error> {
        if let Some(man) = to.strip_prefix("man:") {
            let paren_index = man.find('(').unwrap_or(man.len());
            self.write_raw(b"**")?;
            self.write_escaped(&man[..paren_index])?;
            self.write_raw(b"**")?;
            self.write_escaped(&man[paren_index..])?;
            return Ok(());
        }
        if text == "" {
            self.write_raw(b"<")?;
            self.write_raw(
                to.replace(" ", "%20")
                    .replace("(", "%28")
                    .replace(")", "%29")
                    .replace("<", "%3c")
                    .replace(">", "%3e")
                    .as_bytes(),
            )?;
            self.write_raw(b">")?;
        } else {
            self.write_raw(b"[")?;
            self.write_escaped(text)?;
            self.write_raw(b"](")?;
            self.write_raw(
                to.replace(" ", "%20")
                    .replace("(", "%28")
                    .replace(")", "%29")
                    .as_bytes(),
            )?;
            self.write_raw(b")")?;
        }
        Ok(())
    }

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

    fn option(&mut self, name: &str, default: &str) -> Result<(), Self::Error> {
        self.write_raw(b"- ")?;
        if default == "" {
            self.write_code_literal(name)?;
        } else {
            self.write_code_literal(format!("{}={}", name, default))?;
        }
        self.write_raw(b": ")?;
        // TODO support multiple paragraphs using indents
        Ok(())
    }

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

    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"\n\n## ")?;
        self.write_escaped(name)?;
        self.write_raw(b"\n")?;
        Ok(())
    }

    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.license != "" {
            self.write_raw(b"\n\n## License\n")?;
            let license = mem::take(&mut self.license);
            self.write_escaped(license.replace("\n", "  \n"))?;
        }
        Ok(())
    }
}

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

        if !self.title.is_empty() {
            self.write_raw(b"# ")?;
            let title = mem::take(&mut self.title);
            self.write_escaped(title)?;
            if !self.subtitle.is_empty() {
                self.write_raw(b" &ndash; ")?;
                let subtitle = mem::take(&mut self.subtitle);
                self.write_escaped(subtitle)?;
            }
        }
        self.write_raw(b"\n\n")?;

        Ok(())
    }

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

    fn write_escaped<'a>(&mut self, s: impl Into<Cow<'a, str>>) -> Result<(), io::Error> {
        self.writer.write_all(
            s.into()
                .replace('*', "\\*")
                .replace('_', "\\_")
                .replace('<', "\\<")
                .replace('[', "\\[")
                .replace('`', "\\`")
                .replace('#', "\\#")
                .replace('&', "\\&")
                .replace('.', "\\.")
                .as_bytes(),
        )
    }

    fn write_code_literal<'a>(&mut self, s: impl Into<Cow<'a, str>>) -> Result<(), io::Error> {
        let s = s.into();
        let mut backticks = "`".to_owned();
        loop {
            if !s.contains(&backticks) {
                self.write_raw(backticks.as_bytes())?;
                self.write_raw(s.as_bytes())?;
                self.write_raw(backticks.as_bytes())?;
                break;
            } else {
                backticks.push('`');
            }
        }
        Ok(())
    }
}