1use anyhow::Error;
2use proc_macro2::{TokenStream, TokenTree};
3use quote::ToTokens;
4use rust_i18n_support::I18nConfig;
5use std::collections::HashMap;
6use std::path::PathBuf;
7
8pub type Results = HashMap<String, Message>;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct Location {
12 pub file: std::path::PathBuf,
13 pub line: usize,
14}
15
16#[derive(Debug, Clone, Default, PartialEq, Eq)]
17pub struct Message {
18 pub key: String,
19 pub index: usize,
20 pub minify_key: bool,
21 pub locations: Vec<Location>,
22}
23
24impl Message {
25 fn new(key: &str, index: usize, minify_key: bool) -> Self {
26 Self {
27 key: key.to_owned(),
28 index,
29 minify_key,
30 locations: vec![],
31 }
32 }
33}
34
35static METHOD_NAMES: &[&str] = &["t", "tr"];
36
37#[allow(clippy::ptr_arg)]
38pub fn extract(
39 results: &mut Results,
40 path: &PathBuf,
41 source: &str,
42 cfg: I18nConfig,
43) -> Result<(), Error> {
44 let mut ex = Extractor { results, path, cfg };
45
46 let file = syn::parse_file(source)
47 .unwrap_or_else(|_| panic!("Failed to parse file, file: {}", path.display()));
48 let stream = file.into_token_stream();
49 ex.invoke(stream)
50}
51
52#[allow(dead_code)]
53struct Extractor<'a> {
54 results: &'a mut Results,
55 path: &'a PathBuf,
56 cfg: I18nConfig,
57}
58
59impl<'a> Extractor<'a> {
60 fn invoke(&mut self, stream: TokenStream) -> Result<(), Error> {
61 let mut token_iter = stream.into_iter().peekable();
62
63 while let Some(token) = token_iter.next() {
64 match token {
65 TokenTree::Group(group) => self.invoke(group.stream())?,
66 TokenTree::Ident(ident) => {
67 let mut is_macro = false;
68 if let Some(TokenTree::Punct(punct)) = token_iter.peek() {
69 if punct.to_string() == "!" {
70 is_macro = true;
71 token_iter.next();
72 }
73 }
74
75 let ident_str = ident.to_string();
76 if METHOD_NAMES.contains(&ident_str.as_str()) && is_macro {
77 if let Some(TokenTree::Group(group)) = token_iter.peek() {
78 self.take_message(group.stream());
79 }
80 }
81 }
82 _ => {}
83 }
84 }
85
86 Ok(())
87 }
88
89 fn take_message(&mut self, stream: TokenStream) {
90 let mut token_iter = stream.into_iter().peekable();
91
92 let literal = if let Some(TokenTree::Literal(literal)) = token_iter.next() {
93 literal
94 } else {
95 return;
96 };
97
98 let I18nConfig {
99 minify_key,
100 minify_key_len,
101 minify_key_prefix,
102 minify_key_thresh,
103 ..
104 } = &self.cfg;
105 let key: Option<proc_macro2::Literal> = Some(literal);
106
107 if let Some(lit) = key {
108 if let Some(key) = literal_to_string(&lit) {
109 let (message_key, message_content) = if *minify_key {
110 let hashed_key = rust_i18n_support::MinifyKey::minify_key(
111 &key,
112 *minify_key_len,
113 minify_key_prefix,
114 *minify_key_thresh,
115 );
116 (hashed_key.to_string(), key.clone())
117 } else {
118 let message_key = format_message_key(&key);
119 (message_key.clone(), message_key)
120 };
121 let index = self.results.len();
122 let message = self
123 .results
124 .entry(message_key)
125 .or_insert_with(|| Message::new(&message_content, index, *minify_key));
126
127 let span = lit.span();
128 let line = span.start().line;
129 if line > 0 {
130 message.locations.push(Location {
131 file: self.path.clone(),
132 line,
133 });
134 }
135 }
136 }
137 }
138}
139
140fn literal_to_string(lit: &proc_macro2::Literal) -> Option<String> {
141 match syn::parse_str::<syn::LitStr>(&lit.to_string()) {
142 Ok(lit) => Some(lit.value()),
143 Err(_) => None,
144 }
145}
146
147fn format_message_key(key: &str) -> String {
148 let re = regex::Regex::new(r"\s+").unwrap();
149 let key = re.replace_all(key, " ").into_owned();
150 key.trim().into()
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use std::str::FromStr;
157
158 macro_rules! build_messages {
159 {$(($key:tt, $($line:tt),+)),+} => {{
160 let mut results = Vec::<Message>::new();
161 $(
162 let message = Message {
163 key: $key.into(),
164 locations: vec![
165 $(
166 Location {
167 file: PathBuf::from_str("hello.rs").unwrap(),
168 line: $line
169 },
170 )+
171 ],
172 index: 0,
173 minify_key: false,
174 };
175 results.push(message);
176 )+
177
178 results
179 }}
180 }
181
182 #[test]
183 fn test_format_message_key() {
184 assert_eq!(format_message_key("Hello world"), "Hello world".to_owned());
185 assert_eq!(format_message_key("\n "), "".to_owned());
186 assert_eq!(format_message_key("\n "), "".to_owned());
187 assert_eq!(format_message_key("\n hello"), "hello".to_owned());
188 assert_eq!(format_message_key("\n hello\n"), "hello".to_owned());
189 assert_eq!(format_message_key("\n hello\n "), "hello".to_owned());
190 assert_eq!(
191 format_message_key("\n hello\n world"),
192 "hello world".to_owned()
193 );
194 assert_eq!(
195 format_message_key("\n hello\n world\n\n"),
196 "hello world".to_owned()
197 );
198 assert_eq!(
199 format_message_key("\n hello\n world\n "),
200 "hello world".to_owned()
201 );
202 assert_eq!(
203 format_message_key(" hello\n world\n "),
204 "hello world".to_owned()
205 );
206 assert_eq!(
207 format_message_key(
208 r#"Use YAML for mapping localized text,
209 and support mutiple YAML files merging."#
210 ),
211 "Use YAML for mapping localized text, and support mutiple YAML files merging."
212 .to_owned()
213 );
214 }
215
216 #[test]
217 fn test_extract() {
218 let source = include_str!("example.test.rs");
219 let stream = proc_macro2::TokenStream::from_str(source).unwrap();
220
221 let expected = build_messages![
222 ("hello", 4),
223 ("views.message.title", 5),
224 ("views.message.description", 7),
225 (
226 "Use YAML for mapping localized text, and support mutiple YAML files merging.",
227 11,
228 14
229 ),
230 (
231 "The table below describes some of those behaviours.",
232 18,
233 20
234 )
235 ];
236
237 let mut results = HashMap::new();
238
239 let mut ex = Extractor {
240 results: &mut results,
241 path: &"hello.rs".to_owned().into(),
242 cfg: I18nConfig::default(),
243 };
244
245 ex.invoke(stream).unwrap();
246
247 let mut messages: Vec<_> = ex.results.values().collect();
248 messages.sort_by_key(|m| m.index);
249 assert_eq!(expected.len(), messages.len());
250
251 for (expected_message, actually_message) in expected.iter().zip(messages) {
252 let mut actually_message = actually_message.clone();
253 actually_message.index = 0;
254
255 assert_eq!(*expected_message, actually_message);
256 }
257 }
258}