use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use super::ScarHit;
static SCAR_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(r"^\s*(?://|#|--|/\*|<!--|\{/\*)\s*@kizu\[(\w+)\]:\s*(.*)")
.expect("scar regex")
});
pub fn scan_scars(paths: &[PathBuf]) -> Vec<ScarHit> {
let mut hits = Vec::new();
for path in paths {
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(_) => continue,
};
let fence_aware = is_fenced_code_aware(path);
hits.extend(scan_content_for_scars(
&SCAR_RE,
&content,
path,
fence_aware,
));
}
hits
}
fn is_fenced_code_aware(path: &Path) -> bool {
path.extension()
.and_then(|e| e.to_str())
.is_some_and(|ext| matches!(ext, "md" | "txt" | "rst" | "adoc"))
}
fn scan_content_for_scars(
re: ®ex::Regex,
content: &str,
path: &Path,
fence_aware: bool,
) -> Vec<ScarHit> {
let mut hits = Vec::new();
let mut in_fence = false;
for (i, line) in content.lines().enumerate() {
if fence_aware {
let trimmed = line.trim_start();
if trimmed.starts_with("```") || trimmed.starts_with("~~~") {
in_fence = !in_fence;
continue;
}
if in_fence {
continue;
}
}
if let Some(caps) = re.captures(line) {
hits.push(ScarHit {
path: path.to_path_buf(),
line_number: i + 1,
kind: caps[1].to_string(),
message: trim_scar_message(&caps[2]),
});
}
}
hits
}
fn trim_scar_message(raw: &str) -> String {
let mut message = raw.trim();
for suffix in ["*/}", "*/", "-->"] {
if let Some(stripped) = message.strip_suffix(suffix) {
message = stripped.trim_end();
break;
}
}
message.to_string()
}
pub fn scan_scars_from_index(root: &Path, paths: &[PathBuf]) -> Vec<ScarHit> {
let mut hits = Vec::new();
for path in paths {
let rel = match path.strip_prefix(root) {
Ok(r) => r,
Err(_) => path.as_path(),
};
let output = match std::process::Command::new("git")
.args(["show", &format!(":{}", rel.display())])
.current_dir(root)
.output()
{
Ok(o) if o.status.success() => o,
_ => continue,
};
let content = String::from_utf8_lossy(&output.stdout);
let fence_aware = is_fenced_code_aware(path);
hits.extend(scan_content_for_scars(
&SCAR_RE,
&content,
path,
fence_aware,
));
}
hits
}