#[derive(Clone, Copy)]
pub struct Sentinel<'a> {
pub start: &'a str,
pub end: &'a str,
}
pub fn find_block<'a>(content: &'a str, sentinel: &Sentinel) -> Option<&'a str> {
let start = content.find(sentinel.start)?;
let end = content[start..].find(sentinel.end)? + start + sentinel.end.len();
Some(&content[start..end])
}
pub fn extract_block_with_newline<'a>(content: &'a str, sentinel: &Sentinel) -> Option<&'a str> {
let start = content.find(sentinel.start)?;
let after_start = &content[start..];
let end = after_start.find(sentinel.end)? + sentinel.end.len();
let end = if after_start[end..].starts_with('\n') {
end + 1
} else {
end
};
Some(&after_start[..end])
}
pub fn remove_block_bytewise(content: &str, sentinel: &Sentinel) -> String {
let start = match content.find(sentinel.start) {
Some(i) => i,
None => return content.to_string(),
};
let end_marker = match content.find(sentinel.end) {
Some(i) => i + sentinel.end.len(),
None => return content.to_string(),
};
let end = if content[end_marker..].starts_with("\r\n") {
end_marker + 2
} else if content[end_marker..].starts_with('\n') {
end_marker + 1
} else {
end_marker
};
let block_start = if start > 0 && content[..start].ends_with("\n\n") {
start - 1
} else {
start
};
format!("{}{}", &content[..block_start], &content[end..])
}
pub fn remove_block_linewise(content: &str, sentinel: &Sentinel) -> String {
let mut output = Vec::new();
let mut skip = false;
for line in content.lines() {
if line == sentinel.start {
skip = true;
continue;
}
if line == sentinel.end {
skip = false;
continue;
}
if !skip {
output.push(line);
}
}
let mut result = output.join("\n");
if content.ends_with('\n') && !result.is_empty() {
result.push('\n');
}
result
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum InsertAt {
Start,
End,
}
pub fn insert_block(content: &str, block: &str, at: InsertAt) -> String {
match at {
InsertAt::Start => {
let mut updated = String::new();
updated.push_str(block);
if !content.is_empty() {
if !block.ends_with('\n') {
updated.push('\n');
}
updated.push('\n');
updated.push_str(content);
}
updated
}
InsertAt::End => {
let mut updated = content.to_string();
if !updated.ends_with('\n') && !updated.is_empty() {
updated.push('\n');
}
if !updated.is_empty() {
updated.push('\n');
}
updated.push_str(block);
updated
}
}
}
pub fn trim_outer_blank_lines(content: &str) -> String {
content.trim_matches('\n').to_string()
}
#[cfg(test)]
mod tests {
use super::*;
const SENTINEL: Sentinel<'static> = Sentinel {
start: "START",
end: "END",
};
#[test]
fn find_block_returns_start_through_end_inclusive() {
let content = "pre\nSTART\nbody\nEND\npost";
assert_eq!(find_block(content, &SENTINEL), Some("START\nbody\nEND"));
}
#[test]
fn find_block_none_when_either_marker_missing() {
assert_eq!(find_block("no markers", &SENTINEL), None);
assert_eq!(find_block("STARTonly", &SENTINEL), None);
}
#[test]
fn remove_block_bytewise_and_linewise_agree_on_the_simple_case() {
let content = "before\nSTART\nbody\nEND\nafter\n";
assert_eq!(
remove_block_bytewise(content, &SENTINEL),
remove_block_linewise(content, &SENTINEL)
);
}
#[test]
fn insert_block_start_and_end_are_symmetric_on_empty_content() {
assert_eq!(
insert_block("", "BLOCK\n", InsertAt::Start),
insert_block("", "BLOCK\n", InsertAt::End)
);
}
#[test]
fn trim_outer_blank_lines_is_idempotent() {
let once = trim_outer_blank_lines("\n\nfoo\n\n");
let twice = trim_outer_blank_lines(&once);
assert_eq!(once, twice);
}
}