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 Yaml::Array(arr) => {
134 for (index, val) in arr.into_iter().enumerate() {
135 let new_prefix = if prefix.is_empty() {
136 index.to_string()
137 } else {
138 format!("{}.{}", prefix, index)
139 };
140 Self::flatten_yaml(val, new_prefix, file_path, value_to_line, entries, false);
141 }
142 }
143 _ => {
144 }
146 }
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use std::io::Write;
154 use tempfile::NamedTempFile;
155
156 #[test]
157 fn test_parse_simple_yaml() {
158 let mut file = NamedTempFile::new().unwrap();
159 write!(file, "key: value").unwrap();
160
161 let entries = YamlParser::parse_file(file.path()).unwrap();
162 assert_eq!(entries.len(), 1);
163 assert_eq!(entries[0].key, "key");
164 assert_eq!(entries[0].value, "value");
165 assert_eq!(entries[0].line, 1);
166 }
167
168 #[test]
169 fn test_parse_nested_yaml() {
170 let mut file = NamedTempFile::new().unwrap();
171 write!(file, "parent:\n child: value").unwrap();
172
173 let entries = YamlParser::parse_file(file.path()).unwrap();
174 assert_eq!(entries.len(), 1);
175 assert_eq!(entries[0].key, "parent.child");
176 assert_eq!(entries[0].value, "value");
177 assert_eq!(entries[0].line, 2);
178 }
179
180 #[test]
181 fn test_parse_multiple_keys() {
182 let mut file = NamedTempFile::new().unwrap();
183 write!(
184 file,
185 "
186key1: value1
187key2: value2
188nested:
189 key3: value3
190"
191 )
192 .unwrap();
193
194 let entries = YamlParser::parse_file(file.path()).unwrap();
195 assert_eq!(entries.len(), 3);
196
197 let entry1 = entries.iter().find(|e| e.key == "key1").unwrap();
199 assert_eq!(entry1.value, "value1");
200 assert_eq!(entry1.line, 2);
201
202 let entry2 = entries.iter().find(|e| e.key == "key2").unwrap();
203 assert_eq!(entry2.value, "value2");
204 assert_eq!(entry2.line, 3);
205
206 let entry3 = entries.iter().find(|e| e.key == "nested.key3").unwrap();
207 assert_eq!(entry3.value, "value3");
208 assert_eq!(entry3.line, 5);
209 }
210
211 #[test]
212 fn test_parse_yaml_array() {
213 let mut file = NamedTempFile::new().unwrap();
214 write!(file, "list:\n - item1\n - item2").unwrap();
215
216 let entries = YamlParser::parse_file(file.path()).unwrap();
217 assert_eq!(entries.len(), 2);
218
219 let item1 = entries.iter().find(|e| e.value == "item1").unwrap();
220 assert_eq!(item1.key, "list.0");
221
222 let item2 = entries.iter().find(|e| e.value == "item2").unwrap();
223 assert_eq!(item2.key, "list.1");
224 }
225}