pub fn sanitize_id(raw: &str) -> String {
let mut out = String::with_capacity(raw.len());
let mut last_underscore = false;
for ch in raw.chars() {
let safe = ch.is_ascii_alphanumeric() || ch == '_';
if safe {
out.push(ch);
last_underscore = ch == '_';
} else if !last_underscore {
out.push('_');
last_underscore = true;
}
}
while out.ends_with('_') {
out.pop();
}
while out.starts_with('_') {
out.remove(0);
}
if out.is_empty() {
return "n".into();
}
if out.chars().next().is_some_and(|c| c.is_ascii_digit()) {
out.insert_str(0, "n_");
}
out
}
fn short_hash(raw: &str) -> String {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for b in raw.as_bytes() {
h ^= *b as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
format!("{:08x}", h as u32 ^ (h >> 32) as u32)
}
fn namespaced_id(prefix: &str, raw: &str) -> String {
let sane = sanitize_id(raw);
format!("{prefix}_{sane}_{}", short_hash(raw))
}
pub fn escape_label(raw: &str) -> String {
let mut out = String::with_capacity(raw.len());
for ch in raw.chars() {
match ch {
'"' => out.push_str("""),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'&' => out.push_str("&"),
_ => out.push(ch),
}
}
out
}
pub fn render_hierarchy(pairs: &[(String, String)]) -> String {
let mut out = String::from("graph TD\n");
if pairs.is_empty() {
return out;
}
let mut seen = std::collections::HashSet::with_capacity(pairs.len());
for (child, parent) in pairs {
let key = (child.as_str(), parent.as_str());
if !seen.insert(key) {
continue;
}
let child_id = sanitize_id(child);
let parent_id = sanitize_id(parent);
out.push_str(&format!(
" {child_id}[\"{}\"] --> {parent_id}[\"{}\"]\n",
escape_label(child),
escape_label(parent),
));
}
out
}
pub fn render_deps(file: &str, targets: &[(String, u32)]) -> String {
let mut out = String::from("graph LR\n");
let file_id = namespaced_id("f", file);
out.push_str(&format!(" {file_id}[\"{}\"]\n", escape_label(file)));
for (target, line) in targets {
let target_id = namespaced_id("t", &format!("{file}::{target}::L{line}"));
out.push_str(&format!(
" {file_id} --> {target_id}[\"{} (L{line})\"]\n",
escape_label(target),
));
}
out
}
pub fn render_map(files: &[String], symbols_by_file: &[(String, Vec<(String, String)>)]) -> String {
let mut out = String::from("graph TD\n repo[\"Repo\"]\n");
for file in files {
let fid = namespaced_id("f", file);
out.push_str(&format!(" repo --> {fid}[\"{}\"]\n", escape_label(file)));
}
for (file, syms) in symbols_by_file {
let fid = namespaced_id("f", file);
for (name, kind) in syms {
let sid = namespaced_id("s", &format!("{file}::{name}"));
out.push_str(&format!(
" {fid} --> {sid}[\"{} ({})\"]\n",
escape_label(name),
escape_label(kind),
));
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sanitize_strips_punctuation_and_collapses_runs() {
assert_eq!(sanitize_id("foo/bar.py"), "foo_bar_py");
assert_eq!(sanitize_id("AuthService"), "AuthService");
assert_eq!(sanitize_id("123abc"), "n_123abc");
assert_eq!(sanitize_id("--weird--"), "weird");
assert_eq!(sanitize_id(""), "n");
assert_eq!(sanitize_id("..."), "n");
}
#[test]
fn namespaced_id_distinguishes_inputs_that_sanitize_to_same_string() {
let a = namespaced_id("f", "foo/bar.py");
let b = namespaced_id("f", "foo_bar.py");
assert_ne!(a, b, "distinct paths must produce distinct namespaced IDs");
assert!(a.starts_with("f_foo_bar_py_"));
assert!(b.starts_with("f_foo_bar_py_"));
}
#[test]
fn escape_label_entity_encodes_mermaid_breakers() {
assert_eq!(escape_label(r#"foo"bar"#), "foo"bar");
assert_eq!(escape_label("Vec<T>"), "Vec<T>");
assert_eq!(escape_label("Result<T, E>"), "Result<T, E>");
assert_eq!(escape_label("A & B"), "A & B");
assert_eq!(escape_label("Dict[str, int]"), "Dict[str, int]");
assert_eq!(escape_label("A|B"), "A|B");
}
#[test]
fn render_hierarchy_emits_header_and_edges() {
let pairs = vec![
("AdminService".into(), "AuthService".into()),
("AuthService".into(), "BaseService".into()),
];
let out = render_hierarchy(&pairs);
assert!(out.starts_with("graph TD\n"));
assert!(out.contains("AdminService[\"AdminService\"] --> AuthService[\"AuthService\"]"));
assert!(out.contains("AuthService[\"AuthService\"] --> BaseService[\"BaseService\"]"));
}
#[test]
fn render_hierarchy_deduplicates_identical_pairs() {
let pairs = vec![
("AuthService".into(), "BaseService".into()),
("AuthService".into(), "BaseService".into()),
("AuthService".into(), "BaseService".into()),
];
let out = render_hierarchy(&pairs);
let edges = out.matches("AuthService[\"AuthService\"]").count();
assert_eq!(edges, 1, "duplicate pairs must emit one edge: {out}");
}
#[test]
fn render_hierarchy_empty_still_valid_mermaid() {
let out = render_hierarchy(&[]);
assert_eq!(out, "graph TD\n");
}
#[test]
fn render_deps_roots_at_file_with_imports_as_leaves() {
let out = render_deps("auth/service.py", &[("validate_token".into(), 5)]);
assert!(out.starts_with("graph LR\n"));
assert!(out.contains("[\"auth/service.py\"]"));
assert!(out.contains("[\"validate_token (L5)\"]"));
}
#[test]
fn render_deps_preserves_duplicate_target_names_as_distinct_nodes() {
let out = render_deps(
"a.py",
&[("validate_token".into(), 5), ("validate_token".into(), 12)],
);
assert!(
out.contains("[\"validate_token (L5)\"]"),
"L5 label must survive: {out}"
);
assert!(
out.contains("[\"validate_token (L12)\"]"),
"L12 label must survive: {out}"
);
let edge_lines = out.lines().filter(|l| l.contains(" --> ")).count();
assert_eq!(edge_lines, 2, "two distinct edges expected: {out}");
}
#[test]
fn render_map_roots_at_repo_and_attaches_symbols() {
let files = vec!["src/auth.py".to_string()];
let syms = vec![(
"src/auth.py".to_string(),
vec![("login".to_string(), "function".to_string())],
)];
let out = render_map(&files, &syms);
assert!(out.starts_with("graph TD\n repo[\"Repo\"]\n"));
assert!(out.contains("[\"src/auth.py\"]"));
assert!(out.contains("[\"login (function)\"]"));
}
#[test]
fn render_map_distinguishes_files_that_sanitize_to_the_same_id() {
let files = vec!["foo/bar.py".to_string(), "foo_bar.py".to_string()];
let out = render_map(&files, &[]);
assert!(out.contains("[\"foo/bar.py\"]"));
assert!(out.contains("[\"foo_bar.py\"]"));
let edges = out.lines().filter(|l| l.contains("repo --> ")).count();
assert_eq!(edges, 2, "two distinct file edges expected: {out}");
}
}