Something went wrong. Try again.
we (web engine): Experimental web browser project to understand the limits of Claude
Something went wrong. Try again.
Rust
at url-parser
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160//! HTML tree builder: construct a DOM tree from tokenizer output.//!//! Implements a simplified subset of the WHATWG HTML5 tree construction//! algorithm for Phase 3 of the browser engine.
use we_dom::{Document, NodeId};
use crate::{Token, Tokenizer};
/// Insertion modes for the tree builder state machine.#[derive(Debug, Clone, Copy, PartialEq)]enum InsertionMode { Initial, BeforeHtml, BeforeHead, InHead, Text, AfterHead, InBody, AfterBody, AfterAfterBody,}
/// Returns true if the given tag name is a void element (self-closing, no end tag).fn is_void_element(tag: &str) -> bool { matches!( tag, "area" | "base" | "br" | "col" | "embed" | "hr" | "img" | "input" | "link" | "meta" | "param" | "source" | "track" | "wbr" )}
/// HTML tree builder that processes tokens and constructs a DOM tree.pub struct TreeBuilder { document: Document, /// Stack of open elements (the current nesting context). open_elements: Vec<NodeId>, head_element: Option<NodeId>, body_element: Option<NodeId>, insertion_mode: InsertionMode, /// Original insertion mode, saved when switching to Text mode. original_insertion_mode: Option<InsertionMode>, /// Pending text for the Text insertion mode (e.g., inside `<title>`). pending_text: String,}
impl TreeBuilder { /// Create a new tree builder with an empty document. pub fn new() -> Self { TreeBuilder { document: Document::new(), open_elements: Vec::new(), head_element: None, body_element: None, insertion_mode: InsertionMode::Initial, original_insertion_mode: None, pending_text: String::new(), } }
/// Process a single token, updating the DOM tree. pub fn process_token(&mut self, token: Token) { match self.insertion_mode { InsertionMode::Initial => self.handle_initial(token), InsertionMode::BeforeHtml => self.handle_before_html(token), InsertionMode::BeforeHead => self.handle_before_head(token), InsertionMode::InHead => self.handle_in_head(token), InsertionMode::Text => self.handle_text(token), InsertionMode::AfterHead => self.handle_after_head(token), InsertionMode::InBody => self.handle_in_body(token), InsertionMode::AfterBody => self.handle_after_body(token), InsertionMode::AfterAfterBody => self.handle_after_after_body(token), } }
/// Finish building and return the constructed DOM document. pub fn finish(self) -> Document { self.document }
// --- Insertion mode handlers ---
fn handle_initial(&mut self, token: Token) { match token { Token::Doctype { .. } => { // For Phase 3, we just acknowledge the DOCTYPE and move on. self.insertion_mode = InsertionMode::BeforeHtml; } Token::Comment(data) => { let comment = self.document.create_comment(&data); let root = self.document.root(); self.document.append_child(root, comment); } Token::Character(ref s) if s.chars().all(|c| c.is_ascii_whitespace()) => { // Ignore whitespace in Initial mode. } _ => { // Anything else: switch to BeforeHtml and reprocess. self.insertion_mode = InsertionMode::BeforeHtml; self.handle_before_html(token); } } }
fn handle_before_html(&mut self, token: Token) { match token { Token::Doctype { .. } => { /* ignore */ } Token::Comment(data) => { let comment = self.document.create_comment(&data); let root = self.document.root(); self.document.append_child(root, comment); } Token::Character(ref s) if s.chars().all(|c| c.is_ascii_whitespace()) => { // Ignore whitespace. } Token::StartTag { ref name, .. } if name == "html" => { let html = self.create_element_from_token(&token); let root = self.document.root(); self.document.append_child(root, html); self.open_elements.push(html); self.insertion_mode = InsertionMode::BeforeHead; } Token::EndTag { ref name } if name != "head" && name != "body" && name != "html" && name != "br" => { // Parse error, ignore. } _ => { // Create an implicit <html> element. let html = self.document.create_element("html"); let root = self.document.root(); self.document.append_child(root, html); self.open_elements.push(html); self.insertion_mode = InsertionMode::BeforeHead; self.handle_before_head(token); } } }
fn handle_before_head(&mut self, token: Token) { match token { Token::Character(ref s) if s.chars().all(|c| c.is_ascii_whitespace()) => { // Ignore whitespace. } Token::Comment(data) => { self.insert_comment(&data); } Token::Doctype { .. } => { /* ignore */ } Token::StartTag { ref name, .. } if name == "html" => { // Process as if InBody. self.handle_in_body(token); } Token::StartTag { ref name, .. } if name == "head" => { let head = self.create_element_from_token(&token); self.insert_node(head); self.open_elements.push(head); self.head_element = Some(head); self.insertion_mode = InsertionMode::InHead; } Token::EndTag { ref name } if name != "head" && name != "body" && name != "html" && name != "br" => { // Parse error, ignore. } _ => { // Implied <head>. let head = self.document.create_element("head"); self.insert_node(head); self.open_elements.push(head); self.head_element = Some(head); self.insertion_mode = InsertionMode::InHead; self.handle_in_head(token); } } }
fn handle_in_head(&mut self, token: Token) { match token { Token::Character(ref s) if s.chars().all(|c| c.is_ascii_whitespace()) => { self.insert_text(s); } Token::Comment(data) => { self.insert_comment(&data); } Token::Doctype { .. } => { /* ignore */ } Token::StartTag { ref name, .. } if name == "title" => { let elem = self.create_element_from_token(&token); self.insert_node(elem); self.open_elements.push(elem); self.original_insertion_mode = Some(self.insertion_mode); self.insertion_mode = InsertionMode::Text; } Token::StartTag { ref name, .. } if name == "style" || name == "script" || name == "noscript" => { let elem = self.create_element_from_token(&token); self.insert_node(elem); self.open_elements.push(elem); self.original_insertion_mode = Some(self.insertion_mode); self.insertion_mode = InsertionMode::Text; } Token::StartTag { ref name, .. } if name == "meta" || name == "link" => { let elem = self.create_element_from_token(&token); self.insert_node(elem); // Void elements: don't push onto stack. } Token::StartTag { ref name, .. } if name == "head" => { // Ignore duplicate <head>. } Token::EndTag { ref name } if name == "head" => { self.pop_until("head"); self.insertion_mode = InsertionMode::AfterHead; } Token::EndTag { ref name } if name != "body" && name != "html" && name != "br" => { // Parse error, ignore. } _ => { // Pop <head> and switch to AfterHead, then reprocess. self.pop_until("head"); self.insertion_mode = InsertionMode::AfterHead; self.handle_after_head(token); } } }
fn handle_text(&mut self, token: Token) { match token { Token::Character(s) => { self.pending_text.push_str(&s); } Token::EndTag { .. } => { // Flush pending text. if !self.pending_text.is_empty() { let text = self.pending_text.clone(); self.pending_text.clear(); self.insert_text(&text); } // Pop the element (e.g., <title>). self.open_elements.pop(); self.insertion_mode = self .original_insertion_mode .unwrap_or(InsertionMode::InBody); self.original_insertion_mode = None; } Token::Eof => { // Flush pending text. if !self.pending_text.is_empty() { let text = self.pending_text.clone(); self.pending_text.clear(); self.insert_text(&text); } self.open_elements.pop(); self.insertion_mode = self .original_insertion_mode .unwrap_or(InsertionMode::InBody); self.original_insertion_mode = None; self.process_token(Token::Eof); } _ => {} } }
fn handle_after_head(&mut self, token: Token) { match token { Token::Character(ref s) if s.chars().all(|c| c.is_ascii_whitespace()) => { self.insert_text(s); } Token::Comment(data) => { self.insert_comment(&data); } Token::Doctype { .. } => { /* ignore */ } Token::StartTag { ref name, .. } if name == "html" => { self.handle_in_body(token); } Token::StartTag { ref name, .. } if name == "body" => { let body = self.create_element_from_token(&token); self.insert_node(body); self.open_elements.push(body); self.body_element = Some(body); self.insertion_mode = InsertionMode::InBody; } Token::StartTag { ref name, .. } if name == "head" => { // Ignore. } Token::EndTag { ref name } if name != "body" && name != "html" && name != "br" => { // Ignore. } _ => { // Implied <body>. let body = self.document.create_element("body"); self.insert_node(body); self.open_elements.push(body); self.body_element = Some(body); self.insertion_mode = InsertionMode::InBody; self.handle_in_body(token); } } }
fn handle_in_body(&mut self, token: Token) { match token { Token::Character(s) => { self.insert_text(&s); } Token::Comment(data) => { self.insert_comment(&data); } Token::Doctype { .. } => { /* ignore */ } Token::StartTag { ref name, .. } if name == "html" => { // Merge attributes onto existing <html> element. if let Token::StartTag { attributes, .. } = &token { if let Some(&html_id) = self.open_elements.first() { for (attr_name, attr_value) in attributes { if self.document.get_attribute(html_id, attr_name).is_none() { self.document.set_attribute(html_id, attr_name, attr_value); } } } } } Token::StartTag { ref name, .. } if name == "body" || name == "head" || name == "title" || name == "style" || name == "script" => { match name.as_str() { "body" => { // Ignore duplicate <body>. } "head" => { // Ignore <head> in body. } _ => { // title/style/script: process using InHead rules self.handle_in_head(token); } } } Token::StartTag { ref name, .. } if name == "p" || name == "div" || name == "h1" || name == "h2" || name == "h3" || name == "h4" || name == "h5" || name == "h6" || name == "pre" || name == "blockquote" || name == "ul" || name == "ol" || name == "li" => { // If there's a <p> in button scope, close it first. if self.has_element_in_button_scope("p") { self.close_p_element(); } let elem = self.create_element_from_token(&token); self.insert_node(elem); self.open_elements.push(elem); } Token::StartTag { ref name, .. } if is_void_element(name) => { let elem = self.create_element_from_token(&token); self.insert_node(elem); // Don't push void elements onto the stack. } Token::StartTag { .. } => { // Generic start tag: create element and push onto stack. let elem = self.create_element_from_token(&token); self.insert_node(elem); self.open_elements.push(elem); } Token::EndTag { ref name } if name == "body" => { if self.has_element_in_scope("body") { self.insertion_mode = InsertionMode::AfterBody; } } Token::EndTag { ref name } if name == "html" => { if self.has_element_in_scope("body") { self.insertion_mode = InsertionMode::AfterBody; self.handle_after_body(token); } } Token::EndTag { ref name } if name == "p" => { if !self.has_element_in_button_scope("p") { // No matching <p>: insert an empty one, then close it. let p = self.document.create_element("p"); self.insert_node(p); self.open_elements.push(p); } self.close_p_element(); } Token::EndTag { ref name } if name == "div" || name == "pre" || name == "blockquote" || name == "ul" || name == "ol" || name == "li" => { if self.has_element_in_scope(name) { self.generate_implied_end_tags(Some(name)); self.pop_until(name); } } Token::EndTag { ref name } if name == "h1" || name == "h2" || name == "h3" || name == "h4" || name == "h5" || name == "h6" => { if self.has_heading_in_scope() { self.generate_implied_end_tags(None); // Pop until we find a heading element. while let Some(id) = self.open_elements.pop() { if let Some(tag) = self.document.tag_name(id) { if matches!(tag, "h1" | "h2" | "h3" | "h4" | "h5" | "h6") { break; } } } } } Token::EndTag { ref name } => { // Generic end tag: walk back through open elements. self.handle_any_other_end_tag(name); } Token::Eof => { // Stop parsing. } } }
fn handle_after_body(&mut self, token: Token) { match token { Token::Character(ref s) if s.chars().all(|c| c.is_ascii_whitespace()) => { // Process whitespace as in InBody. self.handle_in_body(token); } Token::Comment(data) => { // Insert as last child of the first element (html). let comment = self.document.create_comment(&data); if let Some(&html) = self.open_elements.first() { self.document.append_child(html, comment); } } Token::Doctype { .. } => { /* ignore */ } Token::EndTag { ref name } if name == "html" => { self.insertion_mode = InsertionMode::AfterAfterBody; } Token::Eof => { // Stop parsing. } _ => { // Anything else: switch back to InBody and reprocess. self.insertion_mode = InsertionMode::InBody; self.handle_in_body(token); } } }
fn handle_after_after_body(&mut self, token: Token) { match token { Token::Comment(data) => { let comment = self.document.create_comment(&data); let root = self.document.root(); self.document.append_child(root, comment); } Token::Doctype { .. } => { /* ignore */ } Token::Character(ref s) if s.chars().all(|c| c.is_ascii_whitespace()) => { self.handle_in_body(token); } Token::Eof => { // Stop. } _ => { self.insertion_mode = InsertionMode::InBody; self.handle_in_body(token); } } }
// --- Helper methods ---
/// Create a DOM element from a StartTag token, setting attributes. fn create_element_from_token(&mut self, token: &Token) -> NodeId { if let Token::StartTag { name, attributes, .. } = token { let id = self.document.create_element(name); for (attr_name, attr_value) in attributes { self.document.set_attribute(id, attr_name, attr_value); } id } else { // Should only be called with StartTag tokens. self.document.create_element("unknown") } }
/// Insert a node at the current insertion point (last open element). fn insert_node(&mut self, node: NodeId) { let parent = self .open_elements .last() .copied() .unwrap_or_else(|| self.document.root()); self.document.append_child(parent, node); }
/// Insert a text node at the current insertion point. /// If the last child is already a text node, append to it. fn insert_text(&mut self, data: &str) { let parent = self .open_elements .last() .copied() .unwrap_or_else(|| self.document.root());
// Try to merge with existing text node. if let Some(last_child) = self.document.last_child(parent) { if let we_dom::NodeData::Text { data: ref existing } = *self.document.node_data(last_child) { let mut merged = existing.clone(); merged.push_str(data); self.document.set_text_content(last_child, &merged); return; } }
let text = self.document.create_text(data); self.document.append_child(parent, text); }
/// Insert a comment node at the current insertion point. fn insert_comment(&mut self, data: &str) { let comment = self.document.create_comment(data); self.insert_node(comment); }
/// Pop elements from the stack until we find one with the given tag name. /// The matching element is also popped. fn pop_until(&mut self, tag_name: &str) { while let Some(id) = self.open_elements.pop() { if self.document.tag_name(id) == Some(tag_name) { return; } } }
/// Check if the given tag name is "in scope" (simplified). /// In scope means there's an element with that tag on the stack, /// and no scope barrier element between it and the top. fn has_element_in_scope(&self, target: &str) -> bool { for &id in self.open_elements.iter().rev() { if let Some(tag) = self.document.tag_name(id) { if tag == target { return true; } // Scope barrier elements. if matches!( tag, "applet" | "caption" | "html" | "table" | "td" | "th" | "marquee" | "object" | "template" ) { return false; } } } false }
/// Check if the given tag name is "in button scope". fn has_element_in_button_scope(&self, target: &str) -> bool { for &id in self.open_elements.iter().rev() { if let Some(tag) = self.document.tag_name(id) { if tag == target { return true; } // Button scope includes all regular scope barriers plus <button>. if matches!( tag, "applet" | "button" | "caption" | "html" | "table" | "td" | "th" | "marquee" | "object" | "template" ) { return false; } } } false }
/// Check if any heading element (h1-h6) is in scope. fn has_heading_in_scope(&self) -> bool { for &id in self.open_elements.iter().rev() { if let Some(tag) = self.document.tag_name(id) { if matches!(tag, "h1" | "h2" | "h3" | "h4" | "h5" | "h6") { return true; } if matches!( tag, "applet" | "caption" | "html" | "table" | "td" | "th" | "marquee" | "object" | "template" ) { return false; } } } false }
/// Close a `<p>` element: generate implied end tags (excluding p), /// then pop until we find the `<p>`. fn close_p_element(&mut self) { self.generate_implied_end_tags(Some("p")); self.pop_until("p"); }
/// Generate implied end tags. If `exclude` is provided, don't generate /// an end tag for that element. fn generate_implied_end_tags(&mut self, exclude: Option<&str>) { loop { let should_pop = self .open_elements .last() .and_then(|&id| self.document.tag_name(id)) .map(|tag| { if let Some(excl) = exclude { if tag == excl { return false; } } matches!( tag, "dd" | "dt" | "li" | "optgroup" | "option" | "p" | "rb" | "rp" | "rt" | "rtc" ) }) .unwrap_or(false); if should_pop { self.open_elements.pop(); } else { break; } } }
/// Handle a generic end tag by walking back through open elements /// using the "any other end tag" algorithm. fn handle_any_other_end_tag(&mut self, name: &str) { // Walk backwards through the stack. let mut i = self.open_elements.len(); while i > 0 { i -= 1; let id = self.open_elements[i]; if self.document.tag_name(id) == Some(name) { // Pop everything above and including this element. self.open_elements.truncate(i); return; } // If this is a "special" element, stop. if let Some(tag) = self.document.tag_name(id) { if is_special_element(tag) { return; } } } }}
impl Default for TreeBuilder { fn default() -> Self { Self::new() }}
/// Returns true if the tag is a "special" element per the HTML spec.fn is_special_element(tag: &str) -> bool { matches!( tag, "address" | "applet" | "area" | "article" | "aside" | "base" | "basefont" | "bgsound" | "blockquote" | "body" | "br" | "button" | "caption" | "center" | "col" | "colgroup" | "dd" | "details" | "dir" | "div" | "dl" | "dt" | "embed" | "fieldset" | "figcaption" | "figure" | "footer" | "form" | "frame" | "frameset" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "head" | "header" | "hgroup" | "hr" | "html" | "iframe" | "img" | "input" | "li" | "link" | "listing" | "main" | "marquee" | "menu" | "meta" | "nav" | "noembed" | "noframes" | "noscript" | "object" | "ol" | "p" | "param" | "plaintext" | "pre" | "script" | "section" | "select" | "source" | "style" | "summary" | "table" | "tbody" | "td" | "template" | "textarea" | "tfoot" | "th" | "thead" | "title" | "tr" | "track" | "ul" | "wbr" | "xmp" )}
/// Parse an HTML string into a DOM document.////// This is a convenience function that tokenizes the input and builds/// a DOM tree using the tree builder.pub fn parse_html(input: &str) -> Document { let mut builder = TreeBuilder::new(); let mut tokenizer = Tokenizer::new(input); loop { let token = tokenizer.next_token(); let is_eof = token == Token::Eof; builder.process_token(token); if is_eof { break; } } builder.finish()}
#[cfg(test)]mod tests { use super::*; use we_dom::NodeData;
/// Helper: collect tag names of direct children of a node. fn child_tags(doc: &Document, node: NodeId) -> Vec<String> { doc.children(node) .filter_map(|id| doc.tag_name(id).map(String::from)) .collect() }
/// Helper: get the text content of all text node children, concatenated. fn text_of_children(doc: &Document, node: NodeId) -> String { let mut result = String::new(); for child in doc.children(node) { if let Some(text) = doc.text_content(child) { result.push_str(text); } } result }
#[test] fn parse_full_document() { let doc = parse_html( "<!DOCTYPE html><html><head><title>Test</title></head><body><p>Hello</p></body></html>", ); let root = doc.root();
// Root should have one child: <html> let html_children: Vec<NodeId> = doc.children(root).collect(); assert_eq!(html_children.len(), 1); let html = html_children[0]; assert_eq!(doc.tag_name(html), Some("html"));
// <html> should have <head> and <body> let tags = child_tags(&doc, html); assert_eq!(tags, vec!["head", "body"]);
// <head> should have <title> let head = doc.children(html).next().unwrap(); let head_tags = child_tags(&doc, head); assert_eq!(head_tags, vec!["title"]);
// <title> should contain "Test" let title = doc.children(head).next().unwrap(); assert_eq!(text_of_children(&doc, title), "Test");
// <body> should have <p> let body = doc.children(html).nth(1).unwrap(); let body_tags = child_tags(&doc, body); assert_eq!(body_tags, vec!["p"]);
// <p> should contain "Hello" let p = doc.children(body).next().unwrap(); assert_eq!(text_of_children(&doc, p), "Hello"); }
#[test] fn implicit_html_head_body() { // Minimal document: just <p>Hello let doc = parse_html("<p>Hello"); let root = doc.root();
let html: Vec<NodeId> = doc.children(root).collect(); assert_eq!(html.len(), 1); assert_eq!(doc.tag_name(html[0]), Some("html"));
let html_tags = child_tags(&doc, html[0]); assert_eq!(html_tags, vec!["head", "body"]);
let body = doc.children(html[0]).nth(1).unwrap(); let body_tags = child_tags(&doc, body); assert_eq!(body_tags, vec!["p"]);
let p = doc.children(body).next().unwrap(); assert_eq!(text_of_children(&doc, p), "Hello"); }
#[test] fn void_element_br() { let doc = parse_html("<p>Line 1<br>Line 2</p>"); let root = doc.root(); let html = doc.children(root).next().unwrap(); let body = doc.children(html).nth(1).unwrap(); let p = doc.children(body).next().unwrap();
// <p> should have: text("Line 1"), <br>, text("Line 2") let children: Vec<NodeId> = doc.children(p).collect(); assert_eq!(children.len(), 3); assert_eq!(doc.text_content(children[0]), Some("Line 1")); assert_eq!(doc.tag_name(children[1]), Some("br")); assert_eq!(doc.text_content(children[2]), Some("Line 2")); }
#[test] fn p_inside_p_closes_outer() { let doc = parse_html("<p>First<p>Second"); let root = doc.root(); let html = doc.children(root).next().unwrap(); let body = doc.children(html).nth(1).unwrap();
// Should have two sibling <p> elements, not nested. let body_tags = child_tags(&doc, body); assert_eq!(body_tags, vec!["p", "p"]);
let children: Vec<NodeId> = doc.children(body).collect(); assert_eq!(text_of_children(&doc, children[0]), "First"); assert_eq!(text_of_children(&doc, children[1]), "Second"); }
#[test] fn nested_div_elements() { let doc = parse_html("<div><div>inner</div></div>"); let root = doc.root(); let html = doc.children(root).next().unwrap(); let body = doc.children(html).nth(1).unwrap();
let outer_div = doc.children(body).next().unwrap(); assert_eq!(doc.tag_name(outer_div), Some("div"));
let inner_div = doc.children(outer_div).next().unwrap(); assert_eq!(doc.tag_name(inner_div), Some("div")); assert_eq!(text_of_children(&doc, inner_div), "inner"); }
#[test] fn inline_elements_nest_properly() { let doc = parse_html("<p><span><a href=\"#\">link</a></span></p>"); let root = doc.root(); let html = doc.children(root).next().unwrap(); let body = doc.children(html).nth(1).unwrap();
let p = doc.children(body).next().unwrap(); let span = doc.children(p).next().unwrap(); assert_eq!(doc.tag_name(span), Some("span"));
let a = doc.children(span).next().unwrap(); assert_eq!(doc.tag_name(a), Some("a")); assert_eq!(doc.get_attribute(a, "href"), Some("#")); assert_eq!(text_of_children(&doc, a), "link"); }
#[test] fn headings() { let doc = parse_html("<h1>Title</h1><h2>Subtitle</h2><p>Body text</p>"); let root = doc.root(); let html = doc.children(root).next().unwrap(); let body = doc.children(html).nth(1).unwrap();
let tags = child_tags(&doc, body); assert_eq!(tags, vec!["h1", "h2", "p"]); }
#[test] fn comment_nodes() { let doc = parse_html("<body><!-- a comment --><p>text</p></body>"); let root = doc.root(); let html = doc.children(root).next().unwrap(); let body = doc.children(html).nth(1).unwrap();
let children: Vec<NodeId> = doc.children(body).collect(); assert!(children.len() >= 2);
// First child should be a comment. match doc.node_data(children[0]) { NodeData::Comment { data } => assert_eq!(data, " a comment "), other => panic!("expected comment, got {:?}", other), } }
#[test] fn pre_element() { let doc = parse_html("<pre>line 1\nline 2</pre>"); let root = doc.root(); let html = doc.children(root).next().unwrap(); let body = doc.children(html).nth(1).unwrap();
let pre = doc.children(body).next().unwrap(); assert_eq!(doc.tag_name(pre), Some("pre")); assert_eq!(text_of_children(&doc, pre), "line 1\nline 2"); }
#[test] fn attributes_preserved() { let doc = parse_html("<div id=\"main\" class=\"container\"><a href=\"/page\">link</a></div>"); let root = doc.root(); let html = doc.children(root).next().unwrap(); let body = doc.children(html).nth(1).unwrap();
let div = doc.children(body).next().unwrap(); assert_eq!(doc.get_attribute(div, "id"), Some("main")); assert_eq!(doc.get_attribute(div, "class"), Some("container"));
let a = doc.children(div).next().unwrap(); assert_eq!(doc.get_attribute(a, "href"), Some("/page")); }
#[test] fn empty_document() { let doc = parse_html(""); let root = doc.root(); // Even an empty doc should get html/head/body from EOF handling. // The tree builder creates implicit elements. assert!(doc.children(root).next().is_some()); }
#[test] fn just_text() { let doc = parse_html("Hello, world!"); let root = doc.root(); let html = doc.children(root).next().unwrap(); let body = doc.children(html).nth(1).unwrap();
assert_eq!(text_of_children(&doc, body), "Hello, world!"); }
#[test] fn heading_closes_open_p() { let doc = parse_html("<p>text<h1>heading</h1>"); let root = doc.root(); let html = doc.children(root).next().unwrap(); let body = doc.children(html).nth(1).unwrap();
// <p> should be closed by <h1>, so they're siblings. let tags = child_tags(&doc, body); assert_eq!(tags, vec!["p", "h1"]); }
#[test] fn self_closing_void_elements() { let doc = parse_html("<p>before<br/>after</p>"); let root = doc.root(); let html = doc.children(root).next().unwrap(); let body = doc.children(html).nth(1).unwrap(); let p = doc.children(body).next().unwrap();
let children: Vec<NodeId> = doc.children(p).collect(); assert_eq!(children.len(), 3); assert_eq!(doc.tag_name(children[1]), Some("br")); }
#[test] fn doctype_is_handled() { let doc = parse_html("<!DOCTYPE html><html><body></body></html>"); let root = doc.root(); let html = doc.children(root).next().unwrap(); assert_eq!(doc.tag_name(html), Some("html")); }
#[test] fn tree_builder_step_by_step() { let mut builder = TreeBuilder::new(); builder.process_token(Token::Doctype { name: Some("html".into()), public_id: None, system_id: None, force_quirks: false, }); builder.process_token(Token::StartTag { name: "html".into(), attributes: vec![], self_closing: false, }); builder.process_token(Token::StartTag { name: "head".into(), attributes: vec![], self_closing: false, }); builder.process_token(Token::EndTag { name: "head".into(), }); builder.process_token(Token::StartTag { name: "body".into(), attributes: vec![], self_closing: false, }); builder.process_token(Token::StartTag { name: "p".into(), attributes: vec![], self_closing: false, }); builder.process_token(Token::Character("Hello".into())); builder.process_token(Token::EndTag { name: "p".into() }); builder.process_token(Token::EndTag { name: "body".into(), }); builder.process_token(Token::EndTag { name: "html".into(), }); builder.process_token(Token::Eof);
let doc = builder.finish(); let root = doc.root(); let html = doc.children(root).next().unwrap(); assert_eq!(doc.tag_name(html), Some("html"));
let body = doc.children(html).nth(1).unwrap(); let p = doc.children(body).next().unwrap(); assert_eq!(text_of_children(&doc, p), "Hello"); }
#[test] fn multiple_text_children_merge() { // When consecutive character tokens arrive, they should merge. let mut builder = TreeBuilder::new(); builder.process_token(Token::StartTag { name: "p".into(), attributes: vec![], self_closing: false, }); builder.process_token(Token::Character("Hello ".into())); builder.process_token(Token::Character("world".into())); builder.process_token(Token::EndTag { name: "p".into() }); builder.process_token(Token::Eof);
let doc = builder.finish(); let root = doc.root(); let html = doc.children(root).next().unwrap(); let body = doc.children(html).nth(1).unwrap(); let p = doc.children(body).next().unwrap();
// Should be a single text node. let children: Vec<NodeId> = doc.children(p).collect(); assert_eq!(children.len(), 1); assert_eq!(doc.text_content(children[0]), Some("Hello world")); }}