hephae_locale/
loader.rs

1//! Defines asset loaders for [`Locale`] and [`LocaleLoader`].
2
3use 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/// Parses `\u{xxxxxx}` escaped unicode character.
33#[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/// Parses `\...` escaped character.
55#[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/// Parses `{index}` and extracts the index as positional argument.
78#[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/// Parses escaped `{{` and `}}`.
91#[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/// Parses any characters preceding a backslash or a brace, leaving `{{` and `}}` as special cases.
99#[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                            // Safety: We just set `fmt` to variant `Formatted` above.
136                            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                    // Some sanity checks in case some users for some reason modify the locales manually.
169                    let start = range.start.min(format.len());
170                    let end = range.end.min(format.len());
171                    last = last.max(end);
172
173                    // All these unwraps shouldn't panic.
174                    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/// Errors that may arise when loading [`Locale`]s using [`LocaleLoader`].
238#[derive(Error, Debug)]
239pub enum LocaleError {
240    /// An IO error occurred.
241    #[error(transparent)]
242    Io(#[from] IoError),
243    /// A syntax error occurred.
244    #[error(transparent)]
245    Ron(#[from] SpannedError),
246}
247
248/// Dedicated [`AssetLoader`] for loading [`Locale`]s.
249pub 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/// Errors that may arise when loading [`LocaleCollection`]s using [`LocaleCollectionLoader`].
276#[derive(Error, Debug)]
277pub enum LocaleCollectionError {
278    /// An IO error occurred.
279    #[error(transparent)]
280    Io(#[from] IoError),
281    /// A syntax error occurred.
282    #[error(transparent)]
283    Ron(#[from] SpannedError),
284    /// Invalid sub-asset path.
285    #[error(transparent)]
286    InvalidPath(#[from] ParseAssetPathError),
287    /// A default locale is defined, but is not available.
288    #[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
298/// Dedicated [`AssetLoader`] for loading [`LocaleCollection`]s.
299pub 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}