1use crate::error::{Result, SearchError};
2use std::collections::HashMap;
3use std::fs;
4use std::path::{Path, PathBuf};
5use yaml_rust::scanner::{Scanner, TokenType};
6use yaml_rust::Yaml;
7
8use super::translation::TranslationEntry;
9
10pub struct YamlParser;
12
13impl YamlParser {
14 pub fn parse_file(path: &Path) -> Result<Vec<TranslationEntry>> {
15 let content = fs::read_to_string(path).map_err(|e| {
16 SearchError::yaml_parse_error(path, format!("Failed to read file: {}", e))
17 })?;
18
19 let mut value_to_line: HashMap<String, usize> = HashMap::new();
21 let mut scanner = Scanner::new(content.chars());
22
23 loop {
24 match scanner.next_token() {
25 Ok(Some(token)) => {
26 if let TokenType::Scalar(_, value) = token.1 {
27 value_to_line.insert(value, token.0.line());
29 }
30 }
31 Ok(None) => break, Err(_) => break, }
34 }
35
36 let docs = yaml_rust::YamlLoader::load_from_str(&content).map_err(|e| {
38 SearchError::yaml_parse_error(path, format!("Invalid YAML syntax: {}", e))
39 })?;
40
41 let mut entries = Vec::new();
42
43 for doc in docs {
44 Self::flatten_yaml(doc, String::new(), path, &value_to_line, &mut entries, true);
45 }
46
47 Ok(entries)
48 }
49
50 fn flatten_yaml(
51 yaml: Yaml,
52 prefix: String,
53 file_path: &Path,
54 value_to_line: &HashMap<String, usize>,
55 entries: &mut Vec<TranslationEntry>,
56 is_root: bool,
57 ) {
58 match yaml {
59 Yaml::Hash(hash) => {
60 for (key, value) in hash {
61 if let Some(key_str) = key.as_str() {
62 let new_prefix = if prefix.is_empty() {
63 key_str.to_string()
64 } else {
65 format!("{}.{}", prefix, key_str)
66 };
67
68 let is_locale_root = is_root
70 && prefix.is_empty()
71 && (key_str == "en"
72 || key_str == "fr"
73 || key_str == "de"
74 || key_str == "es"
75 || key_str == "ja"
76 || key_str == "zh");
77
78 Self::flatten_yaml(
79 value.clone(),
80 new_prefix,
81 file_path,
82 value_to_line,
83 entries,
84 false,
85 );
86
87 if is_locale_root {
89 Self::flatten_yaml(
90 value,
91 String::new(),
92 file_path,
93 value_to_line,
94 entries,
95 false,
96 );
97 }
98 }
99 }
100 }
101 Yaml::String(value) => {
102 let line = value_to_line.get(&value).copied().unwrap_or(0);
103
104 entries.push(TranslationEntry {
105 key: prefix,
106 value,
107 line,
108 file: PathBuf::from(file_path),
109 });
110 }
111 Yaml::Integer(value) => {
112 let value_str = value.to_string();
113 let line = value_to_line.get(&value_str).copied().unwrap_or(0);
114
115 entries.push(TranslationEntry {
116 key: prefix,
117 value: value_str,
118 line,
119 file: PathBuf::from(file_path),
120 });
121 }
122 Yaml::Boolean(value) => {
123 let value_str = value.to_string();
124 let line = value_to_line.get(&value_str).copied().unwrap_or(0);
125
126 entries.push(TranslationEntry {
127 key: prefix,
128 value: value_str,
129 line,
130 file: PathBuf::from(file_path),
131 });
132 }
133 _ => {
134 }
136 }
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use std::io::Write;
144 use tempfile::NamedTempFile;
145
146 #[test]
147 fn test_parse_simple_yaml() {
148 let mut file = NamedTempFile::new().unwrap();
149 write!(file, "key: value").unwrap();
150
151 let entries = YamlParser::parse_file(file.path()).unwrap();
152 assert_eq!(entries.len(), 1);
153 assert_eq!(entries[0].key, "key");
154 assert_eq!(entries[0].value, "value");
155 assert_eq!(entries[0].line, 1);
156 }
157
158 #[test]
159 fn test_parse_nested_yaml() {
160 let mut file = NamedTempFile::new().unwrap();
161 write!(file, "parent:\n child: value").unwrap();
162
163 let entries = YamlParser::parse_file(file.path()).unwrap();
164 assert_eq!(entries.len(), 1);
165 assert_eq!(entries[0].key, "parent.child");
166 assert_eq!(entries[0].value, "value");
167 assert_eq!(entries[0].line, 2);
168 }
169
170 #[test]
171 fn test_parse_multiple_keys() {
172 let mut file = NamedTempFile::new().unwrap();
173 write!(
174 file,
175 "
176key1: value1
177key2: value2
178nested:
179 key3: value3
180"
181 )
182 .unwrap();
183
184 let entries = YamlParser::parse_file(file.path()).unwrap();
185 assert_eq!(entries.len(), 3);
186
187 let entry1 = entries.iter().find(|e| e.key == "key1").unwrap();
189 assert_eq!(entry1.value, "value1");
190 assert_eq!(entry1.line, 2);
191
192 let entry2 = entries.iter().find(|e| e.key == "key2").unwrap();
193 assert_eq!(entry2.value, "value2");
194 assert_eq!(entry2.line, 3);
195
196 let entry3 = entries.iter().find(|e| e.key == "nested.key3").unwrap();
197 assert_eq!(entry3.value, "value3");
198 assert_eq!(entry3.line, 5);
199 }
200}