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            _ => {
134                // Ignore arrays and other types for now
135            }
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        // Find entries by key
188        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}