Skip to main content

yog_formats/
yui.rs

1//! The `.yui` XML subset — what `yog build` and Yog-IDLE read screens,
2//! book pages and recipe layouts from: elements with attributes in source
3//! order and their lines, for messages.
4
5/// One XML element: name, attributes in source order, child elements.
6#[derive(Debug, Clone)]
7pub struct Element {
8    pub name: String,
9    pub attrs: Vec<(String, String)>,
10    pub children: Vec<Element>,
11    /// 1-based line of the start tag, for error messages.
12    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    /// The last [`Parser::line`] answer: (position, line there) — the
28    /// parser only moves forward, so lines are counted from there on.
29    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    /// Skips whitespace, comments, `<?...?>` and `<!DOCTYPE ...>`.
67    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    /// Steps over `token` if the rest starts with it.
100    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    /// `key="value"` (or single-quoted), unescaped.
147    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    /// The elements inside `<name>`, through its closing tag.
169    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 `&amp;`".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
233/// Parses an XML document (the subset `.yui` uses) into its root element.
234pub 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}