serde_hkx/xml/de/parser/
tag.rs

1//! XML tag parsers
2use crate::lib::*;
3
4use super::{
5    delimited_comment_multispace0, delimited_multispace0_comment, delimited_with_multispace0,
6    type_kind::pointer,
7};
8use havok_types::{Pointer, Signature};
9use winnow::ascii::{digit1, hex_digit1, oct_digit1};
10use winnow::combinator::{delimited, dispatch, fail, seq};
11use winnow::error::{ContextError, StrContext, StrContextValue, StrContextValue::*};
12use winnow::token::{take, take_until};
13use winnow::{ModalResult, Parser};
14
15/// Parses the start tag `<tag>`
16pub fn start_tag<'a>(tag: &'static str) -> impl Parser<&'a str, (), ContextError> {
17    seq!(
18        _: delimited_comment_multispace0("<"),
19        _: delimited_with_multispace0(tag),
20        _: delimited_multispace0_comment(">")
21    )
22    .context(StrContext::Label("start tag"))
23    .context(StrContext::Label(tag))
24}
25
26/// Parses the end tag `</tag>`
27pub fn end_tag<'a>(tag: &'static str) -> impl Parser<&'a str, (), ContextError> {
28    seq!(
29        _: delimited_comment_multispace0("<"),
30        _: delimited_with_multispace0("/"),
31        _: delimited_with_multispace0(tag),
32        _: delimited_multispace0_comment(">")
33    )
34    .context(StrContext::Label("end tag"))
35    .context(StrContext::Label(tag))
36}
37
38/// Parses the array start tag (e.g. `<hkobject name="#0010" class="hkbProjectData" signature="0x13a39ba7">`)
39///
40/// # Returns
41/// ([`Pointer`], ClassName, [`Signature`]) -> e.g. (`#0010`, `"hkbProjectData"`, `0x13a39ba7`)
42///
43/// # Errors
44/// When parse failed.
45pub fn class_start_tag<'a>(input: &mut &'a str) -> ModalResult<(Pointer, &'a str, Signature)> {
46    seq!(
47        _: delimited_comment_multispace0("<"),
48        _: delimited_with_multispace0("hkobject"),
49        _: delimited_with_multispace0("name"),
50        _: delimited_with_multispace0("="),
51        attr_ptr,
52
53        _: delimited_with_multispace0("class"),
54        _: delimited_with_multispace0("="),
55        attr_string,
56
57        _: delimited_with_multispace0("signature"),
58        _: delimited_with_multispace0("="),
59        _: delimited_with_multispace0("\""),
60        radix_digits.map(|digits| Signature::new(digits as u32)),
61        _: delimited_with_multispace0("\""),
62        _: delimited_multispace0_comment(">")
63    )
64    .context(StrContext::Label("Class start tag"))
65    .context(StrContext::Expected(StrContextValue::Description(
66        r##"e.g. `<hkobject name="#0010" class="hkbProjectData" signature="0x13a39ba7">`"##,
67    )))
68    .parse_next(input)
69}
70
71/// Parses the field of class start opening tag `<hkparam name=`
72///
73/// # Note
74/// All arguments are used only for clarity of error reporting.
75pub fn field_start_open_tag<'a>(
76    class_name: &'static str,
77) -> impl Parser<&'a str, (), ContextError> {
78    seq!(
79        _: delimited_comment_multispace0("<"),
80        _: delimited_with_multispace0("hkparam"),
81        _: delimited_with_multispace0("name"),
82        _: delimited_with_multispace0("="),
83    )
84    .context(StrContext::Label("field of class: start opening tag"))
85    .context(StrContext::Label(class_name))
86    .context(StrContext::Expected(StrContextValue::Description(
87        "e.g. `<hkparam name=`",
88    )))
89}
90
91/// Parses the field of class start closing tag `>`, `numelements="0">`, `/>`, or `numelements="0" />`
92///
93/// # Returns
94/// (numelements, is_self_closing)
95///
96/// # Errors
97/// When parse failed.
98pub fn field_start_close_tag(input: &mut &str) -> ModalResult<(Option<u64>, bool)> {
99    use winnow::combinator::alt;
100
101    alt((
102        // Handle self-closing tag with numelements: numelements="0" />
103        seq!(
104            seq!(
105                _: delimited_with_multispace0("numelements"),
106                _: delimited_with_multispace0("="),
107                number_in_string::<u64>, // e.g. "8"
108            ),
109            _: delimited_with_multispace0("/"),
110            _: delimited_multispace0_comment(">")
111        )
112        .map(|((n,),)| (Some(n), true)),
113        // Handle regular closing tag with numelements: numelements="0">
114        seq!(
115            seq!(
116                _: delimited_with_multispace0("numelements"),
117                _: delimited_with_multispace0("="),
118                number_in_string::<u64>, // e.g. "8"
119            ),
120            _: delimited_multispace0_comment(">")
121        )
122        .map(|((n,),)| (Some(n), false)),
123        // Handle self-closing tag without numelements: />
124        seq!(
125            _: delimited_with_multispace0("/"),
126            _: delimited_multispace0_comment(">")
127        )
128        .map(|()| (None, true)),
129        // Handle regular closing tag without numelements: >
130        seq!(
131            _: delimited_multispace0_comment(">")
132        )
133        .map(|()| (None, false)),
134    ))
135    .context(StrContext::Label("field of class: start closing tag"))
136    .context(StrContext::Expected(StrContextValue::Description(
137        "e.g. `>`, `/>`, `numelements=\"0\">`, or `numelements=\"0\" />`",
138    )))
139    .parse_next(input)
140}
141
142////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
143
144// There are support functions that exists only to parse the attributes in the tag.
145
146/// Parses a number inside a string, e.g., `"64"`
147///
148/// # Errors
149/// When parse failed.
150pub fn number_in_string<Num>(input: &mut &str) -> ModalResult<Num>
151where
152    Num: FromStr,
153{
154    attr_string
155        .parse_to()
156        .context(StrContext::Label("number in string"))
157        .context(StrContext::Expected(Description(r#"Number(e.g. `"64"`)"#)))
158        .parse_next(input)
159}
160
161/// Parses a xml attribute string(surrounded double quotes), e.g. `"string"`
162///
163/// # Errors
164/// When parse failed.
165pub fn attr_string<'a>(input: &mut &'a str) -> ModalResult<&'a str> {
166    delimited("\"", take_until(0.., "\""), "\"")
167        .context(StrContext::Label("String in XML attribute"))
168        .context(StrContext::Expected(Description(r#"String(e.g. `"Str"`)"#)))
169        .parse_next(input)
170}
171
172/// Parser a xml attribute pointer in string, e.g. `"#0050"`
173///
174/// # Errors
175/// When parse failed.
176fn attr_ptr(input: &mut &str) -> ModalResult<Pointer> {
177    delimited("\"", pointer, "\"").parse_next(input)
178}
179
180/// Parse radix digits. e.g. `0b101`, `0xff`
181///
182/// # Errors
183/// When parse failed.
184fn radix_digits(input: &mut &str) -> ModalResult<usize> {
185    dispatch!(take(2_usize);
186        "0b" | "0B" => digit1.try_map(|s| usize::from_str_radix(s, 2))
187                        .context(StrContext::Label("digit")).context(StrContext::Expected(StrContextValue::Description("binary"))),
188        "0o" | "0O" => oct_digit1.try_map(|s| usize::from_str_radix(s, 8))
189                        .context(StrContext::Label("digit")).context(StrContext::Expected(StrContextValue::Description("octal"))),
190        "0d" | "0D" => digit1.try_map(|s: &str| s.parse::<usize>())
191                        .context(StrContext::Label("digit")).context(StrContext::Expected(StrContextValue::Description("decimal"))),
192        "0x" | "0X" => hex_digit1.try_map(|s|usize::from_str_radix(s, 16))
193                        .context(StrContext::Label("digit")).context(StrContext::Expected(StrContextValue::Description("hexadecimal"))),
194        _ => fail.context(StrContext::Label("radix prefix"))
195                .context(StrContext::Expected(StrContextValue::StringLiteral("0b")))
196                .context(StrContext::Expected(StrContextValue::StringLiteral("0o")))
197                .context(StrContext::Expected(StrContextValue::StringLiteral("0d")))
198                .context(StrContext::Expected(StrContextValue::StringLiteral("0x"))),
199    ).parse_next(input)
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use crate::errors::readable::ReadableError;
206
207    #[test]
208    fn test_radix_digits() {
209        assert_eq!(radix_digits.parse("0b001010"), Ok(10));
210        assert_eq!(radix_digits.parse("0o57"), Ok(47));
211        assert_eq!(radix_digits.parse("0x1234"), Ok(4660));
212    }
213
214    #[test]
215    fn test_parse_start_tag() {
216        assert!(start_tag("tag").parse("<tag>").is_ok());
217        assert!(start_tag("hkparam").parse("< hkparam \n\n>").is_ok());
218        assert!(start_tag("tag").parse("<tag   >").is_ok());
219    }
220
221    #[test]
222    fn test_parse_end_tag() {
223        assert!(end_tag("tag").parse("</tag>").is_ok());
224        assert!(end_tag("tag").parse("</ tag >").is_ok());
225        assert!(end_tag("tag").parse("</  tag  >").is_ok());
226
227        let input = "</ hkparam >\n";
228        if let Err(err) = end_tag("hkparam")
229            .parse(input)
230            .map_err(|e| ReadableError::from_parse(e, input).to_string())
231        {
232            panic!("{err}");
233        };
234    }
235
236    #[test]
237    fn test_parse_array_start_close_tag() {
238        fn test_parse(input: &str, expected: (Option<u64>, bool)) {
239            match field_start_close_tag
240                .parse(input)
241                .map_err(|e| ReadableError::from_parse(e, input).to_string())
242            {
243                Ok(res) => assert_eq!(res, expected),
244                Err(err) => panic!("{err}"),
245            }
246        }
247
248        // Test regular closing tags
249        let ideal_input = r#" numelements="3">"#;
250        test_parse(ideal_input, (Some(3), false));
251
252        let indent_input = r#"
253
254          numelements
255  = "85"
256
257>"#;
258        test_parse(indent_input, (Some(85), false));
259
260        let simple_closing_input = r#" >"#;
261        test_parse(simple_closing_input, (None, false));
262
263        // Test self-closing tags
264        let self_closing_input = r#" numelements="5" />"#;
265        test_parse(self_closing_input, (Some(5), true));
266
267        let simple_self_closing_input = r#" />"#;
268        test_parse(simple_self_closing_input, (None, true));
269
270        // Test self-closing tags with spaces
271        let spaced_self_closing_input = r#" numelements="0"  /  >"#;
272        test_parse(spaced_self_closing_input, (Some(0), true));
273
274        let spaced_simple_self_closing_input = r#"  /  >"#;
275        test_parse(spaced_simple_self_closing_input, (None, true));
276    }
277
278    #[test]
279    fn test_parse_number_in_string() {
280        assert_eq!(number_in_string.parse(r#""33""#), Ok(33));
281        assert_eq!(number_in_string.parse(r#""100""#), Ok(100));
282        assert_eq!(number_in_string.parse(r#""0""#), Ok(0));
283    }
284}