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