1use regex::Regex;
2use std::path::Path;
3
4use crate::lang::ext_to_lang;
5
6fn make_fence(content: &str) -> String {
8 let max_run = content
9 .as_bytes()
10 .split(|&b| b != b'`')
11 .map(|run| run.len())
12 .max()
13 .unwrap_or(0);
14 let fence_len = if max_run >= 3 { max_run + 1 } else { 3 };
15 "`".repeat(fence_len)
16}
17
18pub struct ProcessResult {
20 pub original: String,
21 pub processed: String,
22}
23
24pub fn process_file(path: &Path) -> Result<ProcessResult, String> {
27 let content = std::fs::read_to_string(path)
28 .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
29
30 let base_dir = path.parent().unwrap_or(Path::new("."));
31 let processed = process_content(&content, base_dir);
32
33 Ok(ProcessResult {
34 original: content,
35 processed,
36 })
37}
38
39pub fn process_content(content: &str, base_dir: &Path) -> String {
50 let open_re = Regex::new(r#"embed-src\s+src="([^"]+)"(?:\s+fence(?:="([^"]*)")?)?"#).unwrap();
51 let close_re = Regex::new(r#"(?:^|[^a-zA-Z0-9_])/embed-src\b"#).unwrap();
54
55 let lines: Vec<&str> = content.lines().collect();
56 let mut result = Vec::new();
57 let mut i = 0;
58 let has_trailing_newline = content.ends_with('\n');
59 let mut in_fence = false;
60 let mut fence_len: usize = 0;
61
62 while i < lines.len() {
63 let line = lines[i];
64
65 let trimmed = line.trim_start();
67 if trimmed.starts_with("```") {
68 let backtick_count = trimmed.bytes().take_while(|&b| b == b'`').count();
69 if !in_fence {
70 in_fence = true;
71 fence_len = backtick_count;
72 result.push(line.to_string());
73 i += 1;
74 continue;
75 } else if backtick_count >= fence_len {
76 in_fence = false;
77 fence_len = 0;
78 result.push(line.to_string());
79 i += 1;
80 continue;
81 }
82 }
83
84 if in_fence {
85 result.push(line.to_string());
86 i += 1;
87 continue;
88 }
89
90 if let Some(cap) = open_re.captures(line) {
91 let src_path = cap[1].to_string();
92 let fence_attr = cap.get(2).map(|m| m.as_str().to_string());
93 let has_fence = cap.get(0).unwrap().as_str().contains("fence");
96
97 result.push(line.to_string());
99
100 let mut found_close = false;
102 let mut close_line_idx = i + 1;
103 while close_line_idx < lines.len() {
104 if close_re.is_match(lines[close_line_idx]) {
105 found_close = true;
106 break;
107 }
108 close_line_idx += 1;
109 }
110
111 if !found_close {
112 eprintln!(
114 "Warning: no closing /embed-src found for directive at line {}",
115 i + 1
116 );
117 i += 1;
118 continue;
119 }
120
121 let file_path = base_dir.join(&src_path);
123 let file_content = match std::fs::read_to_string(&file_path) {
124 Ok(c) => c,
125 Err(e) => {
126 eprintln!("Warning: could not read {}: {}", file_path.display(), e);
127 for line in &lines[(i + 1)..=close_line_idx] {
129 result.push(line.to_string());
130 }
131 i = close_line_idx + 1;
132 continue;
133 }
134 };
135
136 if has_fence {
138 let lang = match &fence_attr {
139 Some(lang) if !lang.is_empty() && lang != "auto" => lang.to_string(),
140 _ => {
141 let ext = Path::new(&src_path)
143 .extension()
144 .and_then(|e| e.to_str())
145 .unwrap_or("");
146 ext_to_lang(ext).to_string()
147 }
148 };
149 let fence = make_fence(&file_content);
150 result.push(format!("{}{}", fence, lang));
151 result.push(file_content.trim_end().to_string());
152 result.push(fence);
153 } else {
154 let trimmed = file_content.trim_end();
156 if !trimmed.is_empty() {
157 result.push(trimmed.to_string());
158 }
159 }
160
161 result.push(lines[close_line_idx].to_string());
163 i = close_line_idx + 1;
164 } else {
165 result.push(line.to_string());
166 i += 1;
167 }
168 }
169
170 let mut output = result.join("\n");
171 if has_trailing_newline {
172 output.push('\n');
173 }
174 output
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use std::path::Path;
181
182 #[test]
183 fn no_directives() {
184 let input = "# Hello\n\nSome text.\n";
185 let result = process_content(input, Path::new("."));
186 assert_eq!(result, input);
187 }
188
189 #[test]
190 fn missing_close_tag() {
191 let input = "<!-- embed-src src=\"foo.rs\" -->\nstale content\n";
192 let result = process_content(input, Path::new("."));
193 assert_eq!(result, input);
195 }
196}