1#[derive(Debug, Clone)]
7pub struct Element {
8 pub name: String,
9 pub attrs: Vec<(String, String)>,
10 pub children: Vec<Element>,
11 pub line: usize,
13}
14
15impl Element {
16 pub fn attr(&self, key: &str) -> Option<&str> {
17 self.attrs
18 .iter()
19 .find(|(k, _)| k == key)
20 .map(|(_, v)| v.as_str())
21 }
22}
23
24struct Parser<'a> {
25 src: &'a str,
26 pos: usize,
27 line_at: std::cell::Cell<(usize, usize)>,
30}
31
32impl<'a> Parser<'a> {
33 fn new(src: &'a str) -> Self {
34 Parser {
35 src,
36 pos: 0,
37 line_at: std::cell::Cell::new((0, 1)),
38 }
39 }
40
41 fn line(&self) -> usize {
42 let (at, line) = self.line_at.get();
43 let (from, line) = if self.pos >= at { (at, line) } else { (0, 1) };
44 let line = line
45 + self.src.as_bytes()[from..self.pos]
46 .iter()
47 .filter(|&&b| b == b'\n')
48 .count();
49 self.line_at.set((self.pos, line));
50 line
51 }
52
53 fn err<T>(&self, msg: impl std::fmt::Display) -> Result<T, String> {
54 Err(format!("line {}: {msg}", self.line()))
55 }
56
57 fn rest(&self) -> &'a str {
58 &self.src[self.pos..]
59 }
60
61 fn skip_ws(&mut self) {
62 let trimmed = self.rest().trim_start();
63 self.pos = self.src.len() - trimmed.len();
64 }
65
66 fn skip_misc(&mut self) -> Result<(), String> {
68 loop {
69 self.skip_ws();
70 let rest = self.rest();
71 let end = if rest.starts_with("<!--") {
72 rest.find("-->").map(|i| i + 3)
73 } else if rest.starts_with("<?") {
74 rest.find("?>").map(|i| i + 2)
75 } else if rest.starts_with("<!") {
76 rest.find('>').map(|i| i + 1)
77 } else {
78 return Ok(());
79 };
80 match end {
81 Some(n) => self.pos += n,
82 None => return self.err("unterminated comment or declaration"),
83 }
84 }
85 }
86
87 fn name(&mut self) -> Result<String, String> {
88 let rest = self.rest();
89 let len = rest
90 .find(|c: char| !(c.is_alphanumeric() || matches!(c, '_' | '-' | ':' | '.')))
91 .unwrap_or(rest.len());
92 if len == 0 {
93 return self.err("expected a name");
94 }
95 self.pos += len;
96 Ok(rest[..len].to_string())
97 }
98
99 fn eat(&mut self, token: &str) -> bool {
101 let found = self.rest().starts_with(token);
102 if found {
103 self.pos += token.len();
104 }
105 found
106 }
107
108 fn element(&mut self) -> Result<Element, String> {
109 let line = self.line();
110 if !self.eat("<") {
111 return self.err("expected an element");
112 }
113 let name = self.name()?;
114 let mut attrs: Vec<(String, String)> = Vec::new();
115 loop {
116 self.skip_ws();
117 if self.eat("/>") {
118 return Ok(Element {
119 name,
120 attrs,
121 children: Vec::new(),
122 line,
123 });
124 }
125 if self.eat(">") {
126 break;
127 }
128 if self.rest().is_empty() {
129 return self.err(format!("unterminated <{name}>"));
130 }
131 let (key, value) = self.attribute()?;
132 if attrs.iter().any(|(k, _)| *k == key) {
133 return self.err(format!("<{name}>: duplicate attribute `{key}`"));
134 }
135 attrs.push((key, value));
136 }
137 let children = self.children(&name)?;
138 Ok(Element {
139 name,
140 attrs,
141 children,
142 line,
143 })
144 }
145
146 fn attribute(&mut self) -> Result<(String, String), String> {
148 let key = self.name()?;
149 self.skip_ws();
150 if !self.eat("=") {
151 return self.err(format!("expected `=` after attribute `{key}`"));
152 }
153 self.skip_ws();
154 let quote = match self.rest().chars().next() {
155 Some(q @ ('"' | '\'')) => q,
156 _ => return self.err(format!("attribute `{key}`: expected a quoted value")),
157 };
158 self.pos += quote.len_utf8();
159 let Some(end) = self.rest().find(quote) else {
160 return self.err(format!("attribute `{key}`: unterminated value"));
161 };
162 let value =
163 unescape(&self.rest()[..end]).map_err(|e| format!("line {}: {e}", self.line()))?;
164 self.pos += end + quote.len_utf8();
165 Ok((key, value))
166 }
167
168 fn children(&mut self, name: &str) -> Result<Vec<Element>, String> {
170 let mut children = Vec::new();
171 loop {
172 self.skip_misc()?;
173 if self.eat("</") {
174 let close = self.name()?;
175 if close != name {
176 return self.err(format!("</{close}> closes <{name}>"));
177 }
178 self.skip_ws();
179 if !self.eat(">") {
180 return self.err("expected `>`");
181 }
182 return Ok(children);
183 }
184 let rest = self.rest();
185 if rest.is_empty() {
186 return self.err(format!("<{name}> is never closed"));
187 }
188 if !rest.starts_with('<') {
189 return self.err(format!(
190 "<{name}>: text content isn't allowed — use a `text` attribute"
191 ));
192 }
193 children.push(self.element()?);
194 }
195 }
196}
197
198fn unescape(raw: &str) -> Result<String, String> {
199 let mut out = String::with_capacity(raw.len());
200 let mut rest = raw;
201 while let Some(i) = rest.find('&') {
202 out.push_str(&rest[..i]);
203 let Some(semi) = rest[i..].find(';') else {
204 return Err("`&` without `;` — write `&`".into());
205 };
206 let entity = &rest[i + 1..i + semi];
207 match entity {
208 "amp" => out.push('&'),
209 "lt" => out.push('<'),
210 "gt" => out.push('>'),
211 "quot" => out.push('"'),
212 "apos" => out.push('\''),
213 _ => {
214 let code = if let Some(hex) = entity.strip_prefix("#x") {
215 u32::from_str_radix(hex, 16).ok()
216 } else if let Some(dec) = entity.strip_prefix('#') {
217 dec.parse().ok()
218 } else {
219 None
220 };
221 match code.and_then(char::from_u32) {
222 Some(c) => out.push(c),
223 None => return Err(format!("unknown entity `&{entity};`")),
224 }
225 }
226 }
227 rest = &rest[i + semi + 1..];
228 }
229 out.push_str(rest);
230 Ok(out)
231}
232
233pub fn parse(src: &str) -> Result<Element, String> {
235 let src = src.strip_prefix('\u{feff}').unwrap_or(src);
236 let mut p = Parser::new(src);
237 p.skip_misc()?;
238 let root = p.element()?;
239 p.skip_misc()?;
240 if !p.rest().is_empty() {
241 return p.err("content after the root element");
242 }
243 Ok(root)
244}