cargo-sync-rdme 0.8.0

Cargo subcommand to synchronize README with crate documentation
use std::fmt;

use snafu::{Snafu, ensure};

use crate::{source::Spanned, sync::PackageSyncContext};

use super::marker::ResolvedReplaceSpecifier;

mod badge;
mod rustdoc;
mod title;

pub(super) fn create_all<'a>(
    cx: &PackageSyncContext<'_>,
    specifiers: Vec<Spanned<ResolvedReplaceSpecifier<'a>>>,
) -> Result<Vec<Contents<'a>>, CreateAllContentsError> {
    let mut contents = vec![];
    let mut errors = vec![];
    for specifier in specifiers {
        let res = create_content(cx, specifier);
        match res {
            Ok(c) => contents.push(c),
            Err(err) => errors.push(err),
        }
    }

    ensure!(errors.is_empty(), CreateAllContentsSnafu { errors });

    Ok(contents)
}

#[derive(Debug, Snafu, miette::Diagnostic)]
#[snafu(display("failed to create replacement contents"))]
pub(crate) struct CreateAllContentsError {
    #[related]
    errors: Vec<CreateContentsError>,
}

#[derive(Debug, Snafu, miette::Diagnostic)]
pub(super) enum CreateContentsError {
    #[snafu(transparent)]
    #[diagnostic(transparent)]
    CreateBadge {
        #[snafu(source)]
        #[diagnostic_source]
        source: badge::CreateAllBadgesError,
    },
    #[snafu(transparent)]
    #[diagnostic(transparent)]
    CreateRustdoc {
        #[snafu(source)]
        #[diagnostic_source]
        source: rustdoc::CreateRustdocError,
    },
}

#[derive(Debug, Clone)]
pub(super) struct Contents<'a> {
    specifier: Spanned<ResolvedReplaceSpecifier<'a>>,
    text: String,
}

fn create_content<'a>(
    cx: &PackageSyncContext<'_>,
    specifier: Spanned<ResolvedReplaceSpecifier<'a>>,
) -> Result<Contents<'a>, CreateContentsError> {
    let text = match specifier.value {
        ResolvedReplaceSpecifier::Title => title::create(cx),
        ResolvedReplaceSpecifier::Badge { group: _, badges } => badge::create_all(cx, badges)?,
        ResolvedReplaceSpecifier::Rustdoc => rustdoc::create(cx)?,
    };

    assert!(text.is_empty() || text.ends_with('\n'));

    Ok(Contents { specifier, text })
}

impl Contents<'_> {
    pub(super) fn specifier(&self) -> &Spanned<ResolvedReplaceSpecifier<'_>> {
        &self.specifier
    }

    pub(super) fn text(&self) -> &str {
        &self.text
    }
}

struct Escape<'s>(&'s str, &'s [char]);

impl fmt::Display for Escape<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut s = self.0;
        while let Some(idx) = s.find(self.1) {
            f.write_str(&s[..idx])?;
            write!(f, r"\{}", s.as_bytes()[idx] as char)?;
            s = &s[idx + 1..];
        }
        f.write_str(s)?;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use similar_asserts::assert_eq;

    #[test]
    fn escape() {
        let need_escape = [
            '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!',
        ];

        assert_eq!(Escape(r"foo", &need_escape).to_string(), r"foo");
        assert_eq!(Escape(r"`foobar", &need_escape).to_string(), r"\`foobar");
        assert_eq!(Escape(r"foo*bar", &need_escape).to_string(), r"foo\*bar");
        assert_eq!(Escape(r"foobar_", &need_escape).to_string(), r"foobar\_");
        assert_eq!(
            Escape(r"`foo*bar_", &need_escape).to_string(),
            r"\`foo\*bar\_"
        );
        assert_eq!(
            Escape(r"\foo\bar\", &need_escape).to_string(),
            r"\\foo\\bar\\"
        );
        assert_eq!(Escape(r"*", &need_escape).to_string(), r"\*");
    }
}