Skip to main content

ktrs_cli/ktlint/baseline/
xml.rs

1//! The slice of a DOM parser (`DocumentBuilder.parse`) that baseline files need: elements and attributes.
2//! Well-formedness errors carry Xerces' message and `line:col` for the common cases only.
3
4pub struct Element {
5    pub name: String,
6    pub attributes: Vec<(String, String)>,
7    pub children: Vec<Element>,
8}
9
10impl Element {
11    /// `getAttribute(name)`: `""` when absent.
12    pub fn attribute(&self, name: &str) -> &str {
13        self.attributes.iter().find(|(n, _)| n == name).map_or("", |(_, v)| v.as_str())
14    }
15
16    /// `getElementsByTagName(name)` on this element: its descendants in document order.
17    pub fn elements_by_tag_name<'a>(&'a self, name: &str, found: &mut Vec<&'a Element>) {
18        for child in &self.children {
19            if child.name == name {
20                found.push(child);
21            }
22            child.elements_by_tag_name(name, found);
23        }
24    }
25}
26
27/// A `SAXParseException`: what Xerces' default error handler prints as `[Fatal Error] :line:col: message`.
28#[derive(Debug, PartialEq, Eq)]
29pub struct XmlError {
30    pub line: usize,
31    pub col: usize,
32    pub message: String,
33}
34
35struct Parser {
36    chars: Vec<char>,
37    pos: usize,
38    line: usize,
39    col: usize,
40}
41
42/// Parses `text` into its root element, wrapped in a document node (so the root itself is found by name).
43pub fn parse_document(text: &str) -> Result<Element, XmlError> {
44    let mut p = Parser { chars: text.trim_start_matches('\u{feff}').chars().collect(), pos: 0, line: 1, col: 1 };
45    p.skip_misc(true)?;
46    if p.peek().is_none() {
47        return Err(p.error("Premature end of file."));
48    }
49    let root = p.element()?;
50    p.skip_misc(false)?;
51    Ok(Element { name: "#document".to_owned(), attributes: Vec::new(), children: vec![root] })
52}
53
54impl Parser {
55    fn peek(&self) -> Option<char> {
56        self.chars.get(self.pos).copied()
57    }
58
59    fn starts_with(&self, s: &str) -> bool {
60        s.chars().enumerate().all(|(i, c)| self.chars.get(self.pos + i) == Some(&c))
61    }
62
63    fn bump(&mut self) -> Option<char> {
64        let c = self.peek()?;
65        self.pos += 1;
66        if c == '\n' {
67            self.line += 1;
68            self.col = 1;
69        } else {
70            self.col += 1;
71        }
72        Some(c)
73    }
74
75    fn error(&self, message: &str) -> XmlError {
76        XmlError { line: self.line, col: self.col, message: message.to_owned() }
77    }
78
79    fn eof(&self) -> XmlError {
80        self.error("XML document structures must start and end within the same entity.")
81    }
82
83    fn skip_until(&mut self, end: &str) -> Result<(), XmlError> {
84        while !self.starts_with(end) {
85            self.bump().ok_or_else(|| self.eof())?;
86        }
87        end.chars().for_each(|_| {
88            self.bump();
89        });
90        Ok(())
91    }
92
93    /// Whitespace, comments and processing instructions around the root element.
94    fn skip_misc(&mut self, prolog: bool) -> Result<(), XmlError> {
95        loop {
96            match self.peek() {
97                Some(c) if c.is_whitespace() => {
98                    self.bump();
99                }
100                Some('<') if self.starts_with("<?") => self.skip_until("?>")?,
101                Some('<') if self.starts_with("<!--") => self.skip_until("-->")?,
102                Some('<') if prolog && self.starts_with("<!DOCTYPE") => self.skip_until(">")?,
103                Some('<') if prolog => return Ok(()),
104                Some('<') => {
105                    self.bump();
106                    return Err(self.error("The markup in the document following the root element must be well-formed."));
107                }
108                Some(_) if prolog => return Err(self.error("Content is not allowed in prolog.")),
109                Some(_) => return Err(self.error("Content is not allowed in trailing section.")),
110                None => return Ok(()),
111            }
112        }
113    }
114
115    fn name(&mut self) -> String {
116        let mut name = String::new();
117        while let Some(c) = self.peek().filter(|&c| c.is_alphanumeric() || matches!(c, '_' | ':' | '-' | '.')) {
118            name.push(c);
119            self.bump();
120        }
121        name
122    }
123
124    fn skip_whitespace(&mut self) {
125        while self.peek().is_some_and(char::is_whitespace) {
126            self.bump();
127        }
128    }
129
130    fn element(&mut self) -> Result<Element, XmlError> {
131        self.bump();
132        let name = self.name();
133        let mut element = Element { name, attributes: Vec::new(), children: Vec::new() };
134        loop {
135            self.skip_whitespace();
136            match self.peek() {
137                None => return Err(self.eof()),
138                Some('/') if self.starts_with("/>") => {
139                    self.bump();
140                    self.bump();
141                    return Ok(element);
142                }
143                Some('>') => {
144                    self.bump();
145                    break;
146                }
147                Some(_) => {
148                    let attribute = self.attribute(&element.name)?;
149                    element.attributes.push(attribute);
150                }
151            }
152        }
153        loop {
154            match self.peek() {
155                None => return Err(self.eof()),
156                Some('<') if self.starts_with("</") => {
157                    self.bump();
158                    self.bump();
159                    let at_name = self.error("");
160                    let end = self.name();
161                    if end != element.name {
162                        return Err(XmlError { message: format!(
163                            "The element type \"{0}\" must be terminated by the matching end-tag \"</{0}>\".",
164                            element.name
165                        ), ..at_name });
166                    }
167                    self.skip_whitespace();
168                    if self.bump() != Some('>') {
169                        return Err(self.error(&format!("The end-tag for element type \"{end}\" must end with a '>' delimiter.")));
170                    }
171                    return Ok(element);
172                }
173                Some('<') if self.starts_with("<!--") => self.skip_until("-->")?,
174                Some('<') if self.starts_with("<![CDATA[") => self.skip_until("]]>")?,
175                Some('<') if self.starts_with("<?") => self.skip_until("?>")?,
176                Some('<') => element.children.push(self.element()?),
177                Some('&') => {
178                    self.reference()?;
179                }
180                Some(_) => {
181                    self.bump();
182                }
183            }
184        }
185    }
186
187    fn attribute(&mut self, element_name: &str) -> Result<(String, String), XmlError> {
188        let expected = || {
189            format!("Element type \"{element_name}\" must be followed by either attribute specifications, \">\" or \"/>\".")
190        };
191        let name = self.name();
192        if name.is_empty() {
193            return Err(self.error(&expected()));
194        }
195        self.skip_whitespace();
196        if self.bump() != Some('=') {
197            return Err(self.error(&format!(
198                "Attribute name \"{name}\" associated with an element type \"{element_name}\" must be followed by the ' = ' character."
199            )));
200        }
201        self.skip_whitespace();
202        let quote = match self.peek() {
203            Some(q @ ('"' | '\'')) => {
204                self.bump();
205                q
206            }
207            _ => {
208                return Err(self.error(&format!(
209                    "Open quote is expected for attribute \"{name}\" associated with an  element type  \"{element_name}\"."
210                )));
211            }
212        };
213        let mut value = String::new();
214        loop {
215            match self.peek() {
216                None => return Err(self.eof()),
217                Some(c) if c == quote => {
218                    self.bump();
219                    return Ok((name, value));
220                }
221                Some('<') => {
222                    return Err(self.error(&format!(
223                        "The value of attribute \"{name}\" associated with an element type \"{element_name}\" must not contain the '<' character."
224                    )));
225                }
226                Some('&') => value.push_str(&self.reference()?),
227                Some(c) => {
228                    self.bump();
229                    value.push(if matches!(c, '\n' | '\r' | '\t') { ' ' } else { c });
230                }
231            }
232        }
233    }
234
235    fn reference(&mut self) -> Result<String, XmlError> {
236        self.bump();
237        let mut name = String::new();
238        loop {
239            match self.bump() {
240                None => return Err(self.eof()),
241                Some(';') => break,
242                Some(c) => name.push(c),
243            }
244        }
245        let numeric = |digits: &str, radix| u32::from_str_radix(digits, radix).ok().and_then(char::from_u32);
246        let c = match name.as_str() {
247            "amp" => Some('&'),
248            "lt" => Some('<'),
249            "gt" => Some('>'),
250            "quot" => Some('"'),
251            "apos" => Some('\''),
252            n if n.starts_with("#x") => numeric(&n[2..], 16),
253            n if n.starts_with('#') => numeric(&n[1..], 10),
254            _ => None,
255        };
256        c.map(String::from)
257            .ok_or_else(|| self.error(&format!("The entity \"{name}\" was referenced, but not declared.")))
258    }
259}