1use crate::{error::TextError, Result};
2use std::collections::HashMap;
3use std::io::{BufRead, Read};
4
5#[derive(Default)]
6pub struct Po {
7 header: Option<HashMap<String, String>>,
8 entities: Entities,
9 contexts: HashMap<String, Entities>,
10}
11
12type Entities = HashMap<String, String>;
13
14impl Po {
15 pub fn parse<R: Read>(reader: R) -> Result<Self> {
16 let mut reader = std::io::BufReader::new(reader);
17 let mut entities = HashMap::new();
18 let mut contexts: HashMap<String, Entities> = HashMap::new();
19 let mut line = String::new();
20
21 enum State {
22 None,
23 Context(String),
24 Msgid {
25 id: String,
26 ctx: Option<String>,
27 },
28 Entity {
29 msgid: String,
30 msgstr: String,
31 ctx: Option<String>,
32 },
33 }
34
35 let mut state = State::None;
36 loop {
37 line.clear();
38 let eof = reader.read_line(&mut line)?;
39 if eof == 0 {
40 match state {
41 State::Msgid { .. } | State::Context(..) => return Err(TextError::FormatError),
42 State::Entity { msgid, msgstr, ctx } => match ctx {
43 Some(ctx) => {
44 contexts.entry(ctx).or_default().insert(msgid, msgstr);
45 }
46 None => {
47 entities.insert(msgid, msgstr);
48 }
49 },
50 _ => (),
52 }
53 break;
54 }
55
56 let is_empty_line = line.is_empty() || line.trim().is_empty();
57 let is_comment = line.starts_with("#");
58 if is_empty_line || is_comment {
59 continue;
60 }
61
62 if line.starts_with("msgctxt") {
63 if let State::Entity { msgid, msgstr, ctx } = state {
66 match ctx {
67 Some(ctx) => {
68 contexts.entry(ctx).or_default().insert(msgid, msgstr);
69 }
70 None => {
71 entities.insert(msgid, msgstr);
72 }
73 }
74 }
75
76 let s: &str = line[7..].trim();
77 let context = unqoute(s).map(|s| s.to_owned())?;
78
79 state = State::Context(context);
80 continue;
81 }
82
83 if line.starts_with("msgid") {
84 let s: &str = line[5..].trim();
85 let id = unqoute(s).map(|s| s.to_owned())?;
86
87 match state {
89 State::Entity { msgid, msgstr, ctx } => match ctx {
92 Some(ctx) => {
93 contexts.entry(ctx).or_default().insert(msgid, msgstr);
94 state = State::Msgid { id, ctx: None };
95 }
96 None => {
97 entities.insert(msgid, msgstr);
98 state = State::Msgid { id, ctx: None };
99 }
100 },
101 State::Context(ctx) => {
102 state = State::Msgid { id, ctx: Some(ctx) };
103 }
104 _ => {
105 state = State::Msgid { id, ctx: None };
106 }
107 }
108
109 continue;
110 }
111
112 match state {
113 State::Msgid { id, ctx } if line.starts_with("msgstr") => {
114 let s: &str = line[6..].trim();
115 let msgstr = unqoute(s).map(|s| s.to_owned())?;
116
117 state = State::Entity {
118 msgid: id.clone(),
119 ctx,
120 msgstr,
121 };
122 continue;
123 }
124 State::Entity { ref mut msgstr, .. } if unqoute(line.trim()).is_ok() => {
126 let s = unqoute(line.trim()).unwrap();
127 msgstr.push_str(&s);
128 continue;
129 }
130 _ => return Err(TextError::FormatError),
132 }
133 }
134
135 let header = entities.get("").and_then(|s| Some(parse_header(s)));
136
137 Ok(Self {
138 entities,
139 contexts,
140 header,
141 })
142 }
143
144 pub fn get(&self, id: &str) -> Option<&str> {
145 self.entities.get(id).and_then(|s| Some(s.as_str()))
146 }
147
148 pub fn getc(&self, context: &str, id: &str) -> Option<&str> {
149 self.contexts
150 .get(context)
151 .and_then(|entities| entities.get(id))
152 .and_then(|s| Some(s.as_str()))
153 }
154
155 pub fn header(&self) -> Option<&HashMap<String, String>> {
156 self.header.as_ref()
157 }
158}
159
160fn parse_header(s: &str) -> HashMap<String, String> {
161 s.split("\\n")
164 .map(|line| {
165 line.find(":")
166 .and_then(|pos| Some((&line[..pos], &line[pos + 1..])))
167 })
168 .flatten()
169 .map(|(key, value)| (key.to_owned(), value.to_owned()))
170 .collect()
171}
172
173fn unqoute<'a>(s: &'a str) -> Result<&'a str> {
174 if !s.starts_with("\"") || !s.ends_with("\"") {
175 return Err(TextError::FormatError);
176 }
177
178 Ok(&s[1..s.len() - 1])
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn parse_po_file() {
187 let file = "msgid \"ask_location_menu.next_button\"\n\
188 msgstr \"Next\"\n";
189 let po = Po::parse(file.as_bytes()).unwrap();
190 assert_eq!(po.get("ask_location_menu.next_button"), Some("Next"));
191 }
192
193 #[test]
194 fn parse_po_file_messy() {
195 let file = "msgid \"ask_location_menu.next_button\" \n\
196 msgstr \"Next\" \n ";
197 let po = Po::parse(file.as_bytes()).unwrap();
198 assert_eq!(po.get("ask_location_menu.next_button"), Some("Next"));
199 }
200
201 #[test]
202 fn parse_po_file_emptylines() {
203 let file = r#"
204msgid "ask_location_menu.next_button"
205
206msgstr "Next""#;
207 let po = Po::parse(file.as_bytes()).unwrap();
208 assert_eq!(po.get("ask_location_menu.next_button"), Some("Next"));
209 }
210
211 #[test]
212 fn parse_po_file_comments() {
213 let file = r#"
214# translator-comments
215#. extracted-comments
216#: referenceā¦
217#, flagā¦
218msgid "ask_location_menu.next_button"
219msgstr "Next""#;
220 let po = Po::parse(file.as_bytes()).unwrap();
221 assert_eq!(po.get("ask_location_menu.next_button"), Some("Next"));
222 }
223
224 #[test]
225 fn parse_po_file_empty() {
226 let file = "";
227 let po = Po::parse(file.as_bytes());
228 assert!(po.is_ok());
229 }
230
231 #[test]
232 fn parse_po_file_multi_entities() {
233 let file = "msgid \"id1\"\n\
234 msgstr \"v1\"\n\
235 msgid \"id2\"\n\
236 msgstr \"v2\"\n";
237 let po = Po::parse(file.as_bytes()).unwrap();
238 assert_eq!(po.get("id1"), Some("v1"));
239 assert_eq!(po.get("id2"), Some("v2"));
240
241 let file = "msgid \"id1\"\n\
242 msgstr \"v1\"\n\
243 \n\
244 msgid \"id2\"\n\
245 msgstr \"v2\"\n";
246 let po = Po::parse(file.as_bytes()).unwrap();
247 assert_eq!(po.get("id1"), Some("v1"));
248 assert_eq!(po.get("id2"), Some("v2"));
249 }
250
251 #[test]
252 fn parse_po_file_multiline() {
253 let file = "msgid \"id\"\n\
254 msgstr \"1\"\n\
255 \"2\"\n\
256 \"3\"\n";
257 let po = Po::parse(file.as_bytes()).unwrap();
258 assert_eq!(po.get("id"), Some("123"));
259 }
260
261 #[test]
262 fn parse_po_file_context() {
263 let file = "msgctxt \"default\"\n\
264 msgid \"id\"\n\
265 msgstr \"1\"\n";
266 let po = Po::parse(file.as_bytes()).unwrap();
267 assert_eq!(po.getc("default", "id"), Some("1"));
268 assert_eq!(po.get("id"), None);
269 }
270}