Skip to main content

rusty_xml_parser/
html.rs

1//! HTML parser matching libxml2 `HTMLparser.c` (a separate grammar, not XML recovery).
2
3use rusty_xml_tree::{NodeId, NodeKind, XmlDoc};
4
5use crate::error::XmlError;
6use crate::parse::default_parse_options;
7
8/// libxml2 `htmlParserOption` bits we honour.
9pub const HTML_PARSE_NOIMPLIED: i32 = 1 << 13;
10pub const HTML_PARSE_NONET: i32 = 1 << 11;
11
12const VOID: &[&str] = &[
13    "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param",
14    "source", "track", "wbr",
15];
16
17fn is_void(name: &str) -> bool {
18    VOID.contains(&name)
19}
20
21/// `htmlReadMemory`.
22#[doc(alias = "htmlReadMemory")]
23pub fn html_read_memory(
24    buffer: &[u8],
25    url: Option<&str>,
26    encoding: Option<&str>,
27    options: i32,
28) -> Result<XmlDoc, XmlError> {
29    let (utf8, _) = crate::encoding::xml_convert_to_utf8(buffer, encoding)?;
30    html_parse_utf8(&utf8, url, options)
31}
32
33/// `htmlReadDoc`.
34#[doc(alias = "htmlReadDoc")]
35pub fn html_read_doc(
36    cur: &str,
37    url: Option<&str>,
38    encoding: Option<&str>,
39    options: i32,
40) -> Result<XmlDoc, XmlError> {
41    html_read_memory(cur.as_bytes(), url, encoding, options)
42}
43
44/// `htmlReadFile`.
45#[doc(alias = "htmlReadFile")]
46pub fn html_read_file(filename: &str, encoding: Option<&str>, options: i32) -> Result<XmlDoc, XmlError> {
47    let b = std::fs::read(filename).map_err(|e| XmlError::new(4, e.to_string(), 0, 0))?;
48    html_read_memory(&b, Some(filename), encoding, options)
49}
50
51fn html_parse_utf8(bytes: &[u8], _url: Option<&str>, options: i32) -> Result<XmlDoc, XmlError> {
52    let text = String::from_utf8_lossy(bytes);
53    let mut p = HtmlParser {
54        src: text.as_ref(),
55        pos: 0,
56        doc: XmlDoc::xml_new_doc(Some("1.0")),
57        stack: Vec::new(),
58        noimplied: (options & HTML_PARSE_NOIMPLIED) != 0,
59        html: None,
60        head: None,
61        body: None,
62    };
63    p.doc.encoding = Some("HTML".into());
64    p.parse()?;
65    while p.stack.len() > 1 {
66        p.stack.pop();
67    }
68    let _ = options | HTML_PARSE_NONET | default_parse_options();
69    Ok(p.doc)
70}
71
72struct HtmlParser<'a> {
73    src: &'a str,
74    pos: usize,
75    doc: XmlDoc,
76    stack: Vec<NodeId>,
77    noimplied: bool,
78    html: Option<NodeId>,
79    head: Option<NodeId>,
80    body: Option<NodeId>,
81}
82
83impl<'a> HtmlParser<'a> {
84    fn rest(&self) -> &'a str {
85        &self.src[self.pos..]
86    }
87    fn eof(&self) -> bool {
88        self.pos >= self.src.len()
89    }
90    fn bump(&mut self, n: usize) {
91        self.pos += n;
92    }
93    fn parent(&self) -> NodeId {
94        *self.stack.last().unwrap_or(&NodeId::DOCUMENT)
95    }
96    fn ensure_html(&mut self) -> NodeId {
97        if let Some(h) = self.html {
98            return h;
99        }
100        let html = self.doc.xml_new_node(None, "html");
101        self.doc.xml_doc_set_root_element(html);
102        self.html = Some(html);
103        html
104    }
105    fn ensure_head(&mut self) -> NodeId {
106        if let Some(h) = self.head {
107            return h;
108        }
109        let html = self.ensure_html();
110        let head = self.doc.xml_new_node(None, "head");
111        self.doc.xml_add_child(html, head);
112        self.head = Some(head);
113        head
114    }
115    fn ensure_body(&mut self) -> NodeId {
116        if let Some(b) = self.body {
117            return b;
118        }
119        let html = self.ensure_html();
120        let body = self.doc.xml_new_node(None, "body");
121        self.doc.xml_add_child(html, body);
122        self.body = Some(body);
123        body
124    }
125    fn ensure_html_body(&mut self) -> NodeId {
126        if self.noimplied {
127            return self.stack.last().copied().unwrap_or(NodeId::DOCUMENT);
128        }
129        self.ensure_body()
130    }
131    fn parse(&mut self) -> Result<(), XmlError> {
132        while !self.eof() {
133            if self.rest().starts_with("<!--") {
134                self.parse_comment()?;
135            } else if self.rest().starts_with("<!") {
136                self.skip_decl();
137            } else if self.rest().starts_with("</") {
138                self.parse_end_tag();
139            } else if self.rest().starts_with('<') {
140                self.parse_start_tag()?;
141            } else {
142                self.parse_text();
143            }
144        }
145        Ok(())
146    }
147    fn parse_comment(&mut self) -> Result<(), XmlError> {
148        self.bump(4);
149        if let Some(end) = self.rest().find("-->") {
150            let body = self.rest()[..end].to_string();
151            self.bump(end + 3);
152            let n = self.doc.alloc(NodeKind::Comment, "#comment");
153            self.doc.node_mut(n).content = body;
154            self.doc.xml_add_child(self.parent(), n);
155        } else {
156            self.pos = self.src.len();
157        }
158        Ok(())
159    }
160    fn skip_decl(&mut self) {
161        if let Some(i) = self.rest().find('>') {
162            self.bump(i + 1);
163        } else {
164            self.pos = self.src.len();
165        }
166    }
167    fn parse_text(&mut self) {
168        let mut i = 0;
169        let r = self.rest();
170        for (off, c) in r.char_indices() {
171            if c == '<' {
172                i = off;
173                break;
174            }
175            i = off + c.len_utf8();
176        }
177        if i == 0 {
178            return;
179        }
180        let t = r[..i].to_string();
181        self.bump(i);
182        if t.chars().all(|c| c.is_whitespace()) && self.stack.is_empty() {
183            return;
184        }
185        let n = self.doc.alloc(NodeKind::Text, "#text");
186        self.doc.node_mut(n).content = t;
187        let parent = if self.stack.is_empty() {
188            self.ensure_html_body()
189        } else {
190            self.parent()
191        };
192        self.doc.xml_add_child(parent, n);
193    }
194    fn parse_start_tag(&mut self) -> Result<(), XmlError> {
195        self.bump(1);
196        let name = self.read_name().to_ascii_lowercase();
197        if name.is_empty() {
198            return Ok(());
199        }
200        let mut attrs: Vec<(String, String)> = Vec::new();
201        loop {
202            self.skip_ws();
203            if self.rest().starts_with('>') {
204                self.bump(1);
205                break;
206            }
207            if self.rest().starts_with("/>") {
208                self.bump(2);
209                break;
210            }
211            if self.eof() {
212                break;
213            }
214            let an = self.read_name().to_ascii_lowercase();
215            if an.is_empty() {
216                self.bump(1);
217                continue;
218            }
219            self.skip_ws();
220            let av = if self.rest().starts_with('=') {
221                self.bump(1);
222                self.skip_ws();
223                self.read_attr_value()
224            } else {
225                an.clone()
226            };
227            attrs.push((an, av));
228        }
229        // autoclose p/li when another p/li starts
230        if name == "p" || name == "li" || name == "tr" || name == "td" || name == "th" {
231            while let Some(&top) = self.stack.last() {
232                if self.doc.name(top) == name {
233                    self.stack.pop();
234                } else {
235                    break;
236                }
237            }
238        }
239        let parent = if self.noimplied {
240            self.stack.last().copied().unwrap_or(NodeId::DOCUMENT)
241        } else if name == "html" {
242            NodeId::DOCUMENT
243        } else if name == "head" {
244            self.ensure_html()
245        } else if name == "body" || name == "frameset" {
246            self.ensure_html()
247        } else if matches!(name.as_str(), "title" | "meta" | "link" | "style" | "base") {
248            self.ensure_head()
249        } else {
250            self.ensure_body()
251        };
252        let elem = self.doc.xml_new_node(None, &name);
253        for (k, v) in attrs {
254            self.doc.xml_set_prop(elem, &k, &v);
255        }
256        if name == "html" {
257            self.doc.xml_doc_set_root_element(elem);
258            self.html = Some(elem);
259        } else {
260            self.doc.xml_add_child(parent, elem);
261        }
262        if name == "head" {
263            self.head = Some(elem);
264        }
265        if name == "body" || name == "frameset" {
266            self.body = Some(elem);
267        }
268        if !is_void(&name) {
269            self.stack.push(elem);
270        }
271        Ok(())
272    }
273    fn parse_end_tag(&mut self) {
274        self.bump(2);
275        let name = self.read_name().to_ascii_lowercase();
276        self.skip_ws();
277        if self.rest().starts_with('>') {
278            self.bump(1);
279        }
280        if let Some(idx) = self.stack.iter().rposition(|&id| self.doc.name(id) == name) {
281            self.stack.truncate(idx);
282        }
283    }
284    fn skip_ws(&mut self) {
285        while let Some(c) = self.rest().chars().next() {
286            if c.is_whitespace() {
287                self.bump(c.len_utf8());
288            } else {
289                break;
290            }
291        }
292    }
293    fn read_name(&mut self) -> String {
294        let r = self.rest();
295        let mut n = 0;
296        for (i, c) in r.char_indices() {
297            if i == 0 {
298                if !(c.is_ascii_alphabetic() || c == '_' || c == ':') {
299                    return String::new();
300                }
301            } else if !(c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == ':' || c == '.') {
302                n = i;
303                break;
304            }
305            n = i + c.len_utf8();
306        }
307        let s = r[..n].to_string();
308        self.bump(n);
309        s
310    }
311    fn read_attr_value(&mut self) -> String {
312        let r = self.rest();
313        if r.starts_with('"') || r.starts_with('\'') {
314            let q = r.as_bytes()[0] as char;
315            self.bump(1);
316            if let Some(end) = self.rest().find(q) {
317                let v = self.rest()[..end].to_string();
318                self.bump(end + 1);
319                return v;
320            }
321        }
322        let mut n = 0;
323        for (i, c) in self.rest().char_indices() {
324            if c.is_whitespace() || c == '>' {
325                n = i;
326                break;
327            }
328            n = i + c.len_utf8();
329        }
330        let v = self.rest()[..n].to_string();
331        self.bump(n);
332        v
333    }
334}