use std::{
fs,
io::{self, Write},
sync::Arc,
};
use cargo_metadata::{
Metadata, Package,
camino::{Utf8Path, Utf8PathBuf},
};
use miette::{IntoDiagnostic, NamedSource, WrapErr};
use pulldown_cmark::{Options, Parser};
use tempfile::NamedTempFile;
use crate::{Result, cli::App, config::Manifest, traits::PackageExt, with_source::WithSource};
mod contents;
mod marker;
#[derive(Debug, Clone)]
struct MarkdownFile {
path: Utf8PathBuf,
text: Arc<str>,
}
impl MarkdownFile {
fn new(package: &Package, path: &Utf8Path) -> Result<Self> {
let path = package.root_directory().join(path);
let text = fs::read_to_string(&path)
.into_diagnostic()
.wrap_err_with(|| {
format!(
"failed to read README of {package}: {path}",
package = package.name
)
})?
.into();
Ok(Self { path, text })
}
fn to_named_source(&self) -> NamedSource<Arc<str>> {
NamedSource::new(self.path.clone(), Arc::clone(&self.text))
}
}
type ManifestFile = WithSource<Manifest>;
pub(crate) fn sync_all(app: &App, workspace: &Metadata, package: &Package) -> Result<()> {
let manifest = ManifestFile::from_toml("package manifest", &package.manifest_path)?;
let _span = tracing::info_span!("sync", "{}", package.name).entered();
let paths = package
.readme
.as_deref()
.into_iter()
.chain(
manifest
.value()
.config()
.extra_targets
.iter()
.map(Utf8Path::new),
)
.collect::<Vec<_>>();
if paths.is_empty() {
bail!(
"no target files found. Please specify `package.readme` or `package.metadata.cargo-sync-rdme.extra-targets`"
);
}
for path in paths {
tracing::info!("syncing {path}...");
let markdown = MarkdownFile::new(package, path)?;
let parser = Parser::new_ext(&markdown.text, Options::all()).into_offset_iter();
let all_markers = marker::find_all(&markdown, &manifest, parser)?;
let replaces = all_markers.iter().map(|x| x.0.clone());
let all_contents = contents::create_all(replaces, app, &manifest, workspace, package)?;
let new_text = marker::replace_all(&markdown.text, &all_markers, &all_contents);
let changed = new_text.as_str() != &*markdown.text;
if !changed {
tracing::info!("already up-to-date {path}");
continue;
}
app.fix
.check_update_allowed(&markdown.path, &markdown.text, &new_text)?;
write_readme(&markdown.path, &new_text)
.into_diagnostic()
.wrap_err_with(|| format!("failed to write markdown file: {path}"))?;
tracing::info!("updated {path}");
}
Ok(())
}
pub(crate) fn write_readme(path: &Utf8Path, text: &str) -> io::Result<()> {
let output_dir = path.parent().unwrap();
let mut tempfile = NamedTempFile::new_in(output_dir)?;
tempfile.as_file_mut().write_all(text.as_bytes())?;
tempfile.as_file_mut().sync_data()?;
let file = tempfile.persist(path).map_err(|err| err.error)?;
file.sync_all()?;
drop(file);
Ok(())
}