use thiserror::Error;
pub const MANAGED_START: &str = "<!-- klasp:managed:start -->";
pub const MANAGED_END: &str = "<!-- klasp:managed:end -->";
pub const DEFAULT_BLOCK_BODY: &str = "## klasp gate\n\
\n\
This region is managed by `klasp install`. Do not edit by hand —\n\
re-run `klasp install` instead. It will be populated with the\n\
gate-invocation guidance once the v0.2 git hooks ship.\n";
#[derive(Debug, Error)]
pub enum AgentsMdError {
#[error(
"AGENTS.md: managed-block markers are malformed \
(expected exactly one `{MANAGED_START}` followed by one `{MANAGED_END}`). \
Fix the file by hand or remove both markers and re-run install."
)]
MalformedMarkers,
}
pub fn render_managed_block(body: &str) -> String {
let trimmed = body.trim_end_matches('\n');
format!("{MANAGED_START}\n{trimmed}\n{MANAGED_END}\n")
}
pub fn install_block(existing: &str, body: &str) -> Result<String, AgentsMdError> {
let block = render_managed_block(body);
match find_block(existing)? {
Some(span) => {
let mut out = String::with_capacity(existing.len() + block.len());
out.push_str(&existing[..span.start]);
out.push_str(&block);
out.push_str(&existing[span.end..]);
Ok(out)
}
None if existing.trim().is_empty() => {
Ok(block)
}
None => {
let mut out = String::with_capacity(existing.len() + block.len() + 2);
out.push_str(existing.trim_end_matches('\n'));
out.push_str("\n\n");
out.push_str(&block);
Ok(out)
}
}
}
pub fn uninstall_block(existing: &str) -> Result<String, AgentsMdError> {
let Some(span) = find_block(existing)? else {
return Ok(existing.to_string());
};
let before = &existing[..span.start];
let after = &existing[span.end..];
let mut out = String::with_capacity(before.len() + after.len() + 1);
if before.is_empty() {
out.push_str(after);
} else if after.is_empty() {
out.push_str(before.trim_end_matches('\n'));
out.push('\n');
} else {
out.push_str(before);
out.push_str(after);
}
Ok(out)
}
pub fn contains_block(existing: &str) -> Result<bool, AgentsMdError> {
Ok(find_block(existing)?.is_some())
}
struct Span {
start: usize,
end: usize,
}
fn find_block(existing: &str) -> Result<Option<Span>, AgentsMdError> {
let (Some(start), Some(end_marker_start)) =
(existing.find(MANAGED_START), existing.find(MANAGED_END))
else {
return if existing.contains(MANAGED_START) || existing.contains(MANAGED_END) {
Err(AgentsMdError::MalformedMarkers)
} else {
Ok(None)
};
};
if existing.rfind(MANAGED_START) != Some(start)
|| existing.rfind(MANAGED_END) != Some(end_marker_start)
|| end_marker_start < start
{
return Err(AgentsMdError::MalformedMarkers);
}
let after_marker = end_marker_start + MANAGED_END.len();
let end = if existing.as_bytes().get(after_marker) == Some(&b'\n') {
after_marker + 1
} else {
after_marker
};
Ok(Some(Span { start, end }))
}
#[cfg(test)]
mod tests {
use super::*;
fn body() -> &'static str {
DEFAULT_BLOCK_BODY
}
#[test]
fn render_block_wraps_body_in_markers() {
let s = render_managed_block("hello");
assert!(s.starts_with(MANAGED_START));
assert!(s.contains("hello"));
assert!(s.trim_end().ends_with(MANAGED_END));
assert!(s.ends_with('\n'));
}
#[test]
fn render_block_normalises_trailing_newlines_in_body() {
let s = render_managed_block("hello\n\n\n");
assert_eq!(s, format!("{MANAGED_START}\nhello\n{MANAGED_END}\n"));
}
#[test]
fn install_into_empty_emits_just_the_block() {
let out = install_block("", body()).unwrap();
assert!(out.starts_with(MANAGED_START));
assert!(out.trim_end().ends_with(MANAGED_END));
assert_eq!(out.chars().next(), Some('<'));
}
#[test]
fn install_into_whitespace_only_emits_just_the_block() {
let out = install_block("\n\n \n", body()).unwrap();
assert!(out.starts_with(MANAGED_START));
}
#[test]
fn install_appends_with_blank_line_separator_when_no_block_exists() {
let pre = "# Project\n\nSome notes from the user.\n";
let out = install_block(pre, body()).unwrap();
assert!(out.starts_with(pre));
let after_pre = &out[pre.len()..];
assert!(
after_pre.starts_with("\n"),
"expected separator newline, got: {after_pre:?}"
);
assert!(after_pre[1..].starts_with(MANAGED_START));
}
#[test]
fn install_replaces_existing_block_in_place() {
let stale_block = render_managed_block("OLD CONTENT");
let pre = format!("# Top\n\n{stale_block}\nbottom prose\n");
let out = install_block(&pre, "NEW CONTENT").unwrap();
assert!(out.contains("NEW CONTENT"));
assert!(!out.contains("OLD CONTENT"));
assert!(out.starts_with("# Top\n\n"));
assert!(out.ends_with("bottom prose\n"));
}
#[test]
fn install_is_idempotent() {
let pre = "# Project\n\nNotes.\n";
let once = install_block(pre, body()).unwrap();
let twice = install_block(&once, body()).unwrap();
assert_eq!(once, twice, "install is not idempotent");
}
#[test]
fn install_uninstall_round_trip_restores_original() {
let pre = "# Project\n\nSome notes from the user.\n";
let installed = install_block(pre, body()).unwrap();
let restored = uninstall_block(&installed).unwrap();
assert_eq!(restored, pre, "round-trip changed the file");
}
#[test]
fn install_uninstall_round_trip_on_empty_returns_empty() {
let installed = install_block("", body()).unwrap();
let restored = uninstall_block(&installed).unwrap();
assert_eq!(restored, "");
}
#[test]
fn uninstall_is_noop_when_no_block_present() {
let pre = "# Project\n\nNo klasp here.\n";
let out = uninstall_block(pre).unwrap();
assert_eq!(out, pre);
}
#[test]
fn uninstall_preserves_content_above_and_below_block() {
let stale_block = render_managed_block("between");
let pre = format!("ABOVE\n\n{stale_block}\nBELOW\n");
let out = uninstall_block(&pre).unwrap();
assert!(out.contains("ABOVE"));
assert!(out.contains("BELOW"));
assert!(!out.contains(MANAGED_START));
assert!(!out.contains(MANAGED_END));
assert!(!out.contains("between"));
}
#[test]
fn uninstall_is_idempotent() {
let pre = "# Project\n\nNotes.\n";
let installed = install_block(pre, body()).unwrap();
let once = uninstall_block(&installed).unwrap();
let twice = uninstall_block(&once).unwrap();
assert_eq!(once, twice);
}
#[test]
fn malformed_markers_are_rejected() {
let pre = format!("{MANAGED_START}\nbody — no closing marker\n");
let err = install_block(&pre, body()).expect_err("must fail");
assert!(matches!(err, AgentsMdError::MalformedMarkers));
}
#[test]
fn end_before_start_is_rejected() {
let pre = format!("{MANAGED_END}\nbody\n{MANAGED_START}\n");
let err = install_block(&pre, body()).expect_err("must fail");
assert!(matches!(err, AgentsMdError::MalformedMarkers));
}
#[test]
fn duplicate_start_marker_is_rejected() {
let pre =
format!("{MANAGED_START}\none\n{MANAGED_END}\n{MANAGED_START}\ntwo\n{MANAGED_END}\n");
let err = install_block(&pre, body()).expect_err("must fail");
assert!(matches!(err, AgentsMdError::MalformedMarkers));
}
#[test]
fn contains_block_returns_true_after_install() {
let installed = install_block("", body()).unwrap();
assert!(contains_block(&installed).unwrap());
}
#[test]
fn contains_block_returns_false_for_unrelated_html_comments() {
let pre = "<!-- generated by some-other-tool -->\n# Notes\n";
assert!(!contains_block(pre).unwrap());
}
}