Initial Release

This commit is contained in:
h4ckf0r0day committed 2026-04-13 12:30:28 +02:00
1 parent 2e5d63cc35
commit 8749725b45
52 files changed
+19804

No files matched your search

+10
View File
@@ -0,0 +1,10 @@
#[macro_use]
extern crate html5ever;
pub mod tree;
pub mod tree_sink;
pub mod selector;
pub mod serialize;
pub use tree::{Attribute, DomTree, Node, NodeData, NodeId};
pub use tree_sink::{parse_html, parse_fragment};
+641
View File
@@ -0,0 +1,641 @@
use cssparser::{CowRcStr, ToCss};
use html5ever::{LocalName, Namespace};
use precomputed_hash::PrecomputedHash;
use selectors::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint};
use selectors::bloom::BloomFilter;
use selectors::context::QuirksMode;
use selectors::matching::{
ElementSelectorFlags, MatchingContext, MatchingForInvalidation, MatchingMode,
NeedsSelectorFlags,
};
use selectors::parser::{self, ParseRelative, SelectorParseErrorKind};
use selectors::{Element, OpaqueElement, SelectorList};
use selectors::visitor::SelectorVisitor;
use crate::tree::{DomTree, NodeData, NodeId};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ObscuraSelector;
impl parser::SelectorImpl for ObscuraSelector {
type ExtraMatchingData<'a> = ();
type AttrValue = CssString;
type Identifier = CssString;
type LocalName = CssLocalName;
type NamespaceUrl = CssNamespace;
type NamespacePrefix = CssString;
type BorrowedLocalName = CssLocalName;
type BorrowedNamespaceUrl = CssNamespace;
type NonTSPseudoClass = PseudoClass;
type PseudoElement = PseudoElement;
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CssString(pub String);
impl<'a> From<&'a str> for CssString {
fn from(s: &'a str) -> Self {
CssString(s.to_string())
}
}
impl AsRef<str> for CssString {
fn as_ref(&self) -> &str {
&self.0
}
}
impl ToCss for CssString {
fn to_css<W: std::fmt::Write>(&self, dest: &mut W) -> std::fmt::Result {
cssparser::serialize_string(&self.0, dest)
}
}
impl PrecomputedHash for CssString {
fn precomputed_hash(&self) -> u32 {
let mut h: u32 = 5381;
for b in self.0.as_bytes() {
h = h.wrapping_mul(33).wrapping_add(*b as u32);
}
h
}
}
impl Default for CssString {
fn default() -> Self {
CssString(String::new())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CssLocalName(pub LocalName);
impl<'a> From<&'a str> for CssLocalName {
fn from(s: &'a str) -> Self {
CssLocalName(LocalName::from(s))
}
}
impl ToCss for CssLocalName {
fn to_css<W: std::fmt::Write>(&self, dest: &mut W) -> std::fmt::Result {
dest.write_str(&self.0)
}
}
impl PrecomputedHash for CssLocalName {
fn precomputed_hash(&self) -> u32 {
self.0.precomputed_hash()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
pub struct CssNamespace(pub Namespace);
impl PrecomputedHash for CssNamespace {
fn precomputed_hash(&self) -> u32 {
self.0.precomputed_hash()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PseudoClass {
Hover,
Active,
Focus,
Enabled,
Disabled,
Checked,
}
impl parser::NonTSPseudoClass for PseudoClass {
type Impl = ObscuraSelector;
fn is_active_or_hover(&self) -> bool {
matches!(self, PseudoClass::Hover | PseudoClass::Active)
}
fn is_user_action_state(&self) -> bool {
matches!(
self,
PseudoClass::Hover | PseudoClass::Active | PseudoClass::Focus
)
}
fn visit<V>(&self, _visitor: &mut V) -> bool
where
V: SelectorVisitor<Impl = Self::Impl>,
{
true
}
}
impl ToCss for PseudoClass {
fn to_css<W: std::fmt::Write>(&self, dest: &mut W) -> std::fmt::Result {
match self {
PseudoClass::Hover => dest.write_str(":hover"),
PseudoClass::Active => dest.write_str(":active"),
PseudoClass::Focus => dest.write_str(":focus"),
PseudoClass::Enabled => dest.write_str(":enabled"),
PseudoClass::Disabled => dest.write_str(":disabled"),
PseudoClass::Checked => dest.write_str(":checked"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PseudoElement {
Before,
After,
}
impl parser::PseudoElement for PseudoElement {
type Impl = ObscuraSelector;
}
impl ToCss for PseudoElement {
fn to_css<W: std::fmt::Write>(&self, dest: &mut W) -> std::fmt::Result {
match self {
PseudoElement::Before => dest.write_str("::before"),
PseudoElement::After => dest.write_str("::after"),
}
}
}
pub struct ObscuraSelectorParser;
impl<'i> parser::Parser<'i> for ObscuraSelectorParser {
type Impl = ObscuraSelector;
type Error = SelectorParseErrorKind<'i>;
fn parse_non_ts_pseudo_class(
&self,
_location: cssparser::SourceLocation,
name: CowRcStr<'i>,
) -> Result<PseudoClass, cssparser::ParseError<'i, Self::Error>> {
match name.as_ref() {
"hover" => Ok(PseudoClass::Hover),
"active" => Ok(PseudoClass::Active),
"focus" => Ok(PseudoClass::Focus),
"enabled" => Ok(PseudoClass::Enabled),
"disabled" => Ok(PseudoClass::Disabled),
"checked" => Ok(PseudoClass::Checked),
_ => Err(cssparser::ParseError {
kind: cssparser::ParseErrorKind::Custom(
SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name),
),
location: _location,
}),
}
}
}
#[derive(Clone, Copy)]
pub struct DomElement<'a> {
pub tree: &'a DomTree,
pub node_id: NodeId,
}
impl<'a> DomElement<'a> {
pub fn new(tree: &'a DomTree, node_id: NodeId) -> Self {
DomElement { tree, node_id }
}
}
impl<'a> std::fmt::Debug for DomElement<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "DomElement({:?})", self.node_id)
}
}
impl<'a> PartialEq for DomElement<'a> {
fn eq(&self, other: &Self) -> bool {
self.node_id == other.node_id
}
}
impl<'a> Eq for DomElement<'a> {}
impl<'a> Element for DomElement<'a> {
type Impl = ObscuraSelector;
fn opaque(&self) -> OpaqueElement {
OpaqueElement::new(self)
}
fn parent_element(&self) -> Option<Self> {
let node = self.tree.get_node(self.node_id)?;
let parent_id = node.parent?;
let parent = self.tree.get_node(parent_id)?;
if parent.is_element() {
Some(DomElement::new(self.tree, parent_id))
} else {
None
}
}
fn parent_node_is_shadow_root(&self) -> bool {
false
}
fn containing_shadow_host(&self) -> Option<Self> {
None
}
fn pseudo_element_originating_element(&self) -> Option<Self> {
None
}
fn is_pseudo_element(&self) -> bool {
false
}
fn prev_sibling_element(&self) -> Option<Self> {
let node = self.tree.get_node(self.node_id)?;
let mut current = node.prev_sibling;
while let Some(sibling_id) = current {
let sibling = self.tree.get_node(sibling_id)?;
if sibling.is_element() {
return Some(DomElement::new(self.tree, sibling_id));
}
current = sibling.prev_sibling;
}
None
}
fn next_sibling_element(&self) -> Option<Self> {
let node = self.tree.get_node(self.node_id)?;
let mut current = node.next_sibling;
while let Some(sibling_id) = current {
let sibling = self.tree.get_node(sibling_id)?;
if sibling.is_element() {
return Some(DomElement::new(self.tree, sibling_id));
}
current = sibling.next_sibling;
}
None
}
fn first_element_child(&self) -> Option<Self> {
let node = self.tree.get_node(self.node_id)?;
let mut current = node.first_child;
while let Some(child_id) = current {
let child = self.tree.get_node(child_id)?;
if child.is_element() {
return Some(DomElement::new(self.tree, child_id));
}
current = child.next_sibling;
}
None
}
fn is_html_element_in_html_document(&self) -> bool {
self.tree
.with_node(self.node_id, |n| {
n.as_element()
.map(|name| name.ns == ns!(html))
.unwrap_or(false)
})
.unwrap_or(false)
}
fn has_local_name(&self, local_name: &CssLocalName) -> bool {
self.tree
.with_node(self.node_id, |n| {
n.as_element()
.map(|name| name.local == local_name.0)
.unwrap_or(false)
})
.unwrap_or(false)
}
fn has_namespace(&self, ns: &CssNamespace) -> bool {
self.tree
.with_node(self.node_id, |n| {
n.as_element()
.map(|name| name.ns == ns.0)
.unwrap_or(false)
})
.unwrap_or(false)
}
fn is_same_type(&self, other: &Self) -> bool {
let self_name = self.tree.with_node(self.node_id, |n| {
n.as_element().map(|name| (name.local.clone(), name.ns.clone()))
}).flatten();
let other_name = self.tree.with_node(other.node_id, |n| {
n.as_element().map(|name| (name.local.clone(), name.ns.clone()))
}).flatten();
match (self_name, other_name) {
(Some((al, ans)), Some((bl, bns))) => al == bl && ans == bns,
_ => false,
}
}
fn attr_matches(
&self,
ns: &NamespaceConstraint<&CssNamespace>,
local_name: &CssLocalName,
operation: &AttrSelectorOperation<&CssString>,
) -> bool {
self.tree
.with_node(self.node_id, |node| {
let attrs = match node.attrs() {
Some(a) => a,
None => return false,
};
attrs.iter().any(|attr| {
let ns_match = match ns {
NamespaceConstraint::Any => true,
NamespaceConstraint::Specific(expected_ns) => attr.name.ns == expected_ns.0,
};
if !ns_match || attr.name.local != local_name.0 {
return false;
}
operation.eval_str(&attr.value)
})
})
.unwrap_or(false)
}
fn has_attr_in_no_namespace(&self, local_name: &CssLocalName) -> bool {
self.tree
.with_node(self.node_id, |node| {
node.attrs()
.map(|attrs| {
attrs.iter().any(|a| {
a.name.ns == html5ever::ns!()
&& a.name.local == local_name.0
})
})
.unwrap_or(false)
})
.unwrap_or(false)
}
fn match_non_ts_pseudo_class(
&self,
_pc: &PseudoClass,
_context: &mut MatchingContext<'_, Self::Impl>,
) -> bool {
false
}
fn match_pseudo_element(
&self,
_pe: &PseudoElement,
_context: &mut MatchingContext<'_, Self::Impl>,
) -> bool {
false
}
fn apply_selector_flags(&self, _flags: ElementSelectorFlags) {}
fn is_link(&self) -> bool {
self.tree
.with_node(self.node_id, |n| {
n.as_element()
.map(|name| {
matches!(name.local.as_ref(), "a" | "area" | "link")
&& n.get_attribute("href").is_some()
})
.unwrap_or(false)
})
.unwrap_or(false)
}
fn is_html_slot_element(&self) -> bool {
false
}
fn assigned_slot(&self) -> Option<Self> {
None
}
fn has_id(&self, id: &CssString, case_sensitivity: CaseSensitivity) -> bool {
self.tree
.with_node(self.node_id, |n| {
n.get_attribute("id")
.map(|value| case_sensitivity.eq(value.as_bytes(), id.0.as_bytes()))
.unwrap_or(false)
})
.unwrap_or(false)
}
fn has_class(&self, name: &CssString, case_sensitivity: CaseSensitivity) -> bool {
self.tree
.with_node(self.node_id, |n| {
n.get_attribute("class")
.map(|class_attr| {
class_attr
.split_whitespace()
.any(|c| case_sensitivity.eq(c.as_bytes(), name.0.as_bytes()))
})
.unwrap_or(false)
})
.unwrap_or(false)
}
fn has_custom_state(&self, _name: &CssString) -> bool {
false
}
fn imported_part(&self, _name: &CssString) -> Option<CssString> {
None
}
fn is_part(&self, _name: &CssString) -> bool {
false
}
fn is_empty(&self) -> bool {
self.tree
.with_node(self.node_id, |node| {
let mut child = node.first_child;
while let Some(child_id) = child {
if let Some(child_node) = self.tree.get_node(child_id) {
match &child_node.data {
NodeData::Element { .. } => return false,
NodeData::Text { contents } if !contents.is_empty() => return false,
_ => {}
}
child = child_node.next_sibling;
} else {
break;
}
}
true
})
.unwrap_or(true)
}
fn is_root(&self) -> bool {
self.tree
.with_node(self.node_id, |n| {
n.parent
.map(|parent_id| {
self.tree
.with_node(parent_id, |p| p.is_document())
.unwrap_or(false)
})
.unwrap_or(false)
})
.unwrap_or(false)
}
fn ignores_nth_child_selectors(&self) -> bool {
false
}
fn add_element_unique_hashes(&self, _filter: &mut BloomFilter) -> bool {
false
}
}
pub fn parse_selector(selector: &str) -> Result<SelectorList<ObscuraSelector>, String> {
let mut parser_input = cssparser::ParserInput::new(selector);
let mut parser = cssparser::Parser::new(&mut parser_input);
SelectorList::parse(&ObscuraSelectorParser, &mut parser, ParseRelative::No)
.map_err(|e| format!("Failed to parse selector '{}': {:?}", selector, e))
}
impl DomTree {
pub fn query_selector(&self, selector: &str) -> Result<Option<NodeId>, String> {
let selector_list = parse_selector(selector)?;
let mut caches = selectors::context::SelectorCaches::default();
let mut context = MatchingContext::new(
MatchingMode::Normal,
None,
&mut caches,
QuirksMode::NoQuirks,
NeedsSelectorFlags::No,
MatchingForInvalidation::No,
);
let doc = self.document();
for desc_id in self.descendants(doc) {
let is_element = self.with_node(desc_id, |n| n.is_element()).unwrap_or(false);
if is_element {
let element = DomElement::new(self, desc_id);
if selectors::matching::matches_selector_list(
&selector_list,
&element,
&mut context,
) {
return Ok(Some(desc_id));
}
}
}
Ok(None)
}
pub fn query_selector_all(&self, selector: &str) -> Result<Vec<NodeId>, String> {
let selector_list = parse_selector(selector)?;
let mut caches = selectors::context::SelectorCaches::default();
let mut context = MatchingContext::new(
MatchingMode::Normal,
None,
&mut caches,
QuirksMode::NoQuirks,
NeedsSelectorFlags::No,
MatchingForInvalidation::No,
);
let mut results = Vec::new();
let doc = self.document();
for desc_id in self.descendants(doc) {
let is_element = self.with_node(desc_id, |n| n.is_element()).unwrap_or(false);
if is_element {
let element = DomElement::new(self, desc_id);
if selectors::matching::matches_selector_list(
&selector_list,
&element,
&mut context,
) {
results.push(desc_id);
}
}
}
Ok(results)
}
}
#[cfg(test)]
mod tests {
use crate::tree_sink::parse_html;
#[test]
fn test_query_selector_tag() {
let tree = parse_html("<html><body><h1>Title</h1><p>Text</p></body></html>");
let result = tree.query_selector("h1").unwrap();
assert!(result.is_some());
let node = tree.get_node(result.unwrap()).unwrap();
assert_eq!(node.as_element().unwrap().local.as_ref(), "h1");
}
#[test]
fn test_query_selector_class() {
let tree =
parse_html(r#"<div class="foo bar">Content</div><div class="baz">Other</div>"#);
let result = tree.query_selector(".foo").unwrap();
assert!(result.is_some());
let node = tree.get_node(result.unwrap()).unwrap();
assert_eq!(node.get_attribute("class"), Some("foo bar"));
}
#[test]
fn test_query_selector_id() {
let tree = parse_html(r#"<div id="main">Content</div>"#);
let result = tree.query_selector("#main").unwrap();
assert!(result.is_some());
}
#[test]
fn test_query_selector_all() {
let tree = parse_html("<ul><li>1</li><li>2</li><li>3</li></ul>");
let results = tree.query_selector_all("li").unwrap();
assert_eq!(results.len(), 3);
}
#[test]
fn test_query_selector_descendant() {
let tree =
parse_html(r#"<div id="outer"><div id="inner"><span>Target</span></div></div>"#);
let result = tree.query_selector("#outer span").unwrap();
assert!(result.is_some());
let node = tree.get_node(result.unwrap()).unwrap();
assert_eq!(node.as_element().unwrap().local.as_ref(), "span");
}
#[test]
fn test_query_selector_attribute() {
let tree = parse_html(
r#"<input type="text" name="user"><input type="password" name="pass">"#,
);
let result = tree.query_selector(r#"input[type="password"]"#).unwrap();
assert!(result.is_some());
let node = tree.get_node(result.unwrap()).unwrap();
assert_eq!(node.get_attribute("name"), Some("pass"));
}
#[test]
fn test_query_selector_no_match() {
let tree = parse_html("<div>Hello</div>");
let result = tree.query_selector("span").unwrap();
assert!(result.is_none());
}
#[test]
fn test_query_selector_complex() {
let tree = parse_html(
r#"<div class="container">
<ul class="list">
<li class="item active">First</li>
<li class="item">Second</li>
<li class="item active">Third</li>
</ul>
</div>"#,
);
let results = tree.query_selector_all(".list .item.active").unwrap();
assert_eq!(results.len(), 2);
}
}
+178
View File
@@ -0,0 +1,178 @@
use crate::tree::{DomTree, NodeData, NodeId};
impl DomTree {
pub fn outer_html(&self, node_id: NodeId) -> String {
let mut buf = String::new();
self.serialize_node(node_id, true, &mut buf);
buf
}
pub fn inner_html(&self, node_id: NodeId) -> String {
let mut buf = String::new();
self.serialize_children(node_id, &mut buf);
buf
}
fn serialize_node(&self, node_id: NodeId, include_self: bool, buf: &mut String) {
let node = match self.get_node(node_id) {
Some(n) => n,
None => return,
};
match &node.data {
NodeData::Document => {
self.serialize_children(node_id, buf);
}
NodeData::Doctype { name, .. } => {
buf.push_str("<!DOCTYPE ");
buf.push_str(name);
buf.push('>');
}
NodeData::Element { name, attrs, .. } => {
let tag = name.local.as_ref();
if include_self {
buf.push('<');
buf.push_str(tag);
for attr in attrs {
buf.push(' ');
let attr_name = attr.name.local.as_ref();
buf.push_str(attr_name);
buf.push_str("=\"");
escape_attr(&attr.value, buf);
buf.push('"');
}
buf.push('>');
}
if !is_void_element(tag) {
self.serialize_children(node_id, buf);
if include_self {
buf.push_str("</");
buf.push_str(tag);
buf.push('>');
}
}
}
NodeData::Text { contents } => {
let parent_is_raw = node.parent
.and_then(|pid| {
self.with_node(pid, |p| {
p.as_element()
.map(|name| is_raw_text_element(name.local.as_ref()))
.unwrap_or(false)
})
})
.unwrap_or(false);
if parent_is_raw {
buf.push_str(contents);
} else {
escape_text(contents, buf);
}
}
NodeData::Comment { contents } => {
buf.push_str("<!--");
buf.push_str(contents);
buf.push_str("-->");
}
NodeData::ProcessingInstruction { target, data } => {
buf.push_str("<?");
buf.push_str(target);
buf.push(' ');
buf.push_str(data);
buf.push('>');
}
}
}
fn serialize_children(&self, node_id: NodeId, buf: &mut String) {
for child_id in self.children(node_id) {
self.serialize_node(child_id, true, buf);
}
}
}
fn escape_text(s: &str, buf: &mut String) {
for c in s.chars() {
match c {
'&' => buf.push_str("&amp;"),
'<' => buf.push_str("&lt;"),
'>' => buf.push_str("&gt;"),
_ => buf.push(c),
}
}
}
fn escape_attr(s: &str, buf: &mut String) {
for c in s.chars() {
match c {
'&' => buf.push_str("&amp;"),
'"' => buf.push_str("&quot;"),
_ => buf.push(c),
}
}
}
fn is_void_element(tag: &str) -> bool {
matches!(
tag,
"area" | "base" | "br" | "col" | "embed" | "hr" | "img" | "input" | "link" | "meta"
| "param" | "source" | "track" | "wbr"
)
}
fn is_raw_text_element(tag: &str) -> bool {
matches!(tag, "script" | "style" | "textarea" | "title")
}
#[cfg(test)]
mod tests {
use crate::tree_sink::parse_html;
#[test]
fn test_outer_html() {
let tree = parse_html(r#"<div id="test"><p>Hello</p></div>"#);
let div = tree.get_element_by_id("test").unwrap();
let html = tree.outer_html(div);
assert!(html.contains(r#"<div id="test">"#));
assert!(html.contains("<p>Hello</p>"));
assert!(html.contains("</div>"));
}
#[test]
fn test_inner_html() {
let tree = parse_html(r#"<div id="test"><p>Hello</p><p>World</p></div>"#);
let div = tree.get_element_by_id("test").unwrap();
let html = tree.inner_html(div);
assert!(html.contains("<p>Hello</p>"));
assert!(html.contains("<p>World</p>"));
assert!(!html.contains("<div"));
}
#[test]
fn test_serialize_attributes() {
let tree = parse_html(r#"<a href="https://example.com" class="link">Click</a>"#);
let a = tree.query_selector("a").unwrap().unwrap();
let html = tree.outer_html(a);
assert!(html.contains("href=\"https://example.com\""));
assert!(html.contains("class=\"link\""));
}
#[test]
fn test_serialize_special_chars() {
let tree = parse_html("<p>Hello &amp; World &lt;3</p>");
let p = tree.query_selector("p").unwrap().unwrap();
let html = tree.outer_html(p);
assert!(html.contains("&amp;"));
assert!(html.contains("&lt;"));
}
#[test]
fn test_void_elements() {
let tree = parse_html(r#"<img src="test.png"><br>"#);
let img = tree.query_selector("img").unwrap().unwrap();
let html = tree.outer_html(img);
assert!(html.contains("<img"));
assert!(!html.contains("</img>"));
}
}
+697
View File
@@ -0,0 +1,697 @@
use html5ever::{LocalName, Namespace, QualName};
use std::cell::RefCell;
use std::collections::HashMap;
use std::fmt;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct NodeId(pub(crate) u32);
impl NodeId {
pub fn new(val: u32) -> Self {
NodeId(val)
}
pub fn index(self) -> usize {
self.0 as usize
}
pub fn raw(self) -> u32 {
self.0
}
}
impl fmt::Display for NodeId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "NodeId({})", self.0)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Attribute {
pub name: QualName,
pub value: String,
}
#[derive(Clone, Debug)]
pub enum NodeData {
Document,
Doctype {
name: String,
public_id: String,
system_id: String,
},
Element {
name: QualName,
attrs: Vec<Attribute>,
template_contents: Option<NodeId>,
mathml_annotation_xml_integration_point: bool,
},
Text {
contents: String,
},
Comment {
contents: String,
},
ProcessingInstruction {
target: String,
data: String,
},
}
#[derive(Clone, Debug)]
pub struct Node {
pub id: NodeId,
pub parent: Option<NodeId>,
pub first_child: Option<NodeId>,
pub last_child: Option<NodeId>,
pub prev_sibling: Option<NodeId>,
pub next_sibling: Option<NodeId>,
pub data: NodeData,
}
impl Node {
pub fn is_document(&self) -> bool {
matches!(self.data, NodeData::Document)
}
pub fn is_element(&self) -> bool {
matches!(self.data, NodeData::Element { .. })
}
pub fn is_text(&self) -> bool {
matches!(self.data, NodeData::Text { .. })
}
pub fn as_element(&self) -> Option<&QualName> {
match &self.data {
NodeData::Element { name, .. } => Some(name),
_ => None,
}
}
pub fn attrs(&self) -> Option<&[Attribute]> {
match &self.data {
NodeData::Element { attrs, .. } => Some(attrs),
_ => None,
}
}
pub fn attrs_mut(&mut self) -> Option<&mut Vec<Attribute>> {
match &mut self.data {
NodeData::Element { attrs, .. } => Some(attrs),
_ => None,
}
}
pub fn get_attribute(&self, name: &str) -> Option<&str> {
self.attrs()?.iter().find_map(|a| {
if a.name.local.as_ref() == name {
Some(a.value.as_str())
} else {
None
}
})
}
pub fn set_attribute(&mut self, name: &str, value: String) {
if let NodeData::Element { attrs, .. } = &mut self.data {
if let Some(attr) = attrs.iter_mut().find(|a| a.name.local.as_ref() == name) {
attr.value = value;
} else {
attrs.push(Attribute {
name: QualName::new(None, Namespace::default(), LocalName::from(name)),
value,
});
}
}
}
pub fn text_content_of_text_node(&self) -> Option<&str> {
match &self.data {
NodeData::Text { contents } => Some(contents),
_ => None,
}
}
}
pub struct DomTree {
inner: RefCell<DomTreeInner>,
}
pub(crate) struct DomTreeInner {
pub(crate) nodes: Vec<Option<Node>>,
pub(crate) free_list: Vec<u32>,
pub(crate) document: NodeId,
pub(crate) id_index: HashMap<String, NodeId>,
}
impl DomTree {
pub fn new() -> Self {
let doc_node = Node {
id: NodeId(0),
parent: None,
first_child: None,
last_child: None,
prev_sibling: None,
next_sibling: None,
data: NodeData::Document,
};
DomTree {
inner: RefCell::new(DomTreeInner {
nodes: vec![Some(doc_node)],
free_list: Vec::new(),
document: NodeId(0),
id_index: HashMap::new(),
}),
}
}
pub fn document(&self) -> NodeId {
self.inner.borrow().document
}
pub(crate) fn borrow_inner(&self) -> std::cell::Ref<'_, DomTreeInner> {
self.inner.borrow()
}
pub fn new_node(&self, data: NodeData) -> NodeId {
let mut inner = self.inner.borrow_mut();
let id = if let Some(slot) = inner.free_list.pop() {
NodeId(slot)
} else {
let idx = inner.nodes.len() as u32;
inner.nodes.push(None);
NodeId(idx)
};
if let NodeData::Element { ref attrs, .. } = data {
if let Some(id_attr) = attrs.iter().find(|a| a.name.local.as_ref() == "id") {
inner.id_index.insert(id_attr.value.clone(), id);
}
}
inner.nodes[id.index()] = Some(Node {
id,
parent: None,
first_child: None,
last_child: None,
prev_sibling: None,
next_sibling: None,
data,
});
id
}
pub fn get_node(&self, id: NodeId) -> Option<Node> {
self.inner.borrow().nodes.get(id.index())?.clone()
}
pub fn with_node<F, R>(&self, id: NodeId, f: F) -> Option<R>
where
F: FnOnce(&Node) -> R,
{
let inner = self.inner.borrow();
inner.nodes.get(id.index())?.as_ref().map(f)
}
pub fn with_node_mut<F, R>(&self, id: NodeId, f: F) -> Option<R>
where
F: FnOnce(&mut Node) -> R,
{
let mut inner = self.inner.borrow_mut();
inner.nodes.get_mut(id.index())?.as_mut().map(f)
}
pub fn append_child(&self, parent_id: NodeId, child_id: NodeId) {
self.detach(child_id);
let mut inner = self.inner.borrow_mut();
let old_last = inner.nodes.get(parent_id.index())
.and_then(|n| n.as_ref())
.and_then(|n| n.last_child);
if let Some(Some(child)) = inner.nodes.get_mut(child_id.index()) {
child.parent = Some(parent_id);
child.prev_sibling = old_last;
child.next_sibling = None;
}
if let Some(old_last_id) = old_last {
if let Some(Some(old_last_node)) = inner.nodes.get_mut(old_last_id.index()) {
old_last_node.next_sibling = Some(child_id);
}
}
if let Some(Some(parent)) = inner.nodes.get_mut(parent_id.index()) {
if parent.first_child.is_none() {
parent.first_child = Some(child_id);
}
parent.last_child = Some(child_id);
}
}
pub fn insert_before(&self, existing_id: NodeId, new_sibling_id: NodeId) {
let (parent_id, prev_id) = {
let inner = self.inner.borrow();
let node = match inner.nodes.get(existing_id.index()).and_then(|n| n.as_ref()) {
Some(n) => n,
None => return,
};
match node.parent {
Some(p) => (p, node.prev_sibling),
None => return,
}
};
self.detach(new_sibling_id);
let mut inner = self.inner.borrow_mut();
if let Some(Some(node)) = inner.nodes.get_mut(new_sibling_id.index()) {
node.parent = Some(parent_id);
node.prev_sibling = prev_id;
node.next_sibling = Some(existing_id);
}
if let Some(Some(node)) = inner.nodes.get_mut(existing_id.index()) {
node.prev_sibling = Some(new_sibling_id);
}
if let Some(prev) = prev_id {
if let Some(Some(node)) = inner.nodes.get_mut(prev.index()) {
node.next_sibling = Some(new_sibling_id);
}
} else if let Some(Some(parent)) = inner.nodes.get_mut(parent_id.index()) {
parent.first_child = Some(new_sibling_id);
}
}
pub fn detach(&self, node_id: NodeId) {
let mut inner = self.inner.borrow_mut();
let (parent_id, prev_id, next_id) = match inner.nodes.get(node_id.index()).and_then(|n| n.as_ref()) {
Some(node) => (node.parent, node.prev_sibling, node.next_sibling),
None => return,
};
if let Some(prev) = prev_id {
if let Some(Some(node)) = inner.nodes.get_mut(prev.index()) {
node.next_sibling = next_id;
}
} else if let Some(parent_id) = parent_id {
if let Some(Some(parent)) = inner.nodes.get_mut(parent_id.index()) {
parent.first_child = next_id;
}
}
if let Some(next) = next_id {
if let Some(Some(node)) = inner.nodes.get_mut(next.index()) {
node.prev_sibling = prev_id;
}
} else if let Some(parent_id) = parent_id {
if let Some(Some(parent)) = inner.nodes.get_mut(parent_id.index()) {
parent.last_child = prev_id;
}
}
if let Some(Some(node)) = inner.nodes.get_mut(node_id.index()) {
node.parent = None;
node.prev_sibling = None;
node.next_sibling = None;
}
}
pub fn remove(&self, node_id: NodeId) {
self.detach(node_id);
let descendants = self.descendants(node_id);
let mut inner = self.inner.borrow_mut();
let mut ids_to_remove = Vec::new();
for &desc_id in &descendants {
if let Some(Some(node)) = inner.nodes.get(desc_id.index()) {
if let Some(id_val) = node.get_attribute("id") {
ids_to_remove.push(id_val.to_string());
}
}
}
if let Some(Some(node)) = inner.nodes.get(node_id.index()) {
if let Some(id_val) = node.get_attribute("id") {
ids_to_remove.push(id_val.to_string());
}
}
for id_str in ids_to_remove {
inner.id_index.remove(&id_str);
}
for desc_id in descendants {
inner.nodes[desc_id.index()] = None;
inner.free_list.push(desc_id.0);
}
inner.nodes[node_id.index()] = None;
inner.free_list.push(node_id.0);
}
pub fn children(&self, node_id: NodeId) -> Vec<NodeId> {
let inner = self.inner.borrow();
let mut result = Vec::new();
let mut current = inner.nodes.get(node_id.index())
.and_then(|n| n.as_ref())
.and_then(|n| n.first_child);
while let Some(child_id) = current {
result.push(child_id);
current = inner.nodes.get(child_id.index())
.and_then(|n| n.as_ref())
.and_then(|n| n.next_sibling);
}
result
}
pub fn descendants(&self, node_id: NodeId) -> Vec<NodeId> {
let inner = self.inner.borrow();
let mut result = Vec::new();
let mut stack = Vec::new();
let mut first = inner.nodes.get(node_id.index())
.and_then(|n| n.as_ref())
.and_then(|n| n.first_child);
let mut children_to_push = Vec::new();
while let Some(child_id) = first {
children_to_push.push(child_id);
first = inner.nodes.get(child_id.index())
.and_then(|n| n.as_ref())
.and_then(|n| n.next_sibling);
}
for child_id in children_to_push.into_iter().rev() {
stack.push(child_id);
}
while let Some(current) = stack.pop() {
result.push(current);
let mut child = inner.nodes.get(current.index())
.and_then(|n| n.as_ref())
.and_then(|n| n.first_child);
let mut children_to_push = Vec::new();
while let Some(child_id) = child {
children_to_push.push(child_id);
child = inner.nodes.get(child_id.index())
.and_then(|n| n.as_ref())
.and_then(|n| n.next_sibling);
}
for child_id in children_to_push.into_iter().rev() {
stack.push(child_id);
}
}
result
}
pub fn ancestors(&self, node_id: NodeId) -> Vec<NodeId> {
let inner = self.inner.borrow();
let mut result = Vec::new();
let mut current = inner.nodes.get(node_id.index())
.and_then(|n| n.as_ref())
.and_then(|n| n.parent);
while let Some(parent_id) = current {
result.push(parent_id);
current = inner.nodes.get(parent_id.index())
.and_then(|n| n.as_ref())
.and_then(|n| n.parent);
}
result
}
pub fn get_element_by_id(&self, id: &str) -> Option<NodeId> {
self.inner.borrow().id_index.get(id).copied()
}
pub fn text_content(&self, node_id: NodeId) -> String {
let inner = self.inner.borrow();
let mut result = String::new();
collect_text_inner(&inner, node_id, &mut result);
result
}
pub fn append_text(&self, parent_id: NodeId, text: &str) {
let last_child_is_text = {
let inner = self.inner.borrow();
inner.nodes.get(parent_id.index())
.and_then(|n| n.as_ref())
.and_then(|n| n.last_child)
.and_then(|lc| inner.nodes.get(lc.index()))
.and_then(|n| n.as_ref())
.map(|n| n.is_text())
.unwrap_or(false)
};
if last_child_is_text {
let last_child_id = {
let inner = self.inner.borrow();
inner.nodes.get(parent_id.index())
.and_then(|n| n.as_ref())
.and_then(|n| n.last_child)
.unwrap()
};
let mut inner = self.inner.borrow_mut();
if let Some(Some(node)) = inner.nodes.get_mut(last_child_id.index()) {
if let NodeData::Text { contents } = &mut node.data {
contents.push_str(text);
return;
}
}
}
let text_id = self.new_node(NodeData::Text {
contents: text.to_string(),
});
self.append_child(parent_id, text_id);
}
pub fn find_body_or_root(&self) -> NodeId {
let doc = self.document();
for child in self.children(doc) {
if let Some(n) = self.get_node(child) {
if n.as_element().map(|name| name.local.as_ref() == "html").unwrap_or(false) {
for html_child in self.children(child) {
if let Some(hc) = self.get_node(html_child) {
if hc.as_element().map(|name| name.local.as_ref() == "body").unwrap_or(false) {
return html_child;
}
}
}
return child;
}
}
}
doc
}
pub fn import_children_from(&self, parent_id: NodeId, source: &DomTree, source_node: NodeId) {
let source_children = source.children(source_node);
for source_child_id in source_children {
self.import_node_from(parent_id, source, source_child_id);
}
}
fn import_node_from(&self, parent_id: NodeId, source: &DomTree, source_node_id: NodeId) {
let node_data = {
let source_inner = source.inner.borrow();
match source_inner.nodes.get(source_node_id.index()) {
Some(Some(node)) => node.data.clone(),
_ => return,
}
};
let new_id = self.new_node(node_data);
self.append_child(parent_id, new_id);
let children = source.children(source_node_id);
for child_id in children {
self.import_node_from(new_id, source, child_id);
}
}
pub fn len(&self) -> usize {
self.inner.borrow().nodes.iter().filter(|n| n.is_some()).count()
}
pub fn is_empty(&self) -> bool {
self.len() <= 1
}
pub fn update_id_index(&self, node_id: NodeId, old_id: Option<&str>, new_id: Option<&str>) {
let mut inner = self.inner.borrow_mut();
if let Some(old) = old_id {
inner.id_index.remove(old);
}
if let Some(new) = new_id {
inner.id_index.insert(new.to_string(), node_id);
}
}
}
fn collect_text_inner(inner: &DomTreeInner, node_id: NodeId, buf: &mut String) {
if let Some(Some(node)) = inner.nodes.get(node_id.index()) {
match &node.data {
NodeData::Text { contents } => buf.push_str(contents),
_ => {
let mut child = node.first_child;
while let Some(child_id) = child {
collect_text_inner(inner, child_id, buf);
child = inner.nodes.get(child_id.index())
.and_then(|n| n.as_ref())
.and_then(|n| n.next_sibling);
}
}
}
}
}
impl Default for DomTree {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_tree_has_document() {
let tree = DomTree::new();
assert_eq!(tree.len(), 1);
let node = tree.get_node(tree.document()).unwrap();
assert!(node.is_document());
}
#[test]
fn test_append_child() {
let tree = DomTree::new();
let child = tree.new_node(NodeData::Text {
contents: "hello".into(),
});
let doc = tree.document();
tree.append_child(doc, child);
assert_eq!(tree.len(), 2);
let doc_node = tree.get_node(doc).unwrap();
assert_eq!(doc_node.first_child, Some(child));
assert_eq!(doc_node.last_child, Some(child));
let child_node = tree.get_node(child).unwrap();
assert_eq!(child_node.parent, Some(doc));
}
#[test]
fn test_multiple_children() {
let tree = DomTree::new();
let doc = tree.document();
let c1 = tree.new_node(NodeData::Text { contents: "a".into() });
let c2 = tree.new_node(NodeData::Text { contents: "b".into() });
let c3 = tree.new_node(NodeData::Text { contents: "c".into() });
tree.append_child(doc, c1);
tree.append_child(doc, c2);
tree.append_child(doc, c3);
assert_eq!(tree.children(doc), vec![c1, c2, c3]);
}
#[test]
fn test_detach() {
let tree = DomTree::new();
let doc = tree.document();
let c1 = tree.new_node(NodeData::Text { contents: "a".into() });
let c2 = tree.new_node(NodeData::Text { contents: "b".into() });
tree.append_child(doc, c1);
tree.append_child(doc, c2);
tree.detach(c1);
assert_eq!(tree.children(doc), vec![c2]);
}
#[test]
fn test_insert_before() {
let tree = DomTree::new();
let doc = tree.document();
let c1 = tree.new_node(NodeData::Text { contents: "a".into() });
let c2 = tree.new_node(NodeData::Text { contents: "b".into() });
let c3 = tree.new_node(NodeData::Text { contents: "c".into() });
tree.append_child(doc, c1);
tree.append_child(doc, c3);
tree.insert_before(c3, c2);
assert_eq!(tree.children(doc), vec![c1, c2, c3]);
}
#[test]
fn test_text_content() {
let tree = DomTree::new();
let doc = tree.document();
let div = tree.new_node(NodeData::Element {
name: QualName::new(None, ns!(html), local_name!("div")),
attrs: vec![],
template_contents: None,
mathml_annotation_xml_integration_point: false,
});
tree.append_child(doc, div);
let t1 = tree.new_node(NodeData::Text { contents: "Hello ".into() });
let t2 = tree.new_node(NodeData::Text { contents: "World".into() });
tree.append_child(div, t1);
tree.append_child(div, t2);
assert_eq!(tree.text_content(div), "Hello World");
}
#[test]
fn test_get_element_by_id() {
let tree = DomTree::new();
let doc = tree.document();
let div = tree.new_node(NodeData::Element {
name: QualName::new(None, ns!(html), local_name!("div")),
attrs: vec![Attribute {
name: QualName::new(None, Namespace::default(), LocalName::from("id")),
value: "main".into(),
}],
template_contents: None,
mathml_annotation_xml_integration_point: false,
});
tree.append_child(doc, div);
assert_eq!(tree.get_element_by_id("main"), Some(div));
assert_eq!(tree.get_element_by_id("nonexistent"), None);
}
#[test]
fn test_append_text_merges() {
let tree = DomTree::new();
let doc = tree.document();
tree.append_text(doc, "Hello ");
tree.append_text(doc, "World");
assert_eq!(tree.children(doc).len(), 1);
assert_eq!(tree.text_content(doc), "Hello World");
}
#[test]
fn test_remove_subtree() {
let tree = DomTree::new();
let doc = tree.document();
let div = tree.new_node(NodeData::Element {
name: QualName::new(None, ns!(html), local_name!("div")),
attrs: vec![],
template_contents: None,
mathml_annotation_xml_integration_point: false,
});
tree.append_child(doc, div);
let text = tree.new_node(NodeData::Text { contents: "hi".into() });
tree.append_child(div, text);
assert_eq!(tree.len(), 3);
tree.remove(div);
assert_eq!(tree.len(), 1);
}
}
+323
View File
@@ -0,0 +1,323 @@
use std::borrow::Cow;
use std::cell::Ref;
use std::fmt;
use html5ever::tendril::StrTendril;
use html5ever::tree_builder::{ElemName, ElementFlags, NodeOrText, QuirksMode, TreeSink};
use html5ever::{Attribute as HtmlAttribute, LocalName, Namespace, QualName};
use crate::tree::{Attribute, DomTree, NodeData, NodeId};
pub struct ObscuraElemName<'a> {
_ref: Ref<'a, ()>,
name: *const QualName,
}
impl<'a> fmt::Debug for ObscuraElemName<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = unsafe { &*self.name };
write!(f, "{:?}", name)
}
}
impl<'a> ElemName for ObscuraElemName<'a> {
fn ns(&self) -> &Namespace {
unsafe { &(*self.name).ns }
}
fn local_name(&self) -> &LocalName {
unsafe { &(*self.name).local }
}
}
impl TreeSink for DomTree {
type Handle = NodeId;
type Output = Self;
type ElemName<'a> = ObscuraElemName<'a>;
fn finish(self) -> Self::Output {
self
}
fn parse_error(&self, _msg: Cow<'static, str>) {}
fn get_document(&self) -> NodeId {
self.document()
}
fn elem_name<'a>(&'a self, target: &'a NodeId) -> ObscuraElemName<'a> {
let borrow = self.borrow_inner();
let node = borrow.nodes.get(target.index())
.and_then(|n| n.as_ref())
.expect("elem_name called on invalid node");
let name_ptr: *const QualName = match &node.data {
NodeData::Element { name, .. } => name as *const QualName,
_ => panic!("elem_name called on non-element"),
};
let ref_guard = Ref::map(borrow, |_| &());
ObscuraElemName {
_ref: ref_guard,
name: name_ptr,
}
}
fn create_element(
&self,
name: QualName,
attrs: Vec<HtmlAttribute>,
flags: ElementFlags,
) -> NodeId {
let converted_attrs: Vec<Attribute> = attrs
.into_iter()
.map(|a| Attribute {
name: a.name,
value: a.value.to_string(),
})
.collect();
let id = self.new_node(NodeData::Element {
name: name.clone(),
attrs: converted_attrs,
template_contents: None,
mathml_annotation_xml_integration_point: flags.mathml_annotation_xml_integration_point,
});
if flags.template {
let template_doc = self.new_node(NodeData::Document);
self.with_node_mut(id, |node| {
if let NodeData::Element { template_contents, .. } = &mut node.data {
*template_contents = Some(template_doc);
}
});
}
id
}
fn create_comment(&self, text: StrTendril) -> NodeId {
self.new_node(NodeData::Comment {
contents: text.to_string(),
})
}
fn create_pi(&self, target: StrTendril, data: StrTendril) -> NodeId {
self.new_node(NodeData::ProcessingInstruction {
target: target.to_string(),
data: data.to_string(),
})
}
fn append(&self, parent: &NodeId, child: NodeOrText<NodeId>) {
match child {
NodeOrText::AppendNode(node_id) => {
self.append_child(*parent, node_id);
}
NodeOrText::AppendText(text) => {
self.append_text(*parent, &text);
}
}
}
fn append_based_on_parent_node(
&self,
element: &NodeId,
prev_element: &NodeId,
child: NodeOrText<NodeId>,
) {
let has_parent = self.with_node(*element, |n| n.parent.is_some()).unwrap_or(false);
if has_parent {
self.append_before_sibling(element, child);
} else {
self.append(prev_element, child);
}
}
fn append_doctype_to_document(
&self,
name: StrTendril,
public_id: StrTendril,
system_id: StrTendril,
) {
let doctype = self.new_node(NodeData::Doctype {
name: name.to_string(),
public_id: public_id.to_string(),
system_id: system_id.to_string(),
});
let doc = self.document();
self.append_child(doc, doctype);
}
fn add_attrs_if_missing(&self, target: &NodeId, attrs: Vec<HtmlAttribute>) {
self.with_node_mut(*target, |node| {
if let NodeData::Element { attrs: existing, .. } = &mut node.data {
for attr in attrs {
let dominated = existing.iter().any(|a| a.name == attr.name);
if !dominated {
existing.push(Attribute {
name: attr.name,
value: attr.value.to_string(),
});
}
}
}
});
}
fn remove_from_parent(&self, target: &NodeId) {
self.detach(*target);
}
fn reparent_children(&self, node: &NodeId, new_parent: &NodeId) {
let children = self.children(*node);
for child_id in children {
self.append_child(*new_parent, child_id);
}
}
fn append_before_sibling(&self, sibling: &NodeId, child: NodeOrText<NodeId>) {
match child {
NodeOrText::AppendNode(node_id) => {
self.insert_before(*sibling, node_id);
}
NodeOrText::AppendText(text) => {
let prev_text_id = {
let node = self.get_node(*sibling);
node.and_then(|n| n.prev_sibling).and_then(|prev_id| {
let prev = self.get_node(prev_id);
prev.and_then(|p| if p.is_text() { Some(prev_id) } else { None })
})
};
if let Some(prev_text_id) = prev_text_id {
self.with_node_mut(prev_text_id, |node| {
if let NodeData::Text { contents } = &mut node.data {
contents.push_str(&text);
}
});
return;
}
let text_id = self.new_node(NodeData::Text {
contents: text.to_string(),
});
self.insert_before(*sibling, text_id);
}
}
}
fn get_template_contents(&self, target: &NodeId) -> NodeId {
self.with_node(*target, |n| match &n.data {
NodeData::Element { template_contents, .. } => *template_contents,
_ => None,
})
.flatten()
.expect("get_template_contents called on non-template element")
}
fn same_node(&self, x: &NodeId, y: &NodeId) -> bool {
x == y
}
fn set_quirks_mode(&self, _mode: QuirksMode) {
}
fn is_mathml_annotation_xml_integration_point(&self, target: &NodeId) -> bool {
self.with_node(*target, |n| match &n.data {
NodeData::Element { mathml_annotation_xml_integration_point, .. } => {
*mathml_annotation_xml_integration_point
}
_ => false,
})
.unwrap_or(false)
}
}
pub fn parse_html(html: &str) -> DomTree {
use html5ever::tendril::TendrilSink;
use html5ever::{parse_document, ParseOpts};
let tree = DomTree::new();
parse_document(tree, ParseOpts::default())
.from_utf8()
.one(html.as_bytes())
}
pub fn parse_fragment(html: &str) -> DomTree {
use html5ever::tendril::TendrilSink;
use html5ever::{parse_fragment, ParseOpts, QualName};
let context_name = QualName::new(None, ns!(html), local_name!("body"));
let tree = DomTree::new();
parse_fragment(tree, ParseOpts::default(), context_name, vec![])
.from_utf8()
.one(html.as_bytes())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_html() {
let tree = parse_html("<html><head></head><body><h1>Hello</h1></body></html>");
assert!(tree.len() > 3);
let text = tree.text_content(tree.document());
assert!(text.contains("Hello"));
}
#[test]
fn test_parse_with_attributes() {
let tree = parse_html(r#"<div id="main" class="container">Text</div>"#);
let main = tree.get_element_by_id("main");
assert!(main.is_some());
let node = tree.get_node(main.unwrap()).unwrap();
assert_eq!(node.get_attribute("class"), Some("container"));
}
#[test]
fn test_parse_nested_structure() {
let tree = parse_html(
r#"<html><body>
<div id="outer">
<p id="para">Hello <strong>World</strong></p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
</div>
</body></html>"#,
);
let outer = tree.get_element_by_id("outer").unwrap();
let text = tree.text_content(outer);
assert!(text.contains("Hello"));
assert!(text.contains("World"));
assert!(text.contains("Item 1"));
assert!(text.contains("Item 2"));
}
#[test]
fn test_parse_malformed_html() {
let tree = parse_html("<div><p>Unclosed paragraph<p>Another<div>Nested wrong</div>");
assert!(tree.len() > 3);
let text = tree.text_content(tree.document());
assert!(text.contains("Unclosed paragraph"));
assert!(text.contains("Another"));
}
#[test]
fn test_parse_doctype() {
let tree = parse_html("<!DOCTYPE html><html><body>Hello</body></html>");
let first_child = tree.children(tree.document())[0];
let node = tree.get_node(first_child).unwrap();
assert!(matches!(node.data, NodeData::Doctype { .. }));
}
#[test]
fn test_parse_fragment() {
let tree = parse_fragment("<p>Hello</p><p>World</p>");
let text = tree.text_content(tree.document());
assert!(text.contains("Hello"));
assert!(text.contains("World"));
}
}