Skip to main content

text_tags/
lib.rs

1/*
2 * Copyright (c) Peter Bjorklund. All rights reserved. https://github.com/swamp/swamp
3 * Licensed under the MIT License. See LICENSE in the project root for license information.
4 */
5
6use fixed32::Fp;
7use seq_map::SeqMap;
8
9#[derive(Clone, Debug, PartialEq)]
10pub enum Node {
11    Text(String),
12    Element(Element),
13}
14
15#[derive(Clone, Debug, Eq, PartialEq)]
16pub enum ArgValue {
17    Int(i64),
18    Fixed(Fp),
19    Bool(bool),
20    Hex(String),
21    Keyword(String),
22    Str(String),
23}
24
25#[derive(Clone, Debug, PartialEq)]
26pub struct Element {
27    pub name: String,
28    pub args: SeqMap<String, ArgValue>,
29    pub children: Vec<Node>,
30}
31
32#[derive(Debug)]
33pub struct ParseError {
34    pub message: String,
35    pub line: usize,
36    pub column: usize,
37}
38
39pub struct Parser<'a> {
40    input: &'a [u8],
41    pos: usize,
42    line: usize,
43    col: usize,
44    pub errors: Vec<ParseError>,
45}
46
47impl<'a> Parser<'a> {
48    pub fn new(input: &'a str) -> Self {
49        Self {
50            input: input.as_bytes(),
51            pos: 0,
52            line: 1,
53            col: 1,
54            errors: Vec::new(),
55        }
56    }
57
58    fn peek(&self) -> Option<u8> {
59        self.input.get(self.pos).copied()
60    }
61
62    fn next(&mut self) -> Option<u8> {
63        let b = self.peek()?;
64        self.pos += 1;
65        if b == b'\n' {
66            self.line += 1;
67            self.col = 1;
68        } else {
69            self.col += 1;
70        }
71        Some(b)
72    }
73
74    fn eof(&self) -> bool {
75        self.pos >= self.input.len()
76    }
77
78    fn starts_with(&self, pat: &[u8]) -> bool {
79        self.input
80            .get(self.pos..)
81            .map_or(false, |r| r.starts_with(pat))
82    }
83
84    fn consume_ws(&mut self) {
85        while let Some(b) = self.peek() {
86            if b.is_ascii_whitespace() {
87                self.next();
88            } else {
89                break;
90            }
91        }
92    }
93
94    pub fn parse(&mut self) -> Vec<Node> {
95        self.parse_nodes(None)
96    }
97
98    fn parse_nodes(&mut self, stop_tag: Option<&str>) -> Vec<Node> {
99        let mut nodes = Vec::new();
100        while !self.eof() {
101            if let Some(tag) = stop_tag {
102                if self.starts_with(format!("[/{tag}]").as_bytes()) {
103                    break;
104                }
105            }
106            if self.starts_with(b"[/") {
107                let (l, c) = (self.line, self.col);
108                self.errors.push(ParseError {
109                    message: "Unexpected closing tag".into(),
110                    line: l,
111                    column: c,
112                });
113                while let Some(b) = self.next() {
114                    if b == b']' {
115                        break;
116                    }
117                }
118                continue;
119            }
120
121            if self.peek() == Some(b'[') {
122                nodes.push(self.parse_element());
123            } else {
124                let txt = self.parse_text();
125                if !txt.is_empty() {
126                    nodes.push(Node::Text(txt));
127                }
128            }
129        }
130        nodes
131    }
132
133    fn parse_text(&mut self) -> String {
134        let mut out = String::new();
135        while let Some(b) = self.peek() {
136            // Stop on an unescaped '[' (start of a tag)
137            if b == b'[' {
138                break;
139            }
140
141            // Handle backslash-escaping of '[' or ']'
142            if b == b'\\' {
143                if let Some(next) = self.input.get(self.pos + 1) {
144                    if *next == b'[' || *next == b']' {
145                        self.next();
146                        let escaped = self.next().unwrap();
147                        out.push(escaped as char);
148                        continue;
149                    }
150                }
151            }
152
153            out.push(self.next().unwrap() as char);
154        }
155        out
156    }
157
158    fn parse_element(&mut self) -> Node {
159        let (l, c) = (self.line, self.col);
160        self.next(); // '['
161
162        // name
163        self.consume_ws();
164        let name_start = self.pos;
165        while let Some(b) = self.peek() {
166            if b.is_ascii_alphanumeric() {
167                self.next();
168            } else {
169                break;
170            }
171        }
172        let name = String::from_utf8_lossy(&self.input[name_start..self.pos]).to_string();
173        if name.is_empty() {
174            self.errors.push(ParseError {
175                message: "Missing tag name".into(),
176                line: l,
177                column: c,
178            });
179        }
180
181        // args
182        let mut args = SeqMap::new();
183        loop {
184            self.consume_ws();
185            match self.peek() {
186                Some(b'=') | Some(b']') | None => break,
187                Some(_) => {
188                    let val = self.parse_arg_value();
189                    self.consume_ws();
190                    if self.peek() == Some(b'=') {
191                        if let ArgValue::Keyword(k) = val.clone() {
192                            self.next();
193                            self.consume_ws();
194                            let v2 = self.parse_arg_value();
195                            let _ = args.insert(k, v2);
196                        } else {
197                            self.errors.push(ParseError {
198                                message: "Invalid arg name".into(),
199                                line: l,
200                                column: c,
201                            });
202                        }
203                    } else {
204                        let _ = args.insert("value".into(), val);
205                    }
206                }
207            }
208        }
209        if self.next() != Some(b']') {
210            self.errors.push(ParseError {
211                message: format!("Unterminated [{}] tag", name),
212                line: l,
213                column: c,
214            });
215        }
216
217        // children
218        let children = self.parse_nodes(Some(&name));
219
220        // skip closing
221        if self.starts_with(format!("[/{name}]").as_bytes()) {
222            for _ in 0..(name.len() + 3) {
223                self.next();
224            }
225        } else {
226            self.errors.push(ParseError {
227                message: format!("Missing closing tag [/{name}]"),
228                line: self.line,
229                column: self.col,
230            });
231        }
232
233        Node::Element(Element {
234            name,
235            args,
236            children,
237        })
238    }
239
240    fn parse_arg_value(&mut self) -> ArgValue {
241        self.consume_ws();
242
243        // quoted
244        if self.peek() == Some(b'"') {
245            self.next();
246            let start = self.pos;
247            while let Some(b) = self.peek() {
248                if b == b'"' {
249                    break;
250                }
251                self.next();
252            }
253            let s = String::from_utf8_lossy(&self.input[start..self.pos]).to_string();
254            self.next();
255            return ArgValue::Str(s);
256        }
257
258        // hex
259        if self.peek() == Some(b'#') {
260            let start = self.pos;
261            self.next();
262            while let Some(b) = self.peek() {
263                if (b as char).is_ascii_hexdigit() {
264                    self.next();
265                } else {
266                    break;
267                }
268            }
269            return ArgValue::Hex(
270                String::from_utf8_lossy(&self.input[start..self.pos]).to_string(),
271            );
272        }
273
274        // unquoted token
275        let start = self.pos;
276        while let Some(b) = self.peek() {
277            if b.is_ascii_whitespace() || b == b']' || b == b'=' {
278                break;
279            }
280            self.next();
281        }
282        let tok = String::from_utf8_lossy(&self.input[start..self.pos]).to_string();
283        if tok == "true" {
284            ArgValue::Bool(true)
285        } else if tok == "false" {
286            ArgValue::Bool(false)
287        } else if let Ok(i) = tok.parse::<i64>() {
288            ArgValue::Int(i)
289        } else if let Ok(f) = tok.parse::<f32>() {
290            let a: Fp = f.into();
291            ArgValue::Fixed(a)
292        } else {
293            ArgValue::Keyword(tok)
294        }
295    }
296}
297
298pub fn parse(input: &str) -> Vec<Node> {
299    Parser::new(input).parse()
300}