cs/parse/
yaml_parser.rs

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
10/// Parser for YAML translation files
11pub 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        // First, build a map of scalar values to their line numbers using Scanner
20        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                        // Store the line number for this scalar value
28                        value_to_line.insert(value, token.0.line());
29                    }
30                }
31                Ok(None) => break, // End of tokens
32                Err(_) => break,   // Error, stop scanning
33            }
34        }
35
36        // Then, use YamlLoader to parse the structure
37        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                        // For root level locale keys (like "en", "fr"), also create entries without the locale prefix
69                        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 this is a locale root, also flatten without the locale prefix
88                        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                // Ignore other types for now
145            }
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        // Find entries by key
198        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}