features_cli/
readme_parser.rs1use anyhow::{Context, Result};
2use std::collections::HashMap;
3use std::fs;
4use std::path::Path;
5
6fn read_readme_content(content: &str) -> String {
7 let mut found_first_title = false;
8 let mut lines_after_title: Vec<&str> = Vec::new();
9
10 for line in content.lines() {
11 let trimmed = line.trim();
12
13 if !found_first_title && trimmed.starts_with('#') {
15 found_first_title = true;
16 continue;
17 }
18
19 if found_first_title {
21 lines_after_title.push(line);
22 }
23 }
24
25 lines_after_title.join("\n").trim().to_string()
27}
28
29pub fn read_readme_info(
32 readme_path: &Path,
33) -> Result<(String, String, HashMap<String, serde_json::Value>)> {
34 if !readme_path.exists() {
35 return Ok(("Unknown".to_string(), "".to_string(), HashMap::new()));
36 }
37
38 let content = fs::read_to_string(readme_path)
39 .with_context(|| format!("could not read README file at `{}`", readme_path.display()))?;
40
41 let mut owner = "Unknown".to_string();
42 let mut description = "".to_string();
43 let mut meta: HashMap<String, serde_json::Value> = HashMap::new();
44
45 if let Some(stripped) = content.strip_prefix("---\n") {
47 if let Some(end_pos) = stripped.find("\n---\n") {
48 let yaml_content = &stripped[..end_pos];
49 let markdown_content = stripped[end_pos + 5..].to_string();
50
51 if let Ok(yaml_value) = serde_yaml::from_str::<serde_yaml::Value>(yaml_content)
53 && let Some(mapping) = yaml_value.as_mapping()
54 {
55 for (key, value) in mapping {
56 if let Some(key_str) = key.as_str() {
57 if key_str == "owner" {
58 if let Some(owner_value) = value.as_str() {
59 owner = owner_value.to_string();
60 }
61 } else {
62 if let Ok(json_value) = serde_json::to_value(value) {
64 meta.insert(key_str.to_string(), json_value);
65 }
66 }
67 }
68 }
69 }
70
71 description = read_readme_content(&markdown_content)
72 }
73 } else {
74 description = read_readme_content(&content)
75 }
76
77 Ok((owner, description, meta))
78}