automd_rs/parser/
readme.rs1use crate::error::Result;
4use crate::handler::{BlockHandler, UpdateContext};
5
6const OPEN_PREFIX: &str = "<!-- automdrs:";
7const OPEN_SUFFIX: &str = "-->";
8const CLOSE_TAG: &str = "<!-- /automdrs -->";
9
10#[derive(Debug, Clone)]
12pub struct BlockRequest {
13 pub name: String,
14 pub open_tag_line: String,
15}
16
17pub fn parse_block_name(line: &str) -> Option<&str> {
19 let t = line.trim();
20 if !t.starts_with(OPEN_PREFIX) || !t.ends_with(OPEN_SUFFIX) {
21 return None;
22 }
23 t.strip_prefix(OPEN_PREFIX)?
24 .strip_suffix(OPEN_SUFFIX)?
25 .trim()
26 .split_whitespace()
27 .next()
28}
29
30pub fn parse_readme_blocks(content: &str) -> Vec<BlockRequest> {
32 let cap = content.matches(OPEN_PREFIX).count();
33 let mut requests = Vec::with_capacity(cap);
34 let mut in_block = false;
35 for line in content.lines() {
36 let t = line.trim();
37 if !in_block {
38 if let Some(name) = parse_block_name(line) {
39 in_block = true;
40 requests.push(BlockRequest {
41 name: name.to_string(),
42 open_tag_line: line.to_string(),
43 });
44 }
45 continue;
46 }
47 if t == CLOSE_TAG.trim() {
48 in_block = false;
49 }
50 }
51 requests
52}
53
54pub fn replace_blocks_with_handler(
56 content: &str,
57 handler: &dyn BlockHandler,
58 context: &UpdateContext,
59) -> Result<String> {
60 let mut out = String::with_capacity(content.len());
61 let mut in_block = false;
62 for line in content.lines() {
63 let t = line.trim();
64 if !in_block {
65 if let Some(name) = parse_block_name(line) {
66 in_block = true;
67 out.push_str(line);
68 out.push('\n');
69 let generated = handler.generate(name, line, context)?;
70 for s in &generated {
71 out.push_str(s);
72 out.push('\n');
73 }
74 continue;
75 }
76 out.push_str(line);
77 out.push('\n');
78 continue;
79 }
80 if t == CLOSE_TAG.trim() {
81 in_block = false;
82 out.push_str(line);
83 out.push('\n');
84 }
85 }
87 if out.ends_with('\n') {
88 out.pop();
89 }
90 Ok(out)
91}
92
93pub fn assign_and_generate(
95 requests: &[BlockRequest],
96 handler: &dyn BlockHandler,
97 context: &UpdateContext,
98) -> Result<Vec<Vec<String>>> {
99 let mut out = Vec::with_capacity(requests.len());
100 for req in requests {
101 out.push(handler.generate(&req.name, &req.open_tag_line, context)?);
102 }
103 Ok(out)
104}
105
106pub fn replace_blocks_once(content: &str, generated: &[Vec<String>]) -> String {
108 let extra: usize = generated
109 .iter()
110 .map(|v| v.iter().map(|s| s.len() + 1).sum::<usize>())
111 .sum();
112 let cap = content.len().saturating_add(extra);
113 let mut out = String::with_capacity(cap);
114 let mut in_block = false;
115 let mut idx = 0usize;
116 for line in content.lines() {
117 let t = line.trim();
118 if !in_block {
119 if parse_block_name(line).is_some() {
120 in_block = true;
121 out.push_str(line);
122 out.push('\n');
123 if idx < generated.len() {
124 for s in &generated[idx] {
125 out.push_str(s);
126 out.push('\n');
127 }
128 }
129 idx += 1;
130 continue;
131 }
132 out.push_str(line);
133 out.push('\n');
134 continue;
135 }
136 if t == CLOSE_TAG.trim() {
137 in_block = false;
138 out.push_str(line);
139 out.push('\n');
140 }
141 }
142 if out.ends_with('\n') {
143 out.pop();
144 }
145 out
146}
147
148pub fn update_readme(
149 content: &str,
150 handler: &dyn BlockHandler,
151 context: &UpdateContext,
152) -> Result<String> {
153 replace_blocks_with_handler(content, handler, context)
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn test_parse_block_name() {
162 assert_eq!(
163 parse_block_name(" <!-- automdrs:badges version --> "),
164 Some("badges")
165 );
166 assert_eq!(
167 parse_block_name("<!-- automdrs:contributors -->"),
168 Some("contributors")
169 );
170 assert_eq!(parse_block_name("<!-- automdrs:foo a b -->"), Some("foo"));
171 assert_eq!(parse_block_name("<!-- other:tag -->"), None);
172 assert_eq!(parse_block_name("not a tag"), None);
173 }
174
175 #[test]
176 fn test_parse_readme_blocks_order() {
177 let content = "A\n<!-- automdrs:badges version -->\n<!-- /automdrs -->\nB\n<!-- automdrs:contributors -->\n<!-- /automdrs -->\n";
178 let reqs = parse_readme_blocks(content);
179 assert_eq!(reqs.len(), 2);
180 assert_eq!(reqs[0].name, "badges");
181 assert_eq!(reqs[1].name, "contributors");
182 }
183
184 #[test]
185 fn test_replace_blocks_once() {
186 let content = "Title\n\n<!-- automdrs:badges version -->\n<!-- /automdrs -->\n\nRest";
187 let generated = vec![vec!["line1".to_string(), "line2".to_string()]];
188 let out = replace_blocks_once(content, &generated);
189 assert!(out.contains("<!-- automdrs:badges version -->"));
190 assert!(out.contains("line1"));
191 assert!(out.contains("line2"));
192 assert!(out.contains("<!-- /automdrs -->"));
193 assert!(out.contains("Rest"));
194 }
195
196 #[test]
197 fn test_assign_and_generate() {
198 let requests = vec![
199 BlockRequest {
200 name: "badges".to_string(),
201 open_tag_line: "<!-- automdrs:badges version -->".to_string(),
202 },
203 BlockRequest {
204 name: "unknown".to_string(),
205 open_tag_line: "<!-- automdrs:unknown -->".to_string(),
206 },
207 ];
208 let ctx = crate::handler::UpdateContext::new(
209 crate::parser::cargo::ParsedManifest {
210 name: "x".to_string(),
211 description: "d".to_string(),
212 username: "u".to_string(),
213 repository_name: "r".to_string(),
214 },
215 std::path::PathBuf::from("."),
216 );
217 let handler = crate::handler::DefaultHandler::default();
218 let out = assign_and_generate(&requests, &handler, &ctx).unwrap();
219 assert_eq!(out.len(), 2);
220 assert!(!out[0].is_empty());
221 assert!(out[1].is_empty());
222 }
223
224 #[test]
225 fn test_update_readme() {
226 let content = "P\n<!-- automdrs:with-automdrs -->\n<!-- /automdrs -->\nQ";
227 let ctx = crate::handler::UpdateContext::new(
228 crate::parser::cargo::ParsedManifest {
229 name: "n".to_string(),
230 description: "d".to_string(),
231 username: "u".to_string(),
232 repository_name: "r".to_string(),
233 },
234 std::path::PathBuf::from("."),
235 );
236 let out = update_readme(content, &crate::handler::DefaultHandler::default(), &ctx).unwrap();
237 assert!(out.contains("automd-rs"));
238 assert!(out.contains("P"));
239 assert!(out.contains("Q"));
240 }
241}