mesh_loader/utils/
xml.rs

1// A module that provides utilities for parsing and visiting XML nodes.
2
3use std::{borrow::Cow, fmt, io, iter, marker::PhantomData, str::FromStr};
4
5pub(crate) use roxmltree::*;
6
7use super::{bytes::memchr_naive, float, int};
8
9#[inline]
10#[must_use]
11pub(crate) const fn is_whitespace(c: char) -> bool {
12    // https://www.w3.org/TR/xml/#NT-S
13    // Note: Unlike is_ascii_whitespace, FORM FEED ('\x0C') is not included.
14    matches!(c, '\t' | '\n' | '\r' | ' ')
15}
16
17#[inline]
18pub(crate) fn trim(s: &str) -> &str {
19    s.trim_matches(is_whitespace)
20}
21#[inline]
22pub(crate) fn trim_start(s: &str) -> &str {
23    s.trim_start_matches(is_whitespace)
24}
25
26// TODO: https://stackoverflow.com/questions/4325363/converting-a-number-with-comma-as-decimal-point-to-float
27#[inline]
28pub(crate) fn comma_to_period(s: &str) -> Cow<'_, str> {
29    if s.as_bytes().contains(&b',') {
30        s.replace(',', ".").into()
31    } else {
32        s.into()
33    }
34}
35
36// -----------------------------------------------------------------------------
37// Parsing array
38
39/// Parses integer array "<int> <int> <int>...".
40pub(crate) fn parse_int_array<T>(text: &str) -> ParseIntArray<'_, T>
41where
42    T: int::Integer,
43{
44    ParseIntArray {
45        text,
46        _marker: PhantomData,
47    }
48}
49
50pub(crate) struct ParseIntArray<'a, T> {
51    text: &'a str,
52    _marker: PhantomData<fn() -> T>,
53}
54
55impl<T> Iterator for ParseIntArray<'_, T>
56where
57    T: int::Integer,
58{
59    type Item = io::Result<T>;
60
61    fn next(&mut self) -> Option<Self::Item> {
62        if self.text.is_empty() {
63            return None;
64        }
65        match int::parse_partial(self.text.as_bytes()) {
66            Some((value, n)) => {
67                self.text = trim_start(self.text.get(n..).unwrap_or_default());
68                Some(Ok(value))
69            }
70            None => Some(Err(format_err!("error while parsing an integer"))),
71        }
72    }
73}
74
75/*
76/// Parses float array "<float> <float> <float>..."
77pub(crate) fn parse_float_array<T>(text: &str) -> ParseFloatArray<'_, T>
78where
79    T: float::Float,
80{
81    ParseFloatArray {
82        text,
83        _marker: PhantomData,
84    }
85}
86
87pub(crate) struct ParseFloatArray<'a, T> {
88    text: &'a str,
89    _marker: PhantomData<fn() -> T>,
90}
91
92impl<T> Iterator for ParseFloatArray<'_, T>
93where
94    T: float::Float,
95{
96    type Item = io::Result<T>;
97
98    fn next(&mut self) -> Option<Self::Item> {
99        if self.text.is_empty() {
100            return None;
101        }
102        match float::parse_partial::<T>(self.text.as_bytes()) {
103            Some((value, n)) => {
104                self.text = trim_start(self.text.get(n..).unwrap_or_default());
105                Some(Ok(value))
106            }
107            None => Some(Err(format_err!("error while parsing a float"))),
108        }
109    }
110}
111*/
112
113/// Parses float array "<float> <float> <float>..."
114pub(crate) fn parse_float_array_exact<T>(text: &str, num: usize) -> ParseFloatArrayExact<'_, T>
115where
116    T: float::Float,
117{
118    ParseFloatArrayExact {
119        text,
120        num,
121        count: 0,
122        _marker: PhantomData,
123    }
124}
125
126pub(crate) struct ParseFloatArrayExact<'a, T> {
127    text: &'a str,
128    num: usize,
129    count: usize,
130    _marker: PhantomData<fn() -> T>,
131}
132
133impl<T> Iterator for ParseFloatArrayExact<'_, T>
134where
135    T: float::Float,
136{
137    type Item = io::Result<T>;
138
139    fn next(&mut self) -> Option<Self::Item> {
140        if self.count >= self.num {
141            if self.text.is_empty() {
142                return None;
143            }
144            return Some(Err(format_err!(
145                "unexpected text {:?} after {} floats",
146                self.text,
147                self.num
148            )));
149        }
150        match float::parse_partial::<T>(self.text.as_bytes()) {
151            Some((value, n)) => {
152                self.text = trim_start(self.text.get(n..).unwrap_or_default());
153                self.count += 1;
154                Some(Ok(value))
155            }
156            None => Some(Err(format_err!("error while parsing a float"))),
157        }
158    }
159}
160
161// -----------------------------------------------------------------------------
162// XmlNodeExt
163
164pub(crate) trait XmlNodeExt<'a, 'input> {
165    fn element_children(&self) -> ElementChildren<'a, 'input>;
166    fn child(&self, name: &str) -> Option<Node<'a, 'input>>;
167    fn required_attribute(&self, name: &str) -> io::Result<&'a str>;
168    fn parse_attribute<T>(&self, name: &str) -> io::Result<Option<T>>
169    where
170        T: FromStr,
171        T::Err: fmt::Display;
172    fn parse_required_attribute<T>(&self, name: &str) -> io::Result<T>
173    where
174        T: FromStr,
175        T::Err: fmt::Display;
176    fn trimmed_text(&self) -> &'a str;
177    fn node_location(&self) -> TextPos;
178    fn text_location(&self) -> TextPos;
179    fn attr_value_location(&self, name: &str) -> TextPos;
180}
181
182impl<'a, 'input> XmlNodeExt<'a, 'input> for Node<'a, 'input> {
183    fn element_children(&self) -> ElementChildren<'a, 'input> {
184        self.children()
185            .filter(|n| n.node_type() == NodeType::Element)
186    }
187
188    fn child(&self, name: &str) -> Option<Node<'a, 'input>> {
189        self.element_children()
190            .find(|n| n.tag_name().name() == name)
191    }
192
193    fn required_attribute(&self, name: &str) -> io::Result<&'a str> {
194        match self.attribute(name) {
195            Some(v) => Ok(v),
196            None => {
197                bail!(
198                    "expected {} attribute in <{}> element at {}",
199                    name,
200                    if self.is_element() {
201                        self.tag_name().name()
202                    } else {
203                        self.parent_element().unwrap().tag_name().name()
204                    },
205                    self.node_location(),
206                )
207            }
208        }
209    }
210
211    fn parse_attribute<T>(&self, name: &str) -> io::Result<Option<T>>
212    where
213        T: FromStr,
214        T::Err: fmt::Display,
215    {
216        match self.attribute(name) {
217            Some(v) => Ok(Some(v.parse::<T>().map_err(|e| {
218                format_err!(
219                    "{} in <{}> element at {}: {:?}",
220                    e,
221                    self.tag_name().name(),
222                    self.attr_value_location(name),
223                    v
224                )
225            })?)),
226            None => Ok(None),
227        }
228    }
229
230    fn parse_required_attribute<T>(&self, name: &str) -> io::Result<T>
231    where
232        T: FromStr,
233        T::Err: fmt::Display,
234    {
235        let v = self.required_attribute(name)?;
236        v.parse::<T>().map_err(|e| {
237            format_err!(
238                "{} in <{}> element at {}: {:?}",
239                e,
240                self.tag_name().name(),
241                self.attr_value_location(name),
242                v
243            )
244        })
245    }
246
247    fn trimmed_text(&self) -> &'a str {
248        trim(self.text().unwrap_or_default())
249    }
250
251    #[cold]
252    fn node_location(&self) -> TextPos {
253        let start = self.range().start;
254        self.document().text_pos_at(start)
255    }
256    #[cold]
257    fn text_location(&self) -> TextPos {
258        let mut start = self.range().start;
259        start += memchr_naive(b'>', self.document().input_text()[start..].as_bytes())
260            .map_or(0, |p| p + 1);
261        self.document().text_pos_at(start)
262    }
263    #[cold]
264    fn attr_value_location(&self, name: &str) -> TextPos {
265        let start = self.attribute_node(name).unwrap().range_value().start;
266        self.document().text_pos_at(start)
267    }
268}
269
270pub(crate) type ElementChildren<'a, 'input> =
271    iter::Filter<Children<'a, 'input>, fn(&Node<'a, 'input>) -> bool>;