Skip to content

Commit

Permalink
src/node/node_ref.rs: implement NodeRef::ancestors
Browse files Browse the repository at this point in the history
  • Loading branch information
niklak committed Oct 20, 2024
1 parent e1ab950 commit 7ccff4e
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 2 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

All notable changes to the `dom_query` crate will be documented in this file.

## [Unreleased]

### Added
- Added `Node::ancestors` method, that allows to get all or limited number of ancestors of a node.
- Added `From<Vec<NodeRef<'a, NodeData>>> for Selection<'a>`

## [0.6.0] - 2024-10-19

### Changed
Expand Down
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,67 @@ assert_eq!(
```
</details>


<details>
<summary><b>Selecting ancestors</b></summary>


```rust
use dom_query::Document;

let doc: Document = r#"<!DOCTYPE>
<html>
<head>Test</head>
<body>
<div id="great-ancestor">
<div id="grand-parent">
<div id="parent">
<div id="child">Child</div>
</div>
</div>
</div>
</body>
</html>
"#.into();

// selecting an element
let child_sel = doc.select("#child");
assert!(child_sel.exists());

let child_node = child_sel.nodes().first().unwrap();

// getting all ancestors
let ancestors = child_node.ancestors(None);

let ancestor_sel = Selection::from(ancestors);

// in this case ancestors includes all ancestral nodes including html

// the root html element is presented in the ancestor selection
assert!(ancestor_sel.is("html"));

// also the direct parent of our starting node is presented
assert!(ancestor_sel.is("#parent"));

// `Selection::is` matches only the current selection without descending down the tree,
// so it won't match the #child node.
assert!(!ancestor_sel.is("#child"));


// if you don't require all ancestors, you can specify a number of ancestors you need -- `max_limit`
let ancestors = child_node.ancestors(Some(2));
let ancestor_sel = Selection::from(ancestors);

// in this case ancestors includes only two ancestral nodes: #grand-parent and #parent
assert!(ancestor_sel.is("#grand-parent #parent"));

assert!(!ancestor_sel.is("#great-ancestor"));

```
</details>



<details>
<summary><b>Selecting with precompiled matchers (for reuse)</b></summary>

Expand Down
18 changes: 17 additions & 1 deletion src/dom_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use html5ever::LocalName;
use html5ever::{namespace_url, ns, QualName};

use crate::entities::NodeIdMap;
use crate::node::children_of;
use crate::node::{ancestors_of, children_of};
use crate::node::{Element, InnerNode, Node, NodeData, NodeId, NodeRef};

fn fix_id(id: Option<NodeId>, offset: usize) -> Option<NodeId> {
Expand Down Expand Up @@ -113,6 +113,22 @@ impl<T: Debug> Tree<T> {
self.get_unchecked(&NodeId::new(0))
}

/// Gets the ancestors nodes of a node by id.
///
/// # Arguments
/// * `id` - The id of the node.
/// * `max_depth` - The maximum depth of the ancestors. If `None`, or Some(0) the maximum depth is unlimited.
///
/// # Returns
/// `Vec<NodeRef<T>>` A vector of ancestors nodes.
pub fn ancestors_of(&self, id: &NodeId, max_depth: Option<usize>) -> Vec<NodeRef<T>> {
let nodes = self.nodes.borrow();
ancestors_of(&nodes, id, max_depth)
.into_iter()
.map(|id| NodeRef::new(id, self))
.collect()
}

/// Gets the children nodes of a node by id
pub fn children_of(&self, id: &NodeId) -> Vec<NodeRef<T>> {
let nodes = self.nodes.borrow();
Expand Down
24 changes: 24 additions & 0 deletions src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,27 @@ pub(crate) fn children_of<T>(nodes: &Ref<Vec<InnerNode<T>>>, id: &NodeId) -> Vec
}
children
}

pub(crate) fn ancestors_of<T>(nodes: &Ref<Vec<InnerNode<T>>>, id: &NodeId, max_depth: Option<usize>) -> Vec<NodeId> {

let max_depth= max_depth.unwrap_or(0);
let mut depth: usize = 0;

let mut ancestors = vec![];

if let Some(node) = nodes.get(id.value) {
let mut parent = node.parent;
while let Some(parent_id) = parent {
if max_depth > 0 && depth == max_depth {
break;
}

ancestors.push(parent_id);
depth += 1;
if let Some(node) = nodes.get(parent_id.value) {
parent = node.parent;
}
}
}
ancestors
}
10 changes: 10 additions & 0 deletions src/node/node_ref.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@ impl<'a, T: Debug> NodeRef<'a, T> {
self.tree.children_of(&self.id)
}

/// Returns ancestor nodes of the selected node.
///
/// # Arguments
/// * `max_depth` - The maximum depth of the ancestors. If `None`, or Some(0) the maximum depth is unlimited.
#[inline]
pub fn ancestors(&self, max_depth: Option<usize>) -> Vec<Self> {
self.tree.ancestors_of(&self.id, max_depth)
}


/// Returns the first child node of the selected node.
#[inline]
pub fn first_child(&self) -> Option<Self> {
Expand Down
9 changes: 8 additions & 1 deletion src/selection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ use html5ever::Attribute;
use tendril::StrTendril;

use crate::matcher::{MatchScope, Matcher, Matches};
use crate::node::Node;
use crate::Document;

use crate::node::{Node,NodeRef,NodeData};

/// Selection represents a collection of nodes matching some criteria. The
/// initial Selection object can be created by using [`crate::document::Document::select`], and then
/// manipulated using methods itself.
Expand All @@ -21,6 +22,12 @@ impl<'a> From<Node<'a>> for Selection<'a> {
}
}

impl<'a> From<Vec<NodeRef<'a, NodeData>>> for Selection<'a> {
fn from(nodes: Vec<NodeRef<'a, NodeData>>) -> Selection {
Self { nodes }
}
}

// property methods
impl<'a> Selection<'a> {
/// Gets the specified attribute's value for the first element in the
Expand Down
76 changes: 76 additions & 0 deletions tests/selection-traversal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use data::doc;
use data::doc_wiki;
use dom_query::Document;

use dom_query::Selection;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::*;

Expand Down Expand Up @@ -296,3 +297,78 @@ fn test_node_children_size() {

assert_eq!(node.children().len(), 1)
}

#[cfg_attr(not(target_arch = "wasm32"), test)]
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
fn test_all_ancestors() {
let doc: Document = r#"<!DOCTYPE>
<html>
<head>Test</head>
<body>
<div id="great-ancestor">
<div id="grand-parent">
<div id="parent">
<div id="child">Child</div>
</div>
</div>
</div>
</body>
</html>
"#.into();

let child_sel = doc.select("#child");
assert!(child_sel.exists());

let child_node = child_sel.nodes().first().unwrap();

let ancestors = child_node.ancestors(None);

let ancestor_sel = Selection::from(ancestors);

// ancestors includes all ancestral nodes including html

// the root html element is presented in the ancestor selection
assert!(ancestor_sel.is("html"));

// also the direct parent of our starting node is presented
assert!(ancestor_sel.is("#parent"));

// `Selection::is` matches only the current selection without descending down the tree,
// so it won't match the #child node.
assert!(!ancestor_sel.is("#child"));

}


#[cfg_attr(not(target_arch = "wasm32"), test)]
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
fn test_ancestors_with_limit() {
let doc: Document = r#"<!DOCTYPE>
<html>
<head>Test</head>
<body>
<div id="great-ancestor">
<div id="grand-parent">
<div id="parent">
<div id="child">Child</div>
</div>
</div>
</div>
</body>
</html>
"#.into();

let child_sel = doc.select("#child");
assert!(child_sel.exists());

let child_node = child_sel.nodes().first().unwrap();

let ancestors = child_node.ancestors(Some(2));
let ancestor_sel = Selection::from(ancestors);

// in this case ancestors includes only two ancestral nodes: #grand-parent and #parent
assert!(ancestor_sel.is("#grand-parent #parent"));

assert!(!ancestor_sel.is("#great-ancestor"));

}

0 comments on commit 7ccff4e

Please sign in to comment.