1use std::{fmt::Formatter, hint::unreachable_unchecked, io::Error as IoError, num::ParseIntError, str::FromStr};
4
5use bevy_asset::{AssetLoader, LoadContext, ParseAssetPathError, io::Reader, ron, ron::de::SpannedError};
6use bevy_utils::HashMap;
7use nom::{
8 Err as NomErr, IResult, Parser,
9 branch::alt,
10 bytes::complete::{is_not, tag, take_while_m_n, take_while1},
11 character::complete::char,
12 combinator::{cut, eof, map_opt, map_res, value, verify},
13 error::{FromExternalError, ParseError},
14 multi::fold,
15 sequence::{delimited, preceded},
16};
17use nom_language::error::{VerboseError, convert_error};
18use serde::{
19 Deserialize, Deserializer, Serialize, Serializer,
20 de::{self, Visitor},
21};
22use thiserror::Error;
23
24use crate::def::{Locale, LocaleCollection, LocaleFmt};
25
26enum FmtFrag<'a> {
27 Literal(&'a str),
28 Escaped(char),
29 Index(usize),
30}
31
32#[inline]
34fn parse_unicode<'a, E: ParseError<&'a str> + FromExternalError<&'a str, ParseIntError>>(
35 input: &'a str,
36) -> IResult<&'a str, char, E> {
37 map_opt(
38 map_res(
39 preceded(
40 char('u'),
41 cut(delimited(
42 char('{'),
43 take_while_m_n(1, 6, |c: char| c.is_ascii_hexdigit()),
44 char('}'),
45 )),
46 ),
47 |hex| u32::from_str_radix(hex, 16),
48 ),
49 char::from_u32,
50 )
51 .parse(input)
52}
53
54#[inline]
56fn parse_escaped<'a, E: ParseError<&'a str> + FromExternalError<&'a str, ParseIntError>>(
57 input: &'a str,
58) -> IResult<&'a str, FmtFrag<'a>, E> {
59 preceded(
60 char('\\'),
61 cut(alt((
62 parse_unicode,
63 value('\n', char('n')),
64 value('\r', char('r')),
65 value('\t', char('t')),
66 value('\u{08}', char('b')),
67 value('\u{0C}', char('f')),
68 value('\\', char('\\')),
69 value('/', char('/')),
70 value('"', char('"')),
71 ))),
72 )
73 .map(FmtFrag::Escaped)
74 .parse(input)
75}
76
77#[inline]
79fn parse_index<'a, E: ParseError<&'a str> + FromExternalError<&'a str, ParseIntError>>(
80 input: &'a str,
81) -> IResult<&'a str, FmtFrag<'a>, E> {
82 map_res(
83 delimited(char('{'), cut(take_while1(|c: char| c.is_ascii_digit())), char('}')),
84 usize::from_str,
85 )
86 .map(FmtFrag::Index)
87 .parse(input)
88}
89
90#[inline]
92fn parse_brace<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, FmtFrag<'a>, E> {
93 alt((value('{', tag("{{")), value('}', tag("}}"))))
94 .map(FmtFrag::Escaped)
95 .parse(input)
96}
97
98#[inline]
100fn parse_literal<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, FmtFrag<'a>, E> {
101 verify(is_not("\\{}"), |s: &str| !s.is_empty())
102 .map(FmtFrag::Literal)
103 .parse(input)
104}
105
106fn parse<'a, E: ParseError<&'a str> + FromExternalError<&'a str, ParseIntError>>(
107 input: &'a str,
108) -> IResult<&'a str, LocaleFmt, E> {
109 cut((
110 fold(
111 0..,
112 alt((parse_literal, parse_brace, parse_index, parse_escaped)),
113 || (0, LocaleFmt::Unformatted(String::new())),
114 |(start, mut fmt), frag| match frag {
115 FmtFrag::Literal(lit) => match &mut fmt {
116 LocaleFmt::Unformatted(format) | LocaleFmt::Formatted { format, .. } => {
117 format.push_str(lit);
118 (start, fmt)
119 }
120 },
121 FmtFrag::Escaped(c) => match &mut fmt {
122 LocaleFmt::Unformatted(format) | LocaleFmt::Formatted { format, .. } => {
123 format.push(c);
124 (start, fmt)
125 }
126 },
127 FmtFrag::Index(i) => {
128 let (end, args) = match fmt {
129 LocaleFmt::Unformatted(format) => {
130 fmt = LocaleFmt::Formatted {
131 format,
132 args: Vec::new(),
133 };
134
135 let LocaleFmt::Formatted { format, args } = &mut fmt else { unsafe { unreachable_unchecked() } };
137 (format.len(), args)
138 }
139 LocaleFmt::Formatted {
140 ref format,
141 ref mut args,
142 } => (format.len(), args),
143 };
144
145 args.push((start..end, i));
146 (end, fmt)
147 }
148 },
149 ),
150 eof,
151 ))
152 .map(|((.., fmt), ..)| fmt)
153 .parse(input)
154}
155
156impl Serialize for LocaleFmt {
157 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
158 where
159 S: Serializer,
160 {
161 match self {
162 Self::Unformatted(raw) => serializer.serialize_str(raw),
163 Self::Formatted { format, args } => {
164 let mut out = String::new();
165
166 let mut last = 0;
167 for &(ref range, i) in args {
168 let start = range.start.min(format.len());
170 let end = range.end.min(format.len());
171 last = last.max(end);
172
173 out.push_str(&format[start..end]);
175 out.push('{');
176 out.push_str(&i.to_string());
177 out.push('}');
178 }
179 out.push_str(&format[last..]);
180
181 serializer.serialize_str(&out)
182 }
183 }
184 }
185}
186
187impl<'de> Deserialize<'de> for LocaleFmt {
188 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
189 where
190 D: Deserializer<'de>,
191 {
192 struct Parser;
193 impl Visitor<'_> for Parser {
194 type Value = LocaleFmt;
195
196 #[inline]
197 fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
198 write!(formatter, "a valid UTF-8 string")
199 }
200
201 #[inline]
202 fn visit_str<E>(self, input: &str) -> Result<Self::Value, E>
203 where
204 E: de::Error,
205 {
206 match parse::<VerboseError<&str>>(input) {
207 Ok(("", fmt)) => Ok(fmt),
208 Ok(..) => unreachable!("`cut(eof)` should've ruled out leftover data"),
209 Err(e) => Err(match e {
210 NomErr::Error(e) | NomErr::Failure(e) => E::custom(convert_error(input, e)),
211 NomErr::Incomplete(..) => unreachable!("only complete operations are used"),
212 }),
213 }
214 }
215 }
216
217 deserializer.deserialize_str(Parser)
218 }
219}
220
221impl FromStr for LocaleFmt {
222 type Err = VerboseError<String>;
223
224 #[inline]
225 fn from_str(input: &str) -> Result<Self, Self::Err> {
226 match parse::<VerboseError<&str>>(input) {
227 Ok(("", fmt)) => Ok(fmt),
228 Ok(..) => unreachable!("`cut(eof)` should've ruled out leftover data"),
229 Err(e) => Err(match e {
230 NomErr::Error(e) | NomErr::Failure(e) => e.into(),
231 NomErr::Incomplete(..) => unreachable!("only complete operations are used"),
232 }),
233 }
234 }
235}
236
237#[derive(Error, Debug)]
239pub enum LocaleError {
240 #[error(transparent)]
242 Io(#[from] IoError),
243 #[error(transparent)]
245 Ron(#[from] SpannedError),
246}
247
248pub struct LocaleLoader;
250impl AssetLoader for LocaleLoader {
251 type Asset = Locale;
252 type Settings = ();
253 type Error = LocaleError;
254
255 async fn load(
256 &self,
257 reader: &mut dyn Reader,
258 _: &Self::Settings,
259 _: &mut LoadContext<'_>,
260 ) -> Result<Self::Asset, Self::Error> {
261 Ok(Locale(ron::de::from_bytes::<HashMap<String, LocaleFmt>>(&{
262 let mut bytes = Vec::new();
263 reader.read_to_end(&mut bytes).await?;
264
265 bytes
266 })?))
267 }
268
269 #[inline]
270 fn extensions(&self) -> &[&str] {
271 &["locale.ron"]
272 }
273}
274
275#[derive(Error, Debug)]
277pub enum LocaleCollectionError {
278 #[error(transparent)]
280 Io(#[from] IoError),
281 #[error(transparent)]
283 Ron(#[from] SpannedError),
284 #[error(transparent)]
286 InvalidPath(#[from] ParseAssetPathError),
287 #[error("locale default '{0}' is defined, but is not available in `locales`")]
289 MissingDefault(String),
290}
291
292#[derive(Deserialize)]
293struct LocaleCollectionFile {
294 default: String,
295 languages: Vec<String>,
296}
297
298pub struct LocaleCollectionLoader;
300impl AssetLoader for LocaleCollectionLoader {
301 type Asset = LocaleCollection;
302 type Settings = ();
303 type Error = LocaleCollectionError;
304
305 async fn load(
306 &self,
307 reader: &mut dyn Reader,
308 _: &Self::Settings,
309 load_context: &mut LoadContext<'_>,
310 ) -> Result<Self::Asset, Self::Error> {
311 let file = ron::de::from_bytes::<LocaleCollectionFile>(&{
312 let mut bytes = Vec::new();
313 reader.read_to_end(&mut bytes).await?;
314
315 bytes
316 })?;
317
318 let mut asset = LocaleCollection {
319 default: file.default,
320 languages: HashMap::with_capacity(file.languages.len()),
321 };
322
323 for key in file.languages {
324 let path = load_context.asset_path().resolve_embed(&format!("locale_{key}.locale.ron"))?;
325 asset.languages.insert(key, load_context.load(path));
326 }
327
328 if !asset.languages.contains_key(&asset.default) {
329 return Err(LocaleCollectionError::MissingDefault(asset.default));
330 }
331
332 Ok(asset)
333 }
334
335 #[inline]
336 fn extensions(&self) -> &[&str] {
337 &["locales.ron"]
338 }
339}