use std::collections::HashMap;
pub fn ccr_marker(
hash_val:&str,
ccr_type:&str,
size:usize,
preview:&str,
headroom_budget:Option<u32>,
meta:Option<&HashMap<String, String>>,
center:Option<&str>,
) -> String {
let mut safe = preview.replace(['\n', '\r'], " ").trim().to_string();
safe = safe.chars().filter(|c| *c >= ' ').collect();
if let Some(budget) = headroom_budget {
safe = if budget < 25 {
safe.chars().take(30).collect()
} else if budget < 50 {
safe.chars().take(60).collect()
} else if budget < 75 {
safe.chars().take(100).collect()
} else {
safe
};
}
let meta_str = if let Some(m) = meta {
let parts:Vec<String> = m
.iter()
.filter_map(|(k, v)| {
let sv = v.replace('|', "/").replace('\n', " ").trim().to_string();
if sv.is_empty() { None } else { Some(format!("{}={}", k, sv)) }
})
.collect();
let mut s = parts.join(";");
if s.len() > 300 {
s = format!("{}...", crate::struct_extract::floor_boundary(&s, 297));
}
s
} else {
String::new()
};
render_marker(&safe, ccr_type, &meta_str, center, hash_val, size)
}
fn render_marker(preview:&str, ccr_type:&str, meta:&str, center:Option<&str>, hash:&str, size:usize) -> String {
let center_str = center.unwrap_or(ccr_type);
let meta_part = if meta.is_empty() { String::new() } else { format!("\n[meta:{}]", meta) };
let preview_line = if is_self_bracketed_preview(preview) {
preview.to_string()
} else {
format!("[{}:{}]", center_str, preview)
};
format!("<<<CCR:{}|{}|{}>>>\n{}{}", hash, ccr_type, size, preview_line, meta_part)
}
fn is_self_bracketed_preview(preview:&str) -> bool {
let p = preview.trim();
if !p.starts_with('[') || !p.ends_with(']') {
return false;
}
let inner = &p[1..];
match inner.find(':') {
Some(colon) => {
let label = &inner[..colon];
!label.is_empty() && label.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
},
None => false,
}
}