features_cli/
readme_parser.rs1use anyhow::{Context, Result};
2use std::collections::HashMap;
3use std::fs;
4use std::path::Path;
5
6pub struct ReadmeInfo {
8 pub title: Option<String>,
10 pub owner: String,
12 pub description: String,
14 pub meta: HashMap<String, serde_json::Value>,
16}
17
18fn extract_first_title(content: &str) -> Option<String> {
19 for line in content.lines() {
20 let trimmed = line.trim();
21
22 if trimmed.starts_with('#') {
24 let title = trimmed.trim_start_matches('#').trim();
26 if !title.is_empty() {
27 return Some(title.to_string());
28 }
29 }
30 }
31 None
32}
33
34fn read_readme_content(content: &str) -> String {
35 let mut found_first_title = false;
36 let mut lines_after_title: Vec<&str> = Vec::new();
37
38 for line in content.lines() {
39 let trimmed = line.trim();
40
41 if !found_first_title && trimmed.starts_with('#') {
43 found_first_title = true;
44 continue;
45 }
46
47 if found_first_title {
49 lines_after_title.push(line);
50 }
51 }
52
53 lines_after_title.join("\n").trim().to_string()
55}
56
57pub fn read_readme_info(readme_path: &Path) -> Result<ReadmeInfo> {
61 if !readme_path.exists() {
62 return Ok(ReadmeInfo {
63 title: None,
64 owner: "".to_string(),
65 description: "".to_string(),
66 meta: HashMap::new(),
67 });
68 }
69
70 let content = fs::read_to_string(readme_path)
71 .with_context(|| format!("could not read README file at `{}`", readme_path.display()))?;
72
73 let mut title: Option<String> = None;
74 let mut owner = "".to_string();
75 let mut description = "".to_string();
76 let mut meta: HashMap<String, serde_json::Value> = HashMap::new();
77
78 if let Some(stripped) = content.strip_prefix("---\n") {
80 if let Some(end_pos) = stripped.find("\n---\n") {
81 let yaml_content = &stripped[..end_pos];
82 let markdown_content = stripped[end_pos + 5..].to_string();
83
84 if let Ok(yaml_value) = serde_yaml::from_str::<serde_yaml::Value>(yaml_content)
86 && let Some(mapping) = yaml_value.as_mapping()
87 {
88 for (key, value) in mapping {
89 if let Some(key_str) = key.as_str() {
90 if key_str == "owner" {
91 if let Some(owner_value) = value.as_str() {
92 owner = owner_value.to_string();
93 }
94 } else {
95 if let Ok(json_value) = serde_json::to_value(value) {
97 meta.insert(key_str.to_string(), json_value);
98 }
99 }
100 }
101 }
102 }
103
104 title = extract_first_title(&markdown_content);
106 description = read_readme_content(&markdown_content)
107 }
108 } else {
109 title = extract_first_title(&content);
111 description = read_readme_content(&content)
112 }
113
114 Ok(ReadmeInfo {
115 title,
116 owner,
117 description,
118 meta,
119 })
120}