1const HASH_PREFIX: &str = "alef:hash:";
8
9const HEADER_BODY: &str = "\
12This file is auto-generated by alef — DO NOT EDIT.
13To regenerate: alef generate
14To verify freshness: alef verify --exit-code
15Issues & docs: https://github.com/kreuzberg-dev/alef";
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum CommentStyle {
20 DoubleSlash,
22 Hash,
24 Block,
26}
27
28pub fn header(style: CommentStyle) -> String {
37 match style {
38 CommentStyle::DoubleSlash => HEADER_BODY.lines().map(|l| format!("// {l}\n")).collect(),
39 CommentStyle::Hash => HEADER_BODY.lines().map(|l| format!("# {l}\n")).collect(),
40 CommentStyle::Block => {
41 let mut out = String::from("/*\n");
42 for line in HEADER_BODY.lines() {
43 out.push_str(&format!(" * {line}\n"));
44 }
45 out.push_str(" */\n");
46 out
47 }
48 }
49}
50
51const HEADER_MARKER: &str = "auto-generated by alef";
54
55pub fn hash_content(content: &str) -> String {
57 blake3::hash(content.as_bytes()).to_hex().to_string()
58}
59
60pub fn inject_hash_line(content: &str, hash: &str) -> String {
66 let mut result = String::with_capacity(content.len() + 80);
67 let mut injected = false;
68
69 for (i, line) in content.lines().enumerate() {
70 result.push_str(line);
71 result.push('\n');
72
73 if !injected && i < 10 && line.contains(HEADER_MARKER) {
74 let trimmed = line.trim();
75 let hash_line = if trimmed.starts_with("<!--") {
76 format!("<!-- {HASH_PREFIX}{hash} -->")
78 } else if trimmed.starts_with("//") {
79 format!("// {HASH_PREFIX}{hash}")
80 } else if trimmed.starts_with('#') {
81 format!("# {HASH_PREFIX}{hash}")
82 } else if trimmed.starts_with("/*") || trimmed.starts_with(" *") || trimmed.ends_with("*/") {
83 format!(" * {HASH_PREFIX}{hash}")
84 } else {
85 format!("// {HASH_PREFIX}{hash}")
86 };
87 result.push_str(&hash_line);
88 result.push('\n');
89 injected = true;
90 }
91 }
92
93 if !content.ends_with('\n') && result.ends_with('\n') {
95 result.pop();
96 }
97
98 result
99}
100
101pub fn extract_hash(content: &str) -> Option<String> {
103 for (i, line) in content.lines().enumerate() {
104 if i >= 10 {
105 break;
106 }
107 if let Some(pos) = line.find(HASH_PREFIX) {
108 let rest = &line[pos + HASH_PREFIX.len()..];
109 let hex = rest.trim().trim_end_matches("*/").trim_end_matches("-->").trim();
111 if !hex.is_empty() {
112 return Some(hex.to_string());
113 }
114 }
115 }
116 None
117}
118
119pub fn strip_hash_line(content: &str) -> String {
121 let mut result = String::with_capacity(content.len());
122 for line in content.lines() {
123 if line.contains(HASH_PREFIX) {
124 continue;
125 }
126 result.push_str(line);
127 result.push('\n');
128 }
129 if !content.ends_with('\n') && result.ends_with('\n') {
131 result.pop();
132 }
133 result
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn test_header_double_slash() {
142 let h = header(CommentStyle::DoubleSlash);
143 assert!(h.contains("// This file is auto-generated by alef"));
144 assert!(h.contains("// Issues & docs: https://github.com/kreuzberg-dev/alef"));
145 }
146
147 #[test]
148 fn test_header_hash() {
149 let h = header(CommentStyle::Hash);
150 assert!(h.contains("# This file is auto-generated by alef"));
151 }
152
153 #[test]
154 fn test_header_block() {
155 let h = header(CommentStyle::Block);
156 assert!(h.starts_with("/*\n"));
157 assert!(h.contains(" * This file is auto-generated by alef"));
158 assert!(h.ends_with(" */\n"));
159 }
160
161 #[test]
162 fn test_inject_and_extract_rust() {
163 let h = header(CommentStyle::DoubleSlash);
164 let content = format!("{h}use foo;\n");
165 let hash = hash_content(&content);
166 let injected = inject_hash_line(&content, &hash);
167 assert!(injected.contains(HASH_PREFIX));
168 assert_eq!(extract_hash(&injected), Some(hash));
169 }
170
171 #[test]
172 fn test_inject_and_extract_python() {
173 let h = header(CommentStyle::Hash);
174 let content = format!("{h}import foo\n");
175 let hash = hash_content(&content);
176 let injected = inject_hash_line(&content, &hash);
177 assert!(injected.contains(&format!("# {HASH_PREFIX}")));
178 assert_eq!(extract_hash(&injected), Some(hash));
179 }
180
181 #[test]
182 fn test_inject_and_extract_c_block() {
183 let h = header(CommentStyle::Block);
184 let content = format!("{h}#include <stdio.h>\n");
185 let hash = hash_content(&content);
186 let injected = inject_hash_line(&content, &hash);
187 assert!(injected.contains(HASH_PREFIX));
188 assert_eq!(extract_hash(&injected), Some(hash));
189 }
190
191 #[test]
192 fn test_inject_php_line2() {
193 let h = header(CommentStyle::DoubleSlash);
194 let content = format!("<?php\n{h}namespace Foo;\n");
195 let hash = hash_content(&content);
196 let injected = inject_hash_line(&content, &hash);
197 let lines: Vec<&str> = injected.lines().collect();
198 assert_eq!(lines[0], "<?php");
199 assert!(lines[1].contains(HEADER_MARKER));
200 assert!(lines.iter().any(|l| l.contains(HASH_PREFIX)));
201 assert_eq!(extract_hash(&injected), Some(hash));
202 }
203
204 #[test]
205 fn test_no_header_returns_unchanged() {
206 let content = "fn main() {}\n";
207 let injected = inject_hash_line(content, "abc123");
208 assert_eq!(injected, content);
209 assert_eq!(extract_hash(&injected), None);
210 }
211
212 #[test]
213 fn test_strip_hash_line() {
214 let content = "// auto-generated by alef\n// alef:hash:abc123\nuse foo;\n";
215 let stripped = strip_hash_line(content);
216 assert_eq!(stripped, "// auto-generated by alef\nuse foo;\n");
217 }
218
219 #[test]
220 fn test_roundtrip() {
221 let h = header(CommentStyle::Hash);
222 let original = format!("{h}import sys\n");
223 let hash = hash_content(&original);
224 let injected = inject_hash_line(&original, &hash);
225 let stripped = strip_hash_line(&injected);
226 assert_eq!(stripped, original);
227 assert_eq!(hash_content(&stripped), hash);
228 }
229}