use std::path::Path;
use anyhow::Context;
use crate::containment::PathGuard;
use crate::ops;
use crate::write::{WritePolicy, atomic_write};
use super::{
ApplyMode, EditResult, apply_cross_file_mutation, build_edit_result, ensure_contained,
write_if_apply,
};
fn md_edit<F>(
path: &Path,
heading: &str,
mode: ApplyMode,
guard: Option<&PathGuard>,
action: &'static str,
transform: F,
) -> anyhow::Result<EditResult>
where
F: FnOnce(&str, &str) -> Option<String>,
{
let path_str = path.to_string_lossy();
let original =
std::fs::read_to_string(path).with_context(|| format!("failed to read {path_str}"))?;
let new_content = transform(&original, heading)
.ok_or_else(|| anyhow::anyhow!("heading '{}' not found in {}", heading, path_str))?;
let policy = WritePolicy::default();
let applied = write_if_apply(path, &new_content, mode, &policy, guard)?;
Ok(build_edit_result(
&path_str,
original,
new_content,
applied,
action,
None,
))
}
pub fn md_replace_section(
path: &Path,
heading: &str,
content: &str,
mode: ApplyMode,
guard: Option<&PathGuard>,
) -> anyhow::Result<EditResult> {
md_edit(
path,
heading,
mode,
guard,
"md.replace_section",
|orig, h| ops::md::replace_section_in(orig, h, content),
)
}
pub fn md_upsert_bullet(
path: &Path,
heading: &str,
bullet: &str,
mode: ApplyMode,
guard: Option<&PathGuard>,
) -> anyhow::Result<EditResult> {
md_edit(path, heading, mode, guard, "md.upsert_bullet", |orig, h| {
ops::md::upsert_bullet_in(orig, h, bullet)
})
}
pub fn md_table_append(
path: &Path,
heading: &str,
row: &str,
mode: ApplyMode,
guard: Option<&PathGuard>,
) -> anyhow::Result<EditResult> {
md_edit(path, heading, mode, guard, "md.table_append", |orig, h| {
ops::md::table_append_for_tx(orig, h, row)
})
}
pub fn md_insert_after_heading(
path: &Path,
heading: &str,
insertion: &str,
mode: ApplyMode,
guard: Option<&PathGuard>,
) -> anyhow::Result<EditResult> {
md_edit(
path,
heading,
mode,
guard,
"md.insert_after_heading",
|orig, h| ops::md::insert_after_heading_in(orig, h, insertion),
)
}
pub fn md_move_section(
path: &Path,
heading: &str,
position: (&str, &str),
to: Option<&Path>,
mode: ApplyMode,
guard: Option<&PathGuard>,
) -> anyhow::Result<EditResult> {
let path_str = path.to_string_lossy();
let original = std::fs::read_to_string(path)
.with_context(|| format!("failed to read {}", path.display()))?;
let dest_content = match to {
Some(dest_path) => std::fs::read_to_string(dest_path)
.with_context(|| format!("failed to read {}", dest_path.display()))?,
None => original.clone(),
};
let (new_source, new_dest) =
ops::md::move_section_in(&original, heading, &dest_content, position, to.is_none())
.ok_or_else(|| anyhow::anyhow!("heading '{}' not found", heading))?;
let policy = WritePolicy::default();
if let Some(dest_path) = to {
if mode == ApplyMode::Apply {
ensure_contained(guard, dest_path)?;
}
let _ = write_if_apply(dest_path, &new_dest, mode, &policy, guard)?;
}
let applied = apply_cross_file_mutation(
path,
None,
mode,
guard,
|backup| backup.save_before_write(path),
|| atomic_write(path, &new_source, &policy),
)?;
let dest = to.map(|p| p.to_string_lossy().to_string());
Ok(build_edit_result(
&path_str, original, new_source, applied, "md.move", dest,
))
}
pub fn md_dedupe_headings(
path: &Path,
mode: ApplyMode,
guard: Option<&PathGuard>,
) -> anyhow::Result<(EditResult, Vec<String>)> {
let path_str = path.to_string_lossy();
let original = std::fs::read_to_string(path)
.with_context(|| format!("failed to read {}", path.display()))?;
let (new_content, removed) = ops::md::dedupe_headings_in(&original);
let policy = WritePolicy::default();
let applied = write_if_apply(path, &new_content, mode, &policy, guard)?;
Ok((
build_edit_result(&path_str, original, new_content, applied, "md.dedupe", None),
removed,
))
}
pub use crate::ops::md::LintIssue;
pub fn md_lint_agents(path: &Path) -> anyhow::Result<Vec<LintIssue>> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("failed to read {}", path.display()))?;
Ok(crate::ops::md::lint_agents_content(&content))
}
pub fn md_insert_before_heading(
path: &Path,
heading: &str,
insertion: &str,
mode: ApplyMode,
guard: Option<&PathGuard>,
) -> anyhow::Result<EditResult> {
md_edit(
path,
heading,
mode,
guard,
"md.insert_before_heading",
|orig, h| ops::md::insert_before_heading_in(orig, h, insertion),
)
}