1use alloc::format;
22use alloc::string::String;
23use alloc::vec::Vec;
24use datetime::Civil;
25
26use super::{DATE_LONG, DATE_SHORT, Locale, language_code, locale, plural};
27
28#[derive(Clone, Copy, Debug)]
30#[non_exhaustive]
31pub enum Arg<'a> {
32 Str(&'a str),
33 Int(i64),
34 Num(f64),
35 Date(Civil),
36}
37
38impl<'a> From<&'a str> for Arg<'a> {
39 fn from(s: &'a str) -> Self {
40 Arg::Str(s)
41 }
42}
43impl<'a> From<&'a String> for Arg<'a> {
44 fn from(s: &'a String) -> Self {
45 Arg::Str(s)
46 }
47}
48impl From<i64> for Arg<'_> {
49 fn from(n: i64) -> Self {
50 Arg::Int(n)
51 }
52}
53impl From<i32> for Arg<'_> {
54 fn from(n: i32) -> Self {
55 Arg::Int(n as i64)
56 }
57}
58impl From<u32> for Arg<'_> {
59 fn from(n: u32) -> Self {
60 Arg::Int(n as i64)
61 }
62}
63impl From<u64> for Arg<'_> {
64 fn from(n: u64) -> Self {
65 Arg::Int(n.min(i64::MAX as u64) as i64)
66 }
67}
68impl From<usize> for Arg<'_> {
69 fn from(n: usize) -> Self {
70 Arg::Int(n.min(i64::MAX as usize) as i64)
71 }
72}
73impl From<f64> for Arg<'_> {
74 fn from(v: f64) -> Self {
75 Arg::Num(v)
76 }
77}
78impl From<f32> for Arg<'_> {
79 fn from(v: f32) -> Self {
80 Arg::Num(v as f64)
81 }
82}
83impl From<Civil> for Arg<'_> {
84 fn from(c: Civil) -> Self {
85 Arg::Date(c)
86 }
87}
88
89struct Cx<'a, 'b> {
90 args: &'b [(&'b str, Arg<'a>)],
91 loc: Locale,
92 lang: String,
93}
94
95pub fn msg(text: &str, args: &[(&str, Arg)]) -> String {
97 let s: Vec<char> = text.chars().collect();
98 let cx = Cx { args, loc: locale(), lang: language_code() };
99 let mut out = String::with_capacity(text.len() + 16);
100 let (mut p, mut seq) = (0, 0);
101 run(&s, &mut p, &cx, &mut seq, None, false, &mut out);
102 out
103}
104
105fn run(s: &[char], p: &mut usize, cx: &Cx, seq: &mut usize, hash: Option<&str>, nested: bool, out: &mut String) {
107 while *p < s.len() {
108 let c = s[*p];
109 match c {
110 '\'' => quote(s, p, hash.is_some(), out),
111 '{' => {
112 let start = *p;
113 *p += 1;
114 if arg(s, p, cx, seq, out).is_none() {
115 out.extend(&s[start..]);
116 *p = s.len();
117 }
118 }
119 '}' if nested => return,
120 '#' if hash.is_some() => {
121 out.push_str(hash.unwrap_or(""));
122 *p += 1;
123 }
124 _ => {
125 out.push(c);
126 *p += 1;
127 }
128 }
129 }
130}
131
132fn quote(s: &[char], p: &mut usize, in_plural: bool, out: &mut String) {
134 let next = s.get(*p + 1).copied();
135 if next == Some('\'') {
136 out.push('\'');
137 *p += 2;
138 return;
139 }
140 if !(matches!(next, Some('{' | '}')) || (in_plural && next == Some('#'))) {
141 out.push('\'');
142 *p += 1;
143 return;
144 }
145 *p += 1;
146 while *p < s.len() {
147 if s[*p] == '\'' {
148 if s.get(*p + 1) == Some(&'\'') {
149 out.push('\'');
150 *p += 2;
151 continue;
152 }
153 *p += 1;
154 return;
155 }
156 out.push(s[*p]);
157 *p += 1;
158 }
159}
160
161fn skip_ws(s: &[char], p: &mut usize) {
162 while *p < s.len() && s[*p].is_whitespace() {
163 *p += 1;
164 }
165}
166
167fn word(s: &[char], p: &mut usize, stop: &[char]) -> String {
169 let start = *p;
170 while *p < s.len() && !stop.contains(&s[*p]) {
171 *p += 1;
172 }
173 let w: String = s[start..*p].iter().collect();
174 String::from(w.trim())
175}
176
177fn arg(s: &[char], p: &mut usize, cx: &Cx, seq: &mut usize, out: &mut String) -> Option<()> {
179 let name = word(s, p, &[',', '}']);
180 let value = lookup(cx, &name, seq);
181 if s.get(*p) == Some(&'}') {
182 *p += 1;
183 match value {
184 Some(v) => out.push_str(&plain(cx, v)),
185 None => out.push_str(&format!("{{{name}}}")),
186 }
187 return Some(());
188 }
189 *p += 1; let typ = word(s, p, &[',', '}']);
191 let style = if s.get(*p) == Some(&',') {
192 *p += 1;
193 match typ.as_str() {
194 "plural" | "select" => None,
195 _ => Some(word(s, p, &['}'])),
196 }
197 } else {
198 Some(String::new())
199 };
200 match typ.as_str() {
201 "plural" | "select" if style.is_none() => {
202 let opts = options(s, p)?;
203 let n = value.map(|v| number(cx, v));
204 let branch = if typ == "plural" { choose_plural(cx, &opts, n) } else { choose_select(&opts, value) }?;
205 let hash = if typ == "plural" { n.map(|(x, digits)| hash_text(cx, x - opts.offset, digits)) } else { None };
206 let (mut q, mut inner_seq) = (branch.0, *seq);
207 let mut sub = String::new();
208 run(&s[..branch.1], &mut q, cx, &mut inner_seq, hash.as_deref(), false, &mut sub);
209 out.push_str(&sub);
210 Some(())
211 }
212 _ => {
213 if s.get(*p) != Some(&'}') {
214 return None;
215 }
216 *p += 1;
217 let style = style.unwrap_or_default();
218 match value {
219 Some(v) => out.push_str(&typed(cx, v, &typ, &style)),
220 None => out.push_str(&format!("{{{name}}}")),
221 }
222 Some(())
223 }
224 }
225}
226
227fn lookup<'a>(cx: &Cx<'a, '_>, name: &str, seq: &mut usize) -> Option<Arg<'a>> {
228 if name.is_empty() {
229 *seq += 1;
230 return cx.args.get(*seq - 1).map(|a| a.1);
231 }
232 if name.bytes().all(|b| b.is_ascii_digit()) {
233 return name.parse::<usize>().ok().and_then(|k| cx.args.get(k)).map(|a| a.1);
234 }
235 cx.args.iter().find(|a| a.0 == name).map(|a| a.1)
236}
237
238struct Opts {
241 offset: f64,
242 list: Vec<(String, usize, usize)>,
243}
244
245fn options(s: &[char], p: &mut usize) -> Option<Opts> {
246 let mut o = Opts { offset: 0.0, list: Vec::new() };
247 loop {
248 skip_ws(s, p);
249 match s.get(*p) {
250 None => return None,
251 Some('}') => {
252 *p += 1;
253 return Some(o);
254 }
255 _ => {}
256 }
257 let sel = word(s, p, &['{', ' ', '\t', '\r', '\n', '}']);
258 if let Some(n) = sel.strip_prefix("offset:") {
259 o.offset = n.trim().parse().ok()?;
260 continue;
261 }
262 skip_ws(s, p);
263 if sel.is_empty() || s.get(*p) != Some(&'{') {
264 return None;
265 }
266 *p += 1;
267 let start = *p;
268 close(s, p)?;
269 o.list.push((sel, start, *p - 1));
270 }
271}
272
273fn close(s: &[char], p: &mut usize) -> Option<()> {
275 let mut depth = 0;
276 while *p < s.len() {
277 match s[*p] {
278 '\'' => {
279 let mut sink = String::new();
280 quote(s, p, true, &mut sink);
281 continue;
282 }
283 '{' => depth += 1,
284 '}' if depth == 0 => {
285 *p += 1;
286 return Some(());
287 }
288 '}' => depth -= 1,
289 _ => {}
290 }
291 *p += 1;
292 }
293 None
294}
295
296fn number(cx: &Cx, v: Arg) -> (f64, u32) {
298 match v {
299 Arg::Int(n) => (n as f64, 0),
300 Arg::Num(x) => {
301 let s = cx.loc.num_plain(x);
302 (x, s.split(cx.loc.decimal()).nth(1).map_or(0, |f| f.chars().count() as u32))
303 }
304 Arg::Str(t) => {
305 let t = t.trim();
306 let digits = t.split(['.', ',']).nth(1).map_or(0, |f| f.len() as u32);
307 (t.replace(',', ".").parse().unwrap_or(f64::NAN), digits)
308 }
309 Arg::Date(_) => (f64::NAN, 0),
310 }
311}
312
313fn hash_text(cx: &Cx, x: f64, digits: u32) -> String {
314 if x.is_nan() { String::new() } else { cx.loc.fmt_num(x, digits) }
315}
316
317fn choose_plural(cx: &Cx, o: &Opts, n: Option<(f64, u32)>) -> Option<(usize, usize)> {
318 let pick = |name: &str| o.list.iter().find(|e| e.0 == name).map(|e| (e.1, e.2));
319 if let Some((x, digits)) = n {
320 if let Some(e) = o.list.iter().find(|e| e.0.strip_prefix('=').and_then(|v| v.parse::<f64>().ok()) == Some(x)) {
322 return Some((e.1, e.2));
323 }
324 let y = x - o.offset;
325 if !y.is_nan() {
326 let a = if y < 0.0 { -y } else { y };
327 let (i, v, f) = operands(cx, a, digits);
328 if let Some(b) = pick(plural(&cx.lang, i, v, f).name()) {
329 return Some(b);
330 }
331 }
332 }
333 pick("other")
334}
335
336fn operands(cx: &Cx, a: f64, digits: u32) -> (u64, u32, u64) {
338 let s = cx.loc.num(a, digits, false);
339 let (int, frac) = s.split_once(cx.loc.decimal()).unwrap_or((&s, ""));
340 (int.parse().unwrap_or(u64::MAX), frac.len() as u32, frac.parse().unwrap_or(0))
341}
342
343fn choose_select(o: &Opts, v: Option<Arg>) -> Option<(usize, usize)> {
344 let key = match v {
345 Some(Arg::Str(s)) => Some(String::from(s)),
346 Some(Arg::Int(n)) => Some(format!("{n}")),
347 _ => None,
348 };
349 key.and_then(|k| o.list.iter().find(|e| e.0 == k)).or_else(|| o.list.iter().find(|e| e.0 == "other")).map(|e| (e.1, e.2))
350}
351
352fn plain(cx: &Cx, v: Arg) -> String {
354 match v {
355 Arg::Str(s) => String::from(s),
356 Arg::Int(n) => format!("{n}"),
357 Arg::Num(x) => cx.loc.num_plain(x),
358 Arg::Date(c) => cx.loc.fmt_date(&c, DATE_SHORT),
359 }
360}
361
362fn typed(cx: &Cx, v: Arg, typ: &str, style: &str) -> String {
364 let style = style.trim().trim_start_matches("::");
365 match (typ, v) {
366 ("date", Arg::Date(c)) => cx.loc.fmt_date(&c, if style == "long" || style == "full" { DATE_LONG } else { DATE_SHORT }),
367 ("time", Arg::Date(c)) => cx.loc.fmt_time(&c),
368 ("datetime", Arg::Date(c)) => cx.loc.fmt_date_time(&c),
369 ("number", Arg::Int(_) | Arg::Num(_) | Arg::Str(_)) => {
370 let (x, digits) = number(cx, v);
371 if x.is_nan() {
372 return plain(cx, v);
373 }
374 if style == "integer" {
375 cx.loc.fmt_num(x, 0)
376 } else if style == "percent" {
377 cx.loc.fmt_percent(x, 0)
378 } else if let Some(code) = style.strip_prefix("currency/") {
379 cx.loc.fmt_currency(x, code.trim())
380 } else if style == "size" {
381 cx.loc.fmt_size(if x > 0.0 { x as u64 } else { 0 })
382 } else {
383 cx.loc.fmt_num(x, digits)
384 }
385 }
386 _ => plain(cx, v),
387 }
388}