From 9f8d07a234d53cb6ff2c76e03345d8a418bfd48f Mon Sep 17 00:00:00 2001 From: Mykola Humanov Date: Fri, 8 Nov 2024 17:24:30 +0200 Subject: [PATCH 1/3] src/selection.rs: add `Selection::add_selection` and other add methods --- CHANGELOG.md | 1 + src/dom_tree.rs | 2 +- src/selection.rs | 111 +++++++++++++++++++++++++++++++++++ tests/node-traversal.rs | 19 +++--- tests/pseudo-classes.rs | 3 - tests/selection-traversal.rs | 46 +++++++++++++++ 6 files changed, 169 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3335eb..a30e120 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to the `dom_query` crate will be documented in this file. - Added `NodeRef::prepend_children` method, that inserts a child and its siblings at the beginning of the node content. - Added `NodeRef::prepend_html` method, that parses html string and inserts its parsed nodes at the beginning of the node content. - Added `Selection::prepend_html` method, which parses an HTML string and inserts its parsed nodes at the beginning of the content of all matched nodes. +- Added `Selection::add_selection`, `Selection:add_matcher`, `Selection::add` and `Selection::try_add` methods to extend current selection with other selections. ### Fixed - Fixed `Selection::append_selection` to work with selections with multiple nodes and selections from another tree. diff --git a/src/dom_tree.rs b/src/dom_tree.rs index a7637cc..8f0d697 100644 --- a/src/dom_tree.rs +++ b/src/dom_tree.rs @@ -10,7 +10,7 @@ use crate::node::{Element, NodeData, NodeId, NodeRef, TreeNode}; /// fixes node ids fn fix_node(n: &mut TreeNode, offset: usize) { - n.id = NodeId::new(n.id.value + offset); + n.id = NodeId::new(n.id.value + offset); n.parent = n.parent.map(|id| NodeId::new(id.value + offset)); n.prev_sibling = n.prev_sibling.map(|id| NodeId::new(id.value + offset)); n.next_sibling = n.next_sibling.map(|id| NodeId::new(id.value + offset)); diff --git a/src/selection.rs b/src/selection.rs index 3ad34a9..c7fe5ee 100644 --- a/src/selection.rs +++ b/src/selection.rs @@ -286,6 +286,102 @@ impl<'a> Selection<'a> { .collect(); Selection { nodes } } + + /// Adds nodes that match the given CSS selector to the current selection. + /// + /// # Panics + /// + /// If matcher contains invalid CSS selector it panics. + /// + /// # Arguments + /// + /// * `sel` - The CSS selector to match against. + /// + /// # Returns + /// + /// The new `Selection` containing the original nodes and the new nodes. + pub fn add(&self, sel: &str) -> Selection<'a> { + if self.is_empty() { + return self.clone(); + } + let matcher = Matcher::new(sel).expect("Invalid CSS selector"); + self.add_matcher(&matcher) + } + + /// Adds nodes that match the given CSS selector to the current selection. + /// + /// If matcher contains invalid CSS selector it returns `None`. + /// + /// # Arguments + /// + /// * `sel` - The CSS selector to match against. + /// + /// # Returns + /// + /// The new `Selection` containing the original nodes and the new nodes. + pub fn try_add(&self, sel: &str) -> Option { + if self.is_empty() { + return Some(self.clone()); + } + Matcher::new(sel).ok().map(|m| self.add_matcher(&m)) + } + + /// Adds nodes that match the given matcher to the current selection. + /// + /// # Arguments + /// + /// * `matcher` - The matcher to match against. + /// + /// # Returns + /// + /// The new `Selection` containing the original nodes and the new nodes. + pub fn add_matcher(&self, matcher: &Matcher) -> Selection<'a> { + if self.is_empty() { + return self.clone(); + } + let root = self.nodes().first().unwrap().tree.root(); + let other_nodes: Vec = + Matches::from_one(root, matcher, MatchScope::IncludeNode).collect(); + let new_nodes = self.merge_nodes(other_nodes); + Selection { nodes: new_nodes } + } + + /// Adds a selection to the current selection. + /// + /// Behaves like `Union` for sets. + /// + /// # Arguments + /// + /// * `other` - The selection to add to the current selection. + /// + /// # Returns + /// + /// A new `Selection` object containing the combined elements. + pub fn add_selection(&self, other: &'a Selection) -> Selection<'a> { + if other.is_empty() { + return self.clone(); + } + + self.ensure_same_tree(other); + + let other_nodes = other.nodes(); + let new_nodes = self.merge_nodes(other_nodes.to_vec()); + + Selection { nodes: new_nodes } + } + + fn merge_nodes(&self, other_nodes: Vec>) -> Vec> { + let m: Vec = self.nodes().iter().map(|node| node.id.value).collect(); + let add_nodes: Vec = other_nodes + .iter() + .filter(|&node| !m.contains(&node.id.value)) + .cloned() + .collect(); + + let mut new_nodes = self.nodes().to_vec(); + new_nodes.extend(add_nodes); + new_nodes + } } //manipulating methods @@ -624,6 +720,21 @@ impl<'a> Selection<'a> { } } +impl<'a> Selection<'a> { + /// Ensures that the two selections are from the same tree. + /// + /// # Panics + /// + /// Panics if the selections are from different trees or if they are empty. + fn ensure_same_tree(&self, other: &Selection) { + let tree = self.nodes().first().unwrap().tree; + let other_tree = other.nodes().first().unwrap().tree; + if !std::ptr::eq(tree, other_tree) { + panic!("Selections must be from the same tree"); + } + } +} + /// Iterator over a collection of matched elements. pub struct Selections { iter: IntoIter, diff --git a/tests/node-traversal.rs b/tests/node-traversal.rs index e03bdb5..590d4e8 100644 --- a/tests/node-traversal.rs +++ b/tests/node-traversal.rs @@ -7,7 +7,6 @@ use wasm_bindgen_test::*; mod alloc; - #[cfg_attr(not(target_arch = "wasm32"), test)] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] fn test_first_element_child_edge_cases() { @@ -24,31 +23,33 @@ fn test_first_element_child_edge_cases() { "#; - + let doc: Document = html.into(); - + // Test empty parent let empty_sel = doc.select("#empty"); let empty = empty_sel.nodes().first().unwrap(); assert!(empty.first_element_child().is_none()); - + // Test text-only parent let text_only_sel = doc.select("#text-only"); let text_only = text_only_sel.nodes().first().unwrap(); assert!(text_only.first_element_child().is_none()); - - + // Test multiple children let multiple_sel = doc.select("#multiple"); let multiple = multiple_sel.nodes().first().unwrap(); let first = multiple.first_element_child().unwrap(); assert_eq!(first.text(), "First".into()); assert!(first.is_element()); - + // Test nested elements let nested_sel = doc.select("#nested"); let nested = nested_sel.nodes().first().unwrap(); let first_nested = nested.first_element_child().unwrap(); assert!(first_nested.is_element()); - assert_eq!(first_nested.first_element_child().unwrap().text(), "Nested".into()); -} \ No newline at end of file + assert_eq!( + first_nested.first_element_child().unwrap().text(), + "Nested".into() + ); +} diff --git a/tests/pseudo-classes.rs b/tests/pseudo-classes.rs index f982deb..f8f60d7 100644 --- a/tests/pseudo-classes.rs +++ b/tests/pseudo-classes.rs @@ -179,7 +179,6 @@ fn pseudo_class_only_text() { assert_eq!(sel.inner_html(), "Only text".into()); } - #[cfg_attr(not(target_arch = "wasm32"), test)] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] fn pseudo_class_not() { @@ -191,8 +190,6 @@ fn pseudo_class_not() { assert_eq!(text, "Three"); } - - #[cfg_attr(not(target_arch = "wasm32"), test)] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] fn test_is() { diff --git a/tests/selection-traversal.rs b/tests/selection-traversal.rs index 95c74e9..b1de084 100644 --- a/tests/selection-traversal.rs +++ b/tests/selection-traversal.rs @@ -386,3 +386,49 @@ fn test_ancestors_selection_with_limit() { assert!(ancestor_sel.is("#parent")); assert!(!ancestor_sel.is("#great-ancestor")); } + +#[cfg_attr(not(target_arch = "wasm32"), test)] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +fn test_selection_add_selection() { + let doc: Document = ANCESTORS_CONTENTS.into(); + + let first_sel = doc.select("#first-child"); + assert_eq!(first_sel.length(), 1); + let second_sel = doc.select("#second-child"); + assert_eq!(second_sel.length(), 1); + let children_sel = first_sel.add_selection(&second_sel); + assert_eq!(children_sel.length(), 2); +} + +#[cfg_attr(not(target_arch = "wasm32"), test)] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +fn test_selection_add() { + let doc: Document = ANCESTORS_CONTENTS.into(); + + let children_sel = doc.select("#first-child").add("#second-child"); + assert_eq!(children_sel.length(), 2); +} + +#[cfg_attr(not(target_arch = "wasm32"), test)] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +#[should_panic] +fn test_selection_add_panic() { + let doc: Document = ANCESTORS_CONTENTS.into(); + + doc.select("#first-child").add(":;'"); +} + +#[cfg_attr(not(target_arch = "wasm32"), test)] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +fn test_selection_try_add() { + let doc: Document = ANCESTORS_CONTENTS.into(); + + let first_child_sel = doc.select("#first-child"); + assert_eq!(first_child_sel.length(), 1); + + let children_sel = first_child_sel.try_add(":;'"); + assert!(children_sel.is_none()); + + let children_sel = first_child_sel.try_add("#second-child"); + assert_eq!(children_sel.unwrap().length(), 2); +} From 88dfbd7573df403f506a0bf028fb0ce6b8bc447c Mon Sep 17 00:00:00 2001 From: Mykola Humanov Date: Fri, 8 Nov 2024 17:44:05 +0200 Subject: [PATCH 2/3] src/selection.rs: `Selection:add_selection` adjustments --- CHANGELOG.md | 10 +++++----- src/selection.rs | 5 +++++ tests/selection-traversal.rs | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a30e120..fa18d05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,11 +11,11 @@ All notable changes to the `dom_query` crate will be documented in this file. - `NodeRef::children_it` now require `rev` argument. Set `true` to iterate children in reverse order. ### Added -- Added `NodeRef::prepend_child` method, that inserts a child at the beginning of node content. -- Added `NodeRef::prepend_children` method, that inserts a child and its siblings at the beginning of the node content. -- Added `NodeRef::prepend_html` method, that parses html string and inserts its parsed nodes at the beginning of the node content. -- Added `Selection::prepend_html` method, which parses an HTML string and inserts its parsed nodes at the beginning of the content of all matched nodes. -- Added `Selection::add_selection`, `Selection:add_matcher`, `Selection::add` and `Selection::try_add` methods to extend current selection with other selections. +- Implemented `NodeRef::prepend_child` method, that inserts a child at the beginning of node content. +- Implemented `NodeRef::prepend_children` method, that inserts a child and its siblings at the beginning of the node content. +- Implemented `NodeRef::prepend_html` method, that parses html string and inserts its parsed nodes at the beginning of the node content. +- Implemented `Selection::prepend_html` method, which parses an HTML string and inserts its parsed nodes at the beginning of the content of all matched nodes. +- Implemented `Selection::add_selection`, `Selection:add_matcher`, `Selection::add` and `Selection::try_add` methods to extend current selection with other selections. ### Fixed - Fixed `Selection::append_selection` to work with selections with multiple nodes and selections from another tree. diff --git a/src/selection.rs b/src/selection.rs index c7fe5ee..72593c4 100644 --- a/src/selection.rs +++ b/src/selection.rs @@ -358,6 +358,11 @@ impl<'a> Selection<'a> { /// /// A new `Selection` object containing the combined elements. pub fn add_selection(&self, other: &'a Selection) -> Selection<'a> { + + if self.is_empty() { + return other.clone(); + } + if other.is_empty() { return self.clone(); } diff --git a/tests/selection-traversal.rs b/tests/selection-traversal.rs index b1de084..1667cb8 100644 --- a/tests/selection-traversal.rs +++ b/tests/selection-traversal.rs @@ -411,7 +411,7 @@ fn test_selection_add() { #[cfg_attr(not(target_arch = "wasm32"), test)] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] -#[should_panic] +#[should_panic(expected = "failed to parse selector")] fn test_selection_add_panic() { let doc: Document = ANCESTORS_CONTENTS.into(); From 12b2c13ad1826e4f989737b57c1c2e27eda1567a Mon Sep 17 00:00:00 2001 From: Mykola Humanov Date: Fri, 8 Nov 2024 17:57:33 +0200 Subject: [PATCH 3/3] tests/selection-traversal.rs: fix `test_selection_add_panic` --- CHANGELOG.md | 11 +++++------ tests/selection-traversal.rs | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa18d05..a7f869f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,12 +11,11 @@ All notable changes to the `dom_query` crate will be documented in this file. - `NodeRef::children_it` now require `rev` argument. Set `true` to iterate children in reverse order. ### Added -- Implemented `NodeRef::prepend_child` method, that inserts a child at the beginning of node content. -- Implemented `NodeRef::prepend_children` method, that inserts a child and its siblings at the beginning of the node content. -- Implemented `NodeRef::prepend_html` method, that parses html string and inserts its parsed nodes at the beginning of the node content. -- Implemented `Selection::prepend_html` method, which parses an HTML string and inserts its parsed nodes at the beginning of the content of all matched nodes. -- Implemented `Selection::add_selection`, `Selection:add_matcher`, `Selection::add` and `Selection::try_add` methods to extend current selection with other selections. - +- Introduced new `NodeRef::prepend_child` method, that inserts a child at the beginning of node content. +- Introduced new `NodeRef::prepend_children` method, that inserts a child and its siblings at the beginning of the node content. +- Introduced new `NodeRef::prepend_html` method, that parses html string and inserts its parsed nodes at the beginning of the node content. +- Introduced new `Selection::prepend_html` method, which parses an HTML string and inserts its parsed nodes at the beginning of the content of all matched nodes. +- Introduced new selection methods: `Selection::add_selection`, `Selection:add_matcher`, `Selection::add` and `Selection::try_add` to extend current selection with other selections. ### Fixed - Fixed `Selection::append_selection` to work with selections with multiple nodes and selections from another tree. - Fixed `Selection::replace_with_selection` to work with selections with multiple nodes and selections from another tree. diff --git a/tests/selection-traversal.rs b/tests/selection-traversal.rs index 1667cb8..b1de084 100644 --- a/tests/selection-traversal.rs +++ b/tests/selection-traversal.rs @@ -411,7 +411,7 @@ fn test_selection_add() { #[cfg_attr(not(target_arch = "wasm32"), test)] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] -#[should_panic(expected = "failed to parse selector")] +#[should_panic] fn test_selection_add_panic() { let doc: Document = ANCESTORS_CONTENTS.into();