Initial Release
This commit is contained in:
1 parent
2e5d63cc35
commit
8749725b45
52 files changed
+19804
No files matched your search
@@ -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};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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("&"),
|
||||
'<' => buf.push_str("<"),
|
||||
'>' => buf.push_str(">"),
|
||||
_ => buf.push(c),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn escape_attr(s: &str, buf: &mut String) {
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'&' => buf.push_str("&"),
|
||||
'"' => buf.push_str("""),
|
||||
_ => 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 & World <3</p>");
|
||||
let p = tree.query_selector("p").unwrap().unwrap();
|
||||
let html = tree.outer_html(p);
|
||||
assert!(html.contains("&"));
|
||||
assert!(html.contains("<"));
|
||||
}
|
||||
|
||||
#[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>"));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user