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 {
137                                unsafe { unreachable_unchecked() }
138                            };
139                            (format.len(), args)
140                        }
141                        LocaleFmt::Formatted {
142                            ref format,
143                            ref mut args,
144                        } => (format.len(), args),
145                    };
146
147                    args.push((start..end, i));
148                    (end, fmt)
149                }
150            },
151        ),
152        eof,
153    ))
154    .map(|((.., fmt), ..)| fmt)
155    .parse(input)
156}
157
158impl Serialize for LocaleFmt {
159    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
160    where
161        S: Serializer,
162    {
163        match self {
164            Self::Unformatted(raw) => serializer.serialize_str(raw),
165            Self::Formatted { format, args } => {
166                let mut out = String::new();
167
168                let mut last = 0;
169                for &(ref range, i) in args {
170                    // Some sanity checks in case some users for some reason modify the locales manually.
171                    let start = range.start.min(format.len());
172                    let end = range.end.min(format.len());
173                    last = last.max(end);
174
175                    // All these unwraps shouldn't panic.
176                    out.push_str(&format[start..end]);
177                    out.push('{');
178                    out.push_str(&i.to_string());
179                    out.push('}');
180                }
181                out.push_str(&format[last..]);
182
183                serializer.serialize_str(&out)
184            }
185        }
186    }
187}
188
189impl<'de> Deserialize<'de> for LocaleFmt {
190    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
191    where
192        D: Deserializer<'de>,
193    {
194        struct Parser;
195        impl Visitor<'_> for Parser {
196            type Value = LocaleFmt;
197
198            #[inline]
199            fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
200                write!(formatter, "a valid UTF-8 string")
201            }
202
203            #[inline]
204            fn visit_str<E>(self, input: &str) -> Result<Self::Value, E>
205            where
206                E: de::Error,
207            {
208                match parse::<VerboseError<&str>>(input) {
209                    Ok(("", fmt)) => Ok(fmt),
210                    Ok(..) => unreachable!("`cut(eof)` should've ruled out leftover data"),
211                    Err(e) => Err(match e {
212                        NomErr::Error(e) | NomErr::Failure(e) => E::custom(convert_error(input, e)),
213                        NomErr::Incomplete(..) => unreachable!("only complete operations are used"),
214                    }),
215                }
216            }
217        }
218
219        deserializer.deserialize_str(Parser)
220    }
221}
222
223impl FromStr for LocaleFmt {
224    type Err = VerboseError<String>;
225
226    #[inline]
227    fn from_str(input: &str) -> Result<Self, Self::Err> {
228        match parse::<VerboseError<&str>>(input) {
229            Ok(("", fmt)) => Ok(fmt),
230            Ok(..) => unreachable!("`cut(eof)` should've ruled out leftover data"),
231            Err(e) => Err(match e {
232                NomErr::Error(e) | NomErr::Failure(e) => e.into(),
233                NomErr::Incomplete(..) => unreachable!("only complete operations are used"),
234            }),
235        }
236    }
237}
238
239/// Errors that may arise when loading [`Locale`]s using [`LocaleLoader`].
240#[derive(Error, Debug)]
241pub enum LocaleError {
242    /// An IO error occurred.
243    #[error(transparent)]
244    Io(#[from] IoError),
245    /// A syntax error occurred.
246    #[error(transparent)]
247    Ron(#[from] SpannedError),
248}
249
250/// Dedicated [`AssetLoader`] for loading [`Locale`]s.
251pub struct LocaleLoader;
252impl AssetLoader for LocaleLoader {
253    type Asset = Locale;
254    type Settings = ();
255    type Error = LocaleError;
256
257    async fn load(
258        &self,
259        reader: &mut dyn Reader,
260        _: &Self::Settings,
261        _: &mut LoadContext<'_>,
262    ) -> Result<Self::Asset, Self::Error> {
263        Ok(Locale(ron::de::from_bytes::<HashMap<String, LocaleFmt>>(&{
264            let mut bytes = Vec::new();
265            reader.read_to_end(&mut bytes).await?;
266
267            bytes
268        })?))
269    }
270
271    #[inline]
272    fn extensions(&self) -> &[&str] {
273        &["locale.ron"]
274    }
275}
276
277/// Errors that may arise when loading [`LocaleCollection`]s using [`LocaleCollectionLoader`].
278#[derive(Error, Debug)]
279pub enum LocaleCollectionError {
280    /// An IO error occurred.
281    #[error(transparent)]
282    Io(#[from] IoError),
283    /// A syntax error occurred.
284    #[error(transparent)]
285    Ron(#[from] SpannedError),
286    /// Invalid sub-asset path.
287    #[error(transparent)]
288    InvalidPath(#[from] ParseAssetPathError),
289    /// A default locale is defined, but is not available.
290    #[error("locale default '{0}' is defined, but is not available in `locales`")]
291    MissingDefault(String),
292}
293
294#[derive(Deserialize)]
295struct LocaleCollectionFile {
296    default: String,
297    languages: Vec<String>,
298}
299
300/// Dedicated [`AssetLoader`] for loading [`LocaleCollection`]s.
301pub struct LocaleCollectionLoader;
302impl AssetLoader for LocaleCollectionLoader {
303    type Asset = LocaleCollection;
304    type Settings = ();
305    type Error = LocaleCollectionError;
306
307    async fn load(
308        &self,
309        reader: &mut dyn Reader,
310        _: &Self::Settings,
311        load_context: &mut LoadContext<'_>,
312    ) -> Result<Self::Asset, Self::Error> {
313        let file = ron::de::from_bytes::<LocaleCollectionFile>(&{
314            let mut bytes = Vec::new();
315            reader.read_to_end(&mut bytes).await?;
316
317            bytes
318        })?;
319
320        let mut asset = LocaleCollection {
321            default: file.default,
322            languages: HashMap::with_capacity(file.languages.len()),
323        };
324
325        for key in file.languages {
326            let path = load_context.asset_path().resolve_embed(&format!("locale_{key}.locale.ron"))?;
327            asset.languages.insert(key, load_context.load(path));
328        }
329
330        if !asset.languages.contains_key(&asset.default) {
331            return Err(LocaleCollectionError::MissingDefault(asset.default));
332        }
333
334        Ok(asset)
335    }
336
337    #[inline]
338    fn extensions(&self) -> &[&str] {
339        &["locales.ron"]
340    }
341}