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}")
77 } else if trimmed.starts_with('#') {
78 format!("# {HASH_PREFIX}{hash}")
79 } else if trimmed.starts_with("/*") || trimmed.starts_with(" *") || trimmed.ends_with("*/") {
80 format!(" * {HASH_PREFIX}{hash}")
81 } else {
82 format!("// {HASH_PREFIX}{hash}")
83 };
84 result.push_str(&hash_line);
85 result.push('\n');
86 injected = true;
87 }
88 }
89
90 if !content.ends_with('\n') && result.ends_with('\n') {
92 result.pop();
93 }
94
95 result
96}
97
98pub fn extract_hash(content: &str) -> Option<String> {
100 for (i, line) in content.lines().enumerate() {
101 if i >= 10 {
102 break;
103 }
104 if let Some(pos) = line.find(HASH_PREFIX) {
105 let rest = &line[pos + HASH_PREFIX.len()..];
106 let hex = rest.trim().trim_end_matches("*/").trim();
108 if !hex.is_empty() {
109 return Some(hex.to_string());
110 }
111 }
112 }
113 None
114}
115
116pub fn strip_hash_line(content: &str) -> String {
118 let mut result = String::with_capacity(content.len());
119 for line in content.lines() {
120 if line.contains(HASH_PREFIX) {
121 continue;
122 }
123 result.push_str(line);
124 result.push('\n');
125 }
126 if !content.ends_with('\n') && result.ends_with('\n') {
128 result.pop();
129 }
130 result
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 #[test]
138 fn test_header_double_slash() {
139 let h = header(CommentStyle::DoubleSlash);
140 assert!(h.contains("// This file is auto-generated by alef"));
141 assert!(h.contains("// Issues & docs: https://github.com/kreuzberg-dev/alef"));
142 }
143
144 #[test]
145 fn test_header_hash() {
146 let h = header(CommentStyle::Hash);
147 assert!(h.contains("# This file is auto-generated by alef"));
148 }
149
150 #[test]
151 fn test_header_block() {
152 let h = header(CommentStyle::Block);
153 assert!(h.starts_with("/*\n"));
154 assert!(h.contains(" * This file is auto-generated by alef"));
155 assert!(h.ends_with(" */\n"));
156 }
157
158 #[test]
159 fn test_inject_and_extract_rust() {
160 let h = header(CommentStyle::DoubleSlash);
161 let content = format!("{h}use foo;\n");
162 let hash = hash_content(&content);
163 let injected = inject_hash_line(&content, &hash);
164 assert!(injected.contains(HASH_PREFIX));
165 assert_eq!(extract_hash(&injected), Some(hash));
166 }
167
168 #[test]
169 fn test_inject_and_extract_python() {
170 let h = header(CommentStyle::Hash);
171 let content = format!("{h}import foo\n");
172 let hash = hash_content(&content);
173 let injected = inject_hash_line(&content, &hash);
174 assert!(injected.contains(&format!("# {HASH_PREFIX}")));
175 assert_eq!(extract_hash(&injected), Some(hash));
176 }
177
178 #[test]
179 fn test_inject_and_extract_c_block() {
180 let h = header(CommentStyle::Block);
181 let content = format!("{h}#include <stdio.h>\n");
182 let hash = hash_content(&content);
183 let injected = inject_hash_line(&content, &hash);
184 assert!(injected.contains(HASH_PREFIX));
185 assert_eq!(extract_hash(&injected), Some(hash));
186 }
187
188 #[test]
189 fn test_inject_php_line2() {
190 let h = header(CommentStyle::DoubleSlash);
191 let content = format!("<?php\n{h}namespace Foo;\n");
192 let hash = hash_content(&content);
193 let injected = inject_hash_line(&content, &hash);
194 let lines: Vec<&str> = injected.lines().collect();
195 assert_eq!(lines[0], "<?php");
196 assert!(lines[1].contains(HEADER_MARKER));
197 assert!(lines.iter().any(|l| l.contains(HASH_PREFIX)));
198 assert_eq!(extract_hash(&injected), Some(hash));
199 }
200
201 #[test]
202 fn test_no_header_returns_unchanged() {
203 let content = "fn main() {}\n";
204 let injected = inject_hash_line(content, "abc123");
205 assert_eq!(injected, content);
206 assert_eq!(extract_hash(&injected), None);
207 }
208
209 #[test]
210 fn test_strip_hash_line() {
211 let content = "// auto-generated by alef\n// alef:hash:abc123\nuse foo;\n";
212 let stripped = strip_hash_line(content);
213 assert_eq!(stripped, "// auto-generated by alef\nuse foo;\n");
214 }
215
216 #[test]
217 fn test_roundtrip() {
218 let h = header(CommentStyle::Hash);
219 let original = format!("{h}import sys\n");
220 let hash = hash_content(&original);
221 let injected = inject_hash_line(&original, &hash);
222 let stripped = strip_hash_line(&injected);
223 assert_eq!(stripped, original);
224 assert_eq!(hash_content(&stripped), hash);
225 }
226}