cargo-sync-rdme 0.8.0

Cargo subcommand to synchronize README with crate documentation
use std::{fmt, sync::Arc};

use cargo_metadata::PackageName;
use miette::NamedSource;
use pulldown_cmark::Event;
use snafu::{Snafu, ensure};

use crate::{
    config::badge::BadgeMap,
    source::{SourceFile, SourceFilePath, Spanned},
    sync::{
        PackageSyncContext,
        contents::Contents,
        marker::resolve::{ResolveMarkerError, Resolver},
    },
};

mod parse;
mod resolve;
mod scan;

const MAGIC: &str = "cargo-sync-rdme";

#[derive(Debug, Snafu, miette::Diagnostic)]
#[snafu(display(
    "failed to parse `<!-- {MAGIC} ... -->` markers in markdown file for package `{package}`: {markdown}",
    markdown = markdown.workspace_path,
))]
pub(crate) struct ParseMarkersError {
    package: PackageName,
    markdown: SourceFilePath,
    #[source_code]
    source_code: NamedSource<Arc<str>>,
    #[related]
    errors: Vec<ResolveMarkerError>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum ResolvedReplaceSpecifier<'a> {
    Title,
    Badge {
        group: Option<&'a str>,
        badges: &'a BadgeMap,
    },
    Rustdoc,
}

impl fmt::Display for ResolvedReplaceSpecifier<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Title => write!(f, "title"),
            Self::Badge { group, .. } => {
                if let Some(name) = group {
                    write!(f, "badge:{name}")
                } else {
                    write!(f, "badge")
                }
            }
            Self::Rustdoc => write!(f, "rustdoc"),
        }
    }
}

pub(super) fn parse_markers<'cx>(
    cx: &'cx PackageSyncContext<'_>,
    markdown: &SourceFile,
) -> Result<Vec<Spanned<ResolvedReplaceSpecifier<'cx>>>, Box<ParseMarkersError>> {
    let mut resolver = Resolver::new(cx, markdown.text());
    let mut specifiers = vec![];
    let mut errors = vec![];

    while let Some(res) = resolver.try_next().transpose() {
        match res {
            Ok(specifier) => specifiers.push(specifier),
            Err(err) => errors.push(err),
        }
    }

    ensure!(
        errors.is_empty(),
        ParseMarkersSnafu {
            package: cx,
            markdown,
            source_code: markdown.to_named_source(),
            errors
        }
    );

    Ok(specifiers)
}

pub(super) fn make_marked_contents(contents: &Contents<'_>) -> String {
    let specifier = contents.specifier();
    let text = contents.text();
    if text.is_empty() {
        format!("<!-- {MAGIC} {specifier} -->")
    } else {
        format!("<!-- {MAGIC} {specifier} [[ -->\n{text}<!-- {MAGIC} ]] -->")
    }
}

pub(super) fn escape_marker(event: &mut Event<'_>) {
    parse::escape_marker_comment(event);
}