Skip to content

Commit ef08b4d

Browse files
authored
Merge pull request #32 from niklak/feature/improve-selection-append-selection
improve `Selection::append_selection` and `Selection::replace_with_selection`
2 parents d62e9a7 + 64d6e55 commit ef08b4d

File tree

5 files changed

+152
-19
lines changed

5 files changed

+152
-19
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ All notable changes to the `dom_query` crate will be documented in this file.
99
- Using `Tree::merge_with_fn` instead of `Tree::merge` to reduce code duplication.
1010
- `Tree::child_ids_of_it` now require `rev` argument. Set `true` to iterate children in reverse order.
1111
- `NodeRef::children_it` now require `rev` argument. Set `true` to iterate children in reverse order.
12+
- Improved internal logic of `Selection::append_selection` and `Selection::replace_with_selection`.
1213

1314
### Added
1415
- Introduced new `NodeRef::prepend_child` method, that inserts a child at the beginning of node content.
1516
- Introduced new `NodeRef::prepend_children` method, that inserts a child and its siblings at the beginning of the node content.
1617
- Introduced new `NodeRef::prepend_html` method, that parses html string and inserts its parsed nodes at the beginning of the node content.
1718
- 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.
1819
- Introduced new selection methods: `Selection::add_selection`, `Selection:add_matcher`, `Selection::add` and `Selection::try_add` to extend current selection with other selections.
20+
1921
### Fixed
2022
- Fixed `Selection::append_selection` to work with selections with multiple nodes and selections from another tree.
2123
- Fixed `Selection::replace_with_selection` to work with selections with multiple nodes and selections from another tree.

src/dom_tree.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use html5ever::LocalName;
55
use html5ever::{namespace_url, ns, QualName};
66
use tendril::StrTendril;
77

8+
use crate::entities::InnerHashMap;
89
use crate::node::{ancestor_nodes, child_nodes, AncestorNodes, ChildNodes};
910
use crate::node::{Element, NodeData, NodeId, NodeRef, TreeNode};
1011

@@ -519,4 +520,91 @@ impl Tree {
519520
self.merge(other);
520521
f(new_node_id);
521522
}
523+
524+
///Adds a copy of the node and its children to the current tree
525+
///
526+
/// # Arguments
527+
///
528+
/// * `node` - reference to a node in the some tree
529+
///
530+
/// # Returns
531+
///
532+
/// * `NodeId` - id of the new node, that was added into the current tree
533+
pub(crate) fn copy_node(&self, node: &NodeRef) -> NodeId {
534+
let base_id = self.get_new_id();
535+
let mut next_id_val = base_id.value;
536+
537+
let mut id_map: InnerHashMap<usize, usize> = InnerHashMap::default();
538+
id_map.insert(node.id.value, next_id_val);
539+
540+
let mut ops = vec![node.clone()];
541+
542+
while let Some(op) = ops.pop() {
543+
for child in op.children_it(false) {
544+
next_id_val += 1;
545+
id_map.insert(child.id.value, next_id_val);
546+
}
547+
548+
ops.extend(op.children_it(true));
549+
}
550+
551+
// source tree may be the same tree that owns the copy_node fn, and may be not.
552+
let source_tree = node.tree;
553+
let new_nodes = self.copy_tree_nodes(source_tree, &id_map);
554+
555+
let mut nodes = self.nodes.borrow_mut();
556+
nodes.extend(new_nodes);
557+
558+
base_id
559+
}
560+
561+
562+
fn copy_tree_nodes(&self, source_tree: &Tree, id_map: &InnerHashMap<usize, usize>) -> Vec<TreeNode> {
563+
let mut new_nodes: Vec<TreeNode> = vec![];
564+
let source_nodes = source_tree.nodes.borrow();
565+
let tree_nodes_it = id_map.iter().flat_map(|(old_id, new_id)| {
566+
source_nodes.get(*old_id).map(|sn|
567+
TreeNode {
568+
id: NodeId::new(*new_id),
569+
parent: sn
570+
.parent
571+
.and_then(|old_id| id_map.get(&old_id.value).map(|id| NodeId::new(*id))),
572+
prev_sibling: sn
573+
.prev_sibling
574+
.and_then(|old_id| id_map.get(&old_id.value).map(|id| NodeId::new(*id))),
575+
next_sibling: sn
576+
.next_sibling
577+
.and_then(|old_id| id_map.get(&old_id.value).map(|id| NodeId::new(*id))),
578+
first_child: sn
579+
.first_child
580+
.and_then(|old_id| id_map.get(&old_id.value).map(|id| NodeId::new(*id))),
581+
last_child: sn
582+
.last_child
583+
.and_then(|old_id| id_map.get(&old_id.value).map(|id| NodeId::new(*id))),
584+
data: sn.data.clone(),
585+
}
586+
)
587+
588+
});
589+
new_nodes.extend(tree_nodes_it);
590+
new_nodes.sort_by_key(|k| k.id.value);
591+
new_nodes
592+
}
593+
594+
/// Copies nodes from another tree to the current tree and applies the given function
595+
/// to each copied node. The function is called with the ID of each copied node.
596+
///
597+
/// # Arguments
598+
///
599+
/// * `other_nodes` - slice of nodes to be copied
600+
/// * `f` - function to be applied to each copied node
601+
pub(crate) fn copy_nodes_with_fn<F>(&self, other_nodes: &[NodeRef], f: F)
602+
where
603+
F: Fn(NodeId),
604+
{
605+
for other_node in other_nodes {
606+
let new_node_id = self.copy_node(other_node);
607+
f(new_node_id);
608+
}
609+
}
522610
}

src/entities.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
#[cfg(feature = "hashbrown")]
22
mod inline {
3-
use hashbrown::HashSet;
3+
use hashbrown::{HashMap, HashSet};
44
pub type NodeIdSet = HashSet<crate::NodeId>;
55
pub type HashSetFx<K> = HashSet<K>;
6+
pub type InnerHashMap<K, V> = HashMap<K, V>;
67
}
78

89
#[cfg(not(feature = "hashbrown"))]
910
mod inline {
10-
use foldhash::HashSet;
11+
use foldhash::{HashMap, HashSet};
1112
pub type NodeIdSet = HashSet<crate::NodeId>;
1213
pub type HashSetFx<K> = HashSet<K>;
14+
pub type InnerHashMap<K, V> = HashMap<K, V>;
1315
}
1416

15-
pub(crate) use inline::{HashSetFx, NodeIdSet};
17+
pub(crate) use inline::{HashSetFx, InnerHashMap, NodeIdSet};

src/selection.rs

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,6 @@ impl<'a> Selection<'a> {
358358
///
359359
/// A new `Selection` object containing the combined elements.
360360
pub fn add_selection(&self, other: &'a Selection) -> Selection<'a> {
361-
362361
if self.is_empty() {
363362
return other.clone();
364363
}
@@ -435,17 +434,17 @@ impl<'a> Selection<'a> {
435434
/// This follows the same rules as `append`.
436435
///
437436
pub fn replace_with_selection(&self, sel: &Selection) {
438-
//! This is working solution, but it's not optimal yet!
439437
//! Note: goquery's behavior is taken as the basis.
438+
if sel.is_empty() {
439+
return;
440+
}
440441

441-
let mut contents: StrTendril = StrTendril::new();
442-
sel.iter().for_each(|s| contents.push_tendril(&s.html()));
443-
let fragment = Document::from(contents);
444442
sel.remove();
445443

444+
let sel_nodes = sel.nodes();
446445
for node in self.nodes() {
447-
node.tree.merge_with_fn(fragment.tree.clone(), |node_id| {
448-
node.append_prev_siblings(&node_id)
446+
node.tree.copy_nodes_with_fn(sel_nodes, |new_node_id| {
447+
node.append_prev_sibling(&new_node_id)
449448
});
450449
}
451450

@@ -455,18 +454,17 @@ impl<'a> Selection<'a> {
455454
/// Appends the elements in the selection to the end of each element
456455
/// in the set of matched elements.
457456
pub fn append_selection(&self, sel: &Selection) {
458-
//! This is working solution, but it's not optimal yet!
459457
//! Note: goquery's behavior is taken as the basis.
460458
461-
let mut contents: StrTendril = StrTendril::new();
462-
sel.iter().for_each(|s| contents.push_tendril(&s.html()));
463-
let fragment = Document::from(contents);
464-
sel.remove();
459+
if sel.is_empty() {
460+
return;
461+
}
465462

463+
sel.remove();
464+
let sel_nodes = sel.nodes();
466465
for node in self.nodes() {
467-
node.tree.merge_with_fn(fragment.tree.clone(), |node_id| {
468-
node.append_children(&node_id)
469-
});
466+
node.tree
467+
.copy_nodes_with_fn(sel_nodes, |new_node_id| node.append_children(&new_node_id));
470468
}
471469
}
472470

tests/selection-manipulation.rs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ fn test_append_selection_multiple() {
162162
let sel_dst = doc.select(".ad-content p");
163163
let sel_src = doc.select("span.source");
164164

165+
// sel_src will be detached from it's tree
165166
sel_dst.append_selection(&sel_src);
166167
assert_eq!(doc.select(".ad-content .source").length(), 2);
167168
assert_eq!(doc.select(".ad-content span").length(), 4)
@@ -187,7 +188,7 @@ fn test_replace_with_another_tree_selection() {
187188

188189
#[cfg_attr(not(target_arch = "wasm32"), test)]
189190
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
190-
fn test_append_tree_selection() {
191+
fn test_append_another_tree_selection() {
191192
let doc_dst = Document::from(REPLACEMENT_SEL_CONTENTS);
192193

193194
let contents_src = r#"
@@ -203,3 +204,45 @@ fn test_append_tree_selection() {
203204
assert_eq!(doc_dst.select(".ad-content .source").length(), 4);
204205
assert_eq!(doc_dst.select(".ad-content span").length(), 6)
205206
}
207+
208+
#[cfg_attr(not(target_arch = "wasm32"), test)]
209+
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
210+
fn test_append_another_tree_selection_empty() {
211+
let doc_dst = Document::from(REPLACEMENT_SEL_CONTENTS);
212+
213+
let contents_src = r#"
214+
<span class="source">example</span>
215+
<span class="source">example</span>"#;
216+
217+
let doc_src = Document::from(contents_src);
218+
219+
let sel_dst = doc_dst.select(".ad-content p");
220+
221+
// selecting non-existing elements
222+
let sel_src = doc_src.select("span.src");
223+
assert!(!sel_src.exists());
224+
225+
// sel_dst remained without changes
226+
sel_dst.append_selection(&sel_src);
227+
assert_eq!(doc_dst.select(".ad-content span").length(), 2)
228+
}
229+
230+
#[cfg_attr(not(target_arch = "wasm32"), test)]
231+
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
232+
fn test_replace_with_another_tree_selection_empty() {
233+
let doc_dst = Document::from(REPLACEMENT_SEL_CONTENTS);
234+
235+
let contents_src = r#"
236+
<span class="source">example</span>
237+
<span class="source">example</span>"#;
238+
239+
let doc_src = Document::from(contents_src);
240+
241+
let sel_dst = doc_dst.select(".ad-content p span");
242+
// selecting non-existing elements
243+
let sel_src = doc_src.select("span.src");
244+
assert!(!sel_src.exists());
245+
sel_dst.replace_with_selection(&sel_src);
246+
// sel_dst remained without changes
247+
assert_eq!(doc_dst.select(".ad-content span").length(), 2)
248+
}

0 commit comments

Comments
 (0)