use thiserror::Error;
#[derive(Debug, Clone, Copy)]
pub struct Markers<'a> {
pub start: &'a str,
pub end: &'a str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Span {
pub start: usize,
pub end: usize,
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum BlockError {
#[error(
"managed-block markers are malformed (expected exactly one start followed by one end)"
)]
MalformedMarkers,
}
#[derive(Debug, Clone, Copy)]
pub struct Prelude<'a> {
pub line: &'a str,
}
pub fn find_block(existing: &str, markers: &Markers<'_>) -> Result<Option<Span>, BlockError> {
let (Some(start), Some(end_marker_start)) =
(existing.find(markers.start), existing.find(markers.end))
else {
return if existing.contains(markers.start) || existing.contains(markers.end) {
Err(BlockError::MalformedMarkers)
} else {
Ok(None)
};
};
if existing.rfind(markers.start) != Some(start)
|| existing.rfind(markers.end) != Some(end_marker_start)
|| end_marker_start < start
{
return Err(BlockError::MalformedMarkers);
}
let after_marker = end_marker_start + markers.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 }))
}
pub fn contains_block(existing: &str, markers: &Markers<'_>) -> Result<bool, BlockError> {
Ok(find_block(existing, markers)?.is_some())
}
pub fn render_block(markers: &Markers<'_>, body: &str) -> String {
let trimmed = body.trim_end_matches('\n');
format!("{}\n{}\n{}\n", markers.start, trimmed, markers.end)
}
pub fn install_block(
existing: &str,
markers: &Markers<'_>,
body: &str,
prelude: Option<Prelude<'_>>,
) -> Result<String, BlockError> {
let block = render_block(markers, body);
if let Some(span) = find_block(existing, markers)? {
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..]);
return Ok(out);
}
if existing.trim().is_empty() {
return Ok(match prelude {
None => block,
Some(p) => {
let mut out = String::with_capacity(p.line.len() + block.len() + 2);
out.push_str(p.line);
out.push_str("\n\n");
out.push_str(&block);
out
}
});
}
let needs_prelude = match prelude {
Some(_) => !has_shebang(existing),
None => false,
};
let prelude_line = prelude.map(|p| p.line).unwrap_or("");
let mut out = String::with_capacity(existing.len() + prelude_line.len() + block.len() + 4);
if needs_prelude {
out.push_str(prelude_line);
out.push_str("\n\n");
}
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,
markers: &Markers<'_>,
prelude: Option<Prelude<'_>>,
) -> Result<String, BlockError> {
let Some(span) = find_block(existing, markers)? 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() && prelude.is_some() && is_only_shebang_or_whitespace(before) {
} 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)
}
fn has_shebang(s: &str) -> bool {
s.starts_with("#!")
}
fn is_only_shebang_or_whitespace(s: &str) -> bool {
let trimmed = s.trim();
trimmed.is_empty() || (trimmed.starts_with("#!") && !trimmed.contains('\n'))
}
#[cfg(test)]
mod tests {
use super::*;
const MD: Markers<'static> = Markers {
start: "<!-- start -->",
end: "<!-- end -->",
};
const SH: Markers<'static> = Markers {
start: "# >>> start <<<",
end: "# >>> end <<<",
};
const SHEBANG: Prelude<'static> = Prelude {
line: "#!/usr/bin/env sh",
};
#[test]
fn render_block_wraps_body_in_markers() {
let s = render_block(&MD, "hello");
assert!(s.starts_with(MD.start));
assert!(s.contains("hello"));
assert!(s.trim_end().ends_with(MD.end));
assert!(s.ends_with('\n'));
}
#[test]
fn render_block_normalises_trailing_newlines_in_body() {
let s = render_block(&MD, "hello\n\n\n");
assert_eq!(s, format!("{}\nhello\n{}\n", MD.start, MD.end));
}
#[test]
fn find_block_none_when_absent() {
assert_eq!(find_block("# Project\nNotes.\n", &MD).unwrap(), None);
}
#[test]
fn find_block_rejects_lone_marker() {
let pre = format!("{}\nbody\n", MD.start);
assert_eq!(find_block(&pre, &MD), Err(BlockError::MalformedMarkers));
}
#[test]
fn find_block_rejects_end_before_start() {
let pre = format!("{}\nbody\n{}\n", MD.end, MD.start);
assert_eq!(find_block(&pre, &MD), Err(BlockError::MalformedMarkers));
}
#[test]
fn find_block_rejects_duplicates() {
let pre = format!("{s}\none\n{e}\n{s}\ntwo\n{e}\n", s = MD.start, e = MD.end);
assert_eq!(find_block(&pre, &MD), Err(BlockError::MalformedMarkers));
}
#[test]
fn install_no_prelude_into_empty_emits_just_the_block() {
let out = install_block("", &MD, "body", None).unwrap();
assert!(out.starts_with(MD.start));
assert!(out.trim_end().ends_with(MD.end));
}
#[test]
fn install_no_prelude_appends_with_blank_line_separator() {
let pre = "# Project\n\nNotes.\n";
let out = install_block(pre, &MD, "body", None).unwrap();
assert!(out.starts_with(pre));
let after_pre = &out[pre.len()..];
assert!(after_pre.starts_with('\n'));
assert!(after_pre[1..].starts_with(MD.start));
}
#[test]
fn install_no_prelude_replaces_in_place() {
let stale = render_block(&MD, "OLD");
let pre = format!("# Top\n\n{stale}\nbottom\n");
let out = install_block(&pre, &MD, "NEW", None).unwrap();
assert!(out.contains("NEW"));
assert!(!out.contains("OLD"));
assert!(out.starts_with("# Top\n\n"));
assert!(out.ends_with("bottom\n"));
}
#[test]
fn install_no_prelude_is_idempotent() {
let pre = "# Project\n\nNotes.\n";
let once = install_block(pre, &MD, "body", None).unwrap();
let twice = install_block(&once, &MD, "body", None).unwrap();
assert_eq!(once, twice);
}
#[test]
fn round_trip_no_prelude_restores_original() {
let pre = "# Project\n\nNotes.\n";
let installed = install_block(pre, &MD, "body", None).unwrap();
let restored = uninstall_block(&installed, &MD, None).unwrap();
assert_eq!(restored, pre);
}
#[test]
fn round_trip_no_prelude_on_empty_returns_empty() {
let installed = install_block("", &MD, "body", None).unwrap();
let restored = uninstall_block(&installed, &MD, None).unwrap();
assert_eq!(restored, "");
}
#[test]
fn install_prelude_into_empty_emits_shebang_and_block() {
let out = install_block("", &SH, "body", Some(SHEBANG)).unwrap();
assert!(out.starts_with(SHEBANG.line));
assert!(out.contains(SH.start));
assert!(out.trim_end().ends_with(SH.end));
}
#[test]
fn install_prelude_into_hook_with_shebang_appends() {
let pre = "#!/bin/bash\n\necho 'user lint'\n";
let out = install_block(pre, &SH, "body", Some(SHEBANG)).unwrap();
assert!(out.starts_with(pre));
let after_pre = &out[pre.len()..];
assert!(after_pre.starts_with('\n'));
assert!(after_pre[1..].starts_with(SH.start));
}
#[test]
fn install_prelude_into_hook_without_shebang_prepends_one() {
let pre = "echo lint\n";
let out = install_block(pre, &SH, "body", Some(SHEBANG)).unwrap();
assert!(out.starts_with(SHEBANG.line));
assert!(out.contains("echo lint"));
assert!(out.contains(SH.start));
}
#[test]
fn install_prelude_is_idempotent() {
let pre = "#!/bin/bash\n\necho 'user lint'\n";
let once = install_block(pre, &SH, "body", Some(SHEBANG)).unwrap();
let twice = install_block(&once, &SH, "body", Some(SHEBANG)).unwrap();
assert_eq!(once, twice);
}
#[test]
fn round_trip_prelude_on_user_hook_restores_input() {
let pre = "#!/bin/bash\n\necho 'user lint'\n";
let installed = install_block(pre, &SH, "body", Some(SHEBANG)).unwrap();
let restored = uninstall_block(&installed, &SH, Some(SHEBANG)).unwrap();
assert_eq!(restored, pre);
}
#[test]
fn round_trip_prelude_on_empty_collapses_to_empty() {
let installed = install_block("", &SH, "body", Some(SHEBANG)).unwrap();
let restored = uninstall_block(&installed, &SH, Some(SHEBANG)).unwrap();
assert_eq!(restored, "");
}
#[test]
fn uninstall_is_noop_when_no_block_present() {
let pre = "#!/bin/sh\necho lint\n";
assert_eq!(uninstall_block(pre, &SH, Some(SHEBANG)).unwrap(), pre);
}
#[test]
fn contains_block_true_after_install() {
let installed = install_block("", &MD, "body", None).unwrap();
assert!(contains_block(&installed, &MD).unwrap());
}
#[test]
fn contains_block_false_for_unrelated_text() {
assert!(!contains_block("<!-- some other tool -->\n", &MD).unwrap());
}
}