1pub mod ast;
57pub mod bloom;
59pub mod matcher;
61pub mod parser;
63pub mod xpath;
65
66use std::cell::RefCell;
67use std::collections::{HashMap, VecDeque};
68use std::sync::Arc;
69
70use fhp_core::error::{SelectorError, XPathError};
71use fhp_core::tag::Tag;
72use fhp_tree::node::{NodeFlags, NodeId};
73use fhp_tree::{Document, NodeRef};
74
75use matcher::{select_all_list, select_first_list};
76use parser::parse_selector;
77use xpath::ast::XPathResult;
78
79#[inline]
80fn is_document_element(n: &fhp_tree::node::Node) -> bool {
81 n.depth > 0
82 && !n.flags.has(NodeFlags::IS_TEXT)
83 && !n.flags.has(NodeFlags::IS_COMMENT)
84 && !n.flags.has(NodeFlags::IS_DOCTYPE)
85}
86
87#[derive(Clone)]
105pub struct CompiledSelector {
106 list: Arc<ast::SelectorList>,
107}
108
109impl CompiledSelector {
110 pub fn new(css: &str) -> Result<Self, SelectorError> {
116 Ok(Self {
117 list: Arc::new(parse_selector(css)?),
118 })
119 }
120
121 pub fn as_list(&self) -> &ast::SelectorList {
123 &self.list
124 }
125}
126
127impl core::fmt::Debug for CompiledSelector {
128 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
129 f.debug_struct("CompiledSelector")
130 .field("selectors", &self.list.selectors.len())
131 .finish()
132 }
133}
134
135const SELECTOR_CACHE_CAPACITY: usize = 256;
137const MAX_CACHED_SELECTOR_LEN: usize = 512;
139
140struct SelectorCache {
141 map: HashMap<String, Arc<ast::SelectorList>>,
142 order: VecDeque<String>,
143}
144
145impl SelectorCache {
146 fn new() -> Self {
147 Self {
148 map: HashMap::with_capacity(SELECTOR_CACHE_CAPACITY),
149 order: VecDeque::with_capacity(SELECTOR_CACHE_CAPACITY),
150 }
151 }
152
153 fn get(&self, css: &str) -> Option<Arc<ast::SelectorList>> {
154 self.map.get(css).map(Arc::clone)
155 }
156
157 fn insert(&mut self, css: &str, list: Arc<ast::SelectorList>) {
158 if self.map.contains_key(css) {
159 self.map.insert(css.to_owned(), list);
160 return;
161 }
162
163 if self.map.len() >= SELECTOR_CACHE_CAPACITY {
164 if let Some(old_key) = self.order.pop_front() {
165 self.map.remove(&old_key);
166 }
167 }
168
169 let key = css.to_owned();
170 self.order.push_back(key.clone());
171 self.map.insert(key, list);
172 }
173}
174
175thread_local! {
176 static SELECTOR_CACHE: RefCell<SelectorCache> = RefCell::new(SelectorCache::new());
177}
178
179#[inline]
180fn parse_selector_cached(css: &str) -> Result<Arc<ast::SelectorList>, SelectorError> {
181 if css.len() > MAX_CACHED_SELECTOR_LEN {
182 return Ok(Arc::new(parse_selector(css)?));
183 }
184
185 SELECTOR_CACHE.with(|cache| {
186 if let Some(list) = cache.borrow().get(css) {
187 return Ok(list);
188 }
189
190 let parsed = Arc::new(parse_selector(css)?);
191 cache.borrow_mut().insert(css, Arc::clone(&parsed));
192 Ok(parsed)
193 })
194}
195
196fn merge_dedup_results(
201 _arena: &fhp_tree::arena::Arena,
202 roots: &[NodeId],
203 mut query: impl FnMut(NodeId) -> Vec<NodeId>,
204) -> Vec<NodeId> {
205 let mut results = Vec::new();
206 let mut max_seen = u32::MAX; for &root in roots {
208 for id in query(root) {
209 let idx = id.index() as u32;
210 if results.is_empty() || idx > max_seen {
211 max_seen = idx;
212 results.push(id);
213 } else if idx != max_seen {
214 if !results.contains(&id) {
217 results.push(id);
218 }
219 }
220 }
221 }
222 results
223}
224
225pub struct Selection<'a> {
230 doc: &'a Document,
231 nodes: Vec<NodeId>,
232}
233
234impl<'a> Selection<'a> {
235 fn new(doc: &'a Document, nodes: Vec<NodeId>) -> Self {
237 Self { doc, nodes }
238 }
239
240 pub fn first(&self) -> Option<NodeRef<'a>> {
242 self.nodes.first().map(|&id| self.doc.get(id))
243 }
244
245 pub fn iter(&self) -> impl Iterator<Item = NodeRef<'a>> + '_ {
247 self.nodes.iter().map(|&id| self.doc.get(id))
248 }
249
250 pub fn node_ids(&self) -> &[NodeId] {
252 &self.nodes
253 }
254
255 pub fn text(&self) -> String {
257 self.iter()
258 .map(|n| n.text_content())
259 .collect::<Vec<_>>()
260 .join("")
261 }
262
263 pub fn attr(&self, name: &str) -> Option<&'a str> {
265 self.first()?.attr(name)
266 }
267
268 pub fn inner_html(&self) -> String {
270 self.first().map(|n| n.inner_html()).unwrap_or_default()
271 }
272
273 pub fn len(&self) -> usize {
275 self.nodes.len()
276 }
277
278 pub fn is_empty(&self) -> bool {
280 self.nodes.is_empty()
281 }
282
283 pub fn select_compiled(&self, sel: &CompiledSelector) -> Result<Selection<'a>, SelectorError> {
288 let list = &sel.list;
289 if self.nodes.len() == 1 {
290 let results = select_all_list(self.doc.arena(), self.nodes[0], list);
291 return Ok(Selection::new(self.doc, results));
292 }
293 let results = merge_dedup_results(self.doc.arena(), &self.nodes, |root| {
294 select_all_list(self.doc.arena(), root, list)
295 });
296 Ok(Selection::new(self.doc, results))
297 }
298
299 pub fn select(&self, css: &str) -> Result<Selection<'a>, SelectorError> {
304 let list = parse_selector_cached(css)?;
305 if self.nodes.len() == 1 {
306 let results = select_all_list(self.doc.arena(), self.nodes[0], &list);
308 return Ok(Selection::new(self.doc, results));
309 }
310 let results = merge_dedup_results(self.doc.arena(), &self.nodes, |root| {
311 select_all_list(self.doc.arena(), root, &list)
312 });
313 Ok(Selection::new(self.doc, results))
314 }
315
316 pub fn xpath(&self, expr: &str) -> Result<XPathResult, XPathError> {
328 let parsed = xpath::parser::parse_xpath(expr)?;
329 let mut all_nodes = Vec::new();
330 let mut all_strings = Vec::new();
331 let mut seen = std::collections::HashSet::new();
332
333 for &node_id in &self.nodes {
334 let result = xpath::eval::evaluate(&parsed, self.doc.arena(), node_id);
335 match result {
336 XPathResult::Nodes(nodes) => {
337 for id in nodes {
338 if seen.insert(id) {
339 all_nodes.push(id);
340 }
341 }
342 }
343 XPathResult::Strings(strings) => {
344 all_strings.extend(strings);
345 }
346 XPathResult::Boolean(b) => return Ok(XPathResult::Boolean(b)),
347 }
348 }
349
350 if !all_strings.is_empty() {
351 Ok(XPathResult::Strings(all_strings))
352 } else {
353 Ok(XPathResult::Nodes(all_nodes))
354 }
355 }
356}
357
358impl<'a> IntoIterator for &'a Selection<'a> {
359 type Item = NodeRef<'a>;
360 type IntoIter = SelectionIter<'a>;
361
362 fn into_iter(self) -> Self::IntoIter {
363 SelectionIter {
364 doc: self.doc,
365 inner: self.nodes.iter(),
366 }
367 }
368}
369
370pub struct SelectionIter<'a> {
372 doc: &'a Document,
373 inner: std::slice::Iter<'a, NodeId>,
374}
375
376impl<'a> Iterator for SelectionIter<'a> {
377 type Item = NodeRef<'a>;
378
379 fn next(&mut self) -> Option<Self::Item> {
380 self.inner.next().map(|&id| self.doc.get(id))
381 }
382
383 fn size_hint(&self) -> (usize, Option<usize>) {
384 self.inner.size_hint()
385 }
386}
387
388impl<'a> ExactSizeIterator for SelectionIter<'a> {}
389
390pub trait Selectable {
394 fn select(&self, css: &str) -> Result<Selection<'_>, SelectorError>;
411
412 fn select_compiled(&self, sel: &CompiledSelector) -> Result<Selection<'_>, SelectorError>;
417
418 fn select_first_compiled(
420 &self,
421 sel: &CompiledSelector,
422 ) -> Result<Option<NodeRef<'_>>, SelectorError>;
423
424 fn select_first(&self, css: &str) -> Result<Option<NodeRef<'_>>, SelectorError>;
426
427 fn find_by_tag(&self, tag: Tag) -> Selection<'_>;
429
430 fn find_by_id(&self, id: &str) -> Option<NodeRef<'_>>;
435
436 fn find_by_class(&self, class: &str) -> Selection<'_>;
438
439 fn find_by_attr(&self, name: &str, value: &str) -> Selection<'_>;
441
442 fn xpath(&self, expr: &str) -> Result<XPathResult, XPathError>;
463}
464
465impl Selectable for Document {
466 fn select(&self, css: &str) -> Result<Selection<'_>, SelectorError> {
467 let list = parse_selector_cached(css)?;
468 let nodes = select_all_list(self.arena(), self.root_id(), &list);
469 Ok(Selection::new(self, nodes))
470 }
471
472 fn select_compiled(&self, sel: &CompiledSelector) -> Result<Selection<'_>, SelectorError> {
473 let nodes = select_all_list(self.arena(), self.root_id(), &sel.list);
474 Ok(Selection::new(self, nodes))
475 }
476
477 fn select_first_compiled(
478 &self,
479 sel: &CompiledSelector,
480 ) -> Result<Option<NodeRef<'_>>, SelectorError> {
481 let node = select_first_list(self.arena(), self.root_id(), &sel.list);
482 Ok(node.map(|id| self.get(id)))
483 }
484
485 fn select_first(&self, css: &str) -> Result<Option<NodeRef<'_>>, SelectorError> {
486 let list = parse_selector_cached(css)?;
487 let node = select_first_list(self.arena(), self.root_id(), &list);
488 Ok(node.map(|id| self.get(id)))
489 }
490
491 fn find_by_tag(&self, tag: Tag) -> Selection<'_> {
492 let arena = self.arena();
493 let mut nodes = Vec::new();
494 for i in 0..arena.len() {
495 let id = NodeId(i as u32);
496 let n = arena.get(id);
497 if n.tag == tag && is_document_element(n) {
498 nodes.push(id);
499 }
500 }
501 Selection::new(self, nodes)
502 }
503
504 fn find_by_id(&self, id: &str) -> Option<NodeRef<'_>> {
505 let arena = self.arena();
506 for i in 0..arena.len() {
507 let node_id = NodeId(i as u32);
508 let attrs = arena.attrs(node_id);
509 for attr in attrs {
510 if arena.attr_name(attr).eq_ignore_ascii_case("id")
511 && arena.attr_value(attr) == Some(id)
512 {
513 return Some(self.get(node_id));
514 }
515 }
516 }
517 None
518 }
519
520 fn find_by_class(&self, class: &str) -> Selection<'_> {
521 let arena = self.arena();
522 let mut nodes = Vec::new();
523 for i in 0..arena.len() {
524 let id = NodeId(i as u32);
525 let attrs = arena.attrs(id);
526 for attr in attrs {
527 if arena.attr_name(attr).eq_ignore_ascii_case("class") {
528 if let Some(val) = arena.attr_value(attr) {
529 if val.split_whitespace().any(|c| c == class) {
530 nodes.push(id);
531 break;
532 }
533 }
534 }
535 }
536 }
537 Selection::new(self, nodes)
538 }
539
540 fn find_by_attr(&self, name: &str, value: &str) -> Selection<'_> {
541 let arena = self.arena();
542 let mut nodes = Vec::new();
543 for i in 0..arena.len() {
544 let id = NodeId(i as u32);
545 let attrs = arena.attrs(id);
546 for attr in attrs {
547 if arena.attr_name(attr).eq_ignore_ascii_case(name)
548 && arena.attr_value(attr) == Some(value)
549 {
550 nodes.push(id);
551 break;
552 }
553 }
554 }
555 Selection::new(self, nodes)
556 }
557
558 fn xpath(&self, expr: &str) -> Result<XPathResult, XPathError> {
559 let parsed = xpath::parser::parse_xpath(expr)?;
560 Ok(xpath::eval::evaluate(&parsed, self.arena(), self.root_id()))
561 }
562}
563
564pub struct DocumentIndex<'a> {
568 doc: &'a Document,
569 id_map: HashMap<String, NodeId>,
570 class_map: HashMap<String, Vec<NodeId>>,
571 tag_map: HashMap<Tag, Vec<NodeId>>,
572}
573
574impl<'a> DocumentIndex<'a> {
575 pub fn build(doc: &'a Document) -> Self {
580 let arena = doc.arena();
581 let mut id_map = HashMap::with_capacity(arena.len() / 8);
582 let mut class_map: HashMap<String, Vec<NodeId>> = HashMap::with_capacity(arena.len() / 4);
583
584 let tag_map = if let Some(pre_built) = arena.tag_index() {
586 let mut map = HashMap::with_capacity(64);
587 for (tag_u8, ids) in pre_built.iter().enumerate() {
588 let filtered_ids: Vec<_> = ids
589 .iter()
590 .copied()
591 .filter(|&id| is_document_element(arena.get(id)))
592 .collect();
593 if !filtered_ids.is_empty() {
594 let tag: Tag = unsafe { std::mem::transmute(tag_u8 as u8) };
598 map.insert(tag, filtered_ids);
599 }
600 }
601 map
602 } else {
603 let mut map: HashMap<Tag, Vec<NodeId>> = HashMap::with_capacity(64);
604 for i in 0..arena.len() {
605 let node_id = NodeId(i as u32);
606 let n = arena.get(node_id);
607 if !is_document_element(n) {
608 continue;
609 }
610 map.entry(n.tag).or_default().push(node_id);
611 }
612 map
613 };
614
615 for i in 0..arena.len() {
617 let node_id = NodeId(i as u32);
618 let n = arena.get(node_id);
619
620 if !is_document_element(n) {
622 continue;
623 }
624
625 let attrs = arena.attrs(node_id);
626 for attr in attrs {
627 let attr_name = arena.attr_name(attr);
628 if attr_name.eq_ignore_ascii_case("id") {
629 if let Some(val) = arena.attr_value(attr) {
630 if let Some(existing) = id_map.get_mut(val) {
631 *existing = node_id;
632 } else {
633 id_map.insert(val.to_owned(), node_id);
634 }
635 }
636 }
637 if attr_name.eq_ignore_ascii_case("class") {
638 if let Some(val) = arena.attr_value(attr) {
639 for class in val.split_whitespace() {
640 if let Some(ids) = class_map.get_mut(class) {
641 ids.push(node_id);
642 } else {
643 class_map.insert(class.to_owned(), vec![node_id]);
644 }
645 }
646 }
647 }
648 }
649 }
650
651 Self {
652 doc,
653 id_map,
654 class_map,
655 tag_map,
656 }
657 }
658
659 pub fn find_by_id(&self, _doc: &Document, id: &str) -> Option<NodeRef<'a>> {
664 self.id_map.get(id).map(|&node_id| self.doc.get(node_id))
665 }
666
667 pub fn find_by_class(&self, _doc: &Document, class: &str) -> Vec<NodeRef<'a>> {
672 self.class_map
673 .get(class)
674 .map(|ids| ids.iter().map(|&id| self.doc.get(id)).collect())
675 .unwrap_or_default()
676 }
677
678 pub fn find_by_tag(&self, _doc: &Document, tag: Tag) -> Vec<NodeRef<'a>> {
683 self.tag_map
684 .get(&tag)
685 .map(|ids| ids.iter().map(|&id| self.doc.get(id)).collect())
686 .unwrap_or_default()
687 }
688}
689
690#[cfg(test)]
691mod tests {
692 use super::*;
693 use fhp_tree::parse;
694
695 #[test]
696 fn select_basic() {
697 let doc = parse("<div><p>Hello</p></div>").unwrap();
698 let sel = doc.select("p").unwrap();
699 assert_eq!(sel.len(), 1);
700 assert_eq!(sel.text(), "Hello");
701 }
702
703 #[test]
704 fn select_first_found() {
705 let doc = parse("<div><p>a</p><p>b</p></div>").unwrap();
706 let first = doc.select_first("p").unwrap();
707 assert!(first.is_some());
708 assert_eq!(first.unwrap().text_content(), "a");
709 }
710
711 #[test]
712 fn select_chaining() {
713 let doc = parse("<ul><li><a>1</a></li><li><a>2</a></li></ul>").unwrap();
714 let lis = doc.select("li").unwrap();
715 assert_eq!(lis.len(), 2);
716 let links = lis.select("a").unwrap();
717 assert_eq!(links.len(), 2);
718 assert_eq!(links.text(), "12");
719 }
720
721 #[test]
722 fn find_by_tag_works() {
723 let doc = parse("<div><span>a</span><span>b</span></div>").unwrap();
724 let sel = doc.find_by_tag(Tag::Span);
725 assert_eq!(sel.len(), 2);
726 }
727
728 #[test]
729 fn find_by_id_works() {
730 let doc = parse("<div id=\"main\">x</div><div>y</div>").unwrap();
731 let node = doc.find_by_id("main");
732 assert!(node.is_some());
733 assert_eq!(node.unwrap().text_content(), "x");
734 }
735
736 #[test]
737 fn find_by_id_missing() {
738 let doc = parse("<div>x</div>").unwrap();
739 assert!(doc.find_by_id("nope").is_none());
740 }
741
742 #[test]
743 fn find_by_class_works() {
744 let doc = parse("<div class=\"a b\">x</div><div class=\"c\">y</div>").unwrap();
745 let sel = doc.find_by_class("a");
746 assert_eq!(sel.len(), 1);
747 assert_eq!(sel.text(), "x");
748 }
749
750 #[test]
751 fn find_by_attr_works() {
752 let doc = parse("<a href=\"x\">a</a><a href=\"y\">b</a>").unwrap();
753 let sel = doc.find_by_attr("href", "x");
754 assert_eq!(sel.len(), 1);
755 assert_eq!(sel.text(), "a");
756 }
757
758 #[test]
759 fn selection_attr() {
760 let doc = parse("<a href=\"url\">link</a>").unwrap();
761 let sel = doc.select("a").unwrap();
762 assert_eq!(sel.attr("href"), Some("url"));
763 }
764
765 #[test]
766 fn selection_inner_html() {
767 let doc = parse("<div><p>Hello</p></div>").unwrap();
768 let sel = doc.select("div").unwrap();
769 assert_eq!(sel.inner_html(), "<p>Hello</p>");
770 }
771
772 #[test]
773 fn selection_empty() {
774 let doc = parse("<div>x</div>").unwrap();
775 let sel = doc.select("span").unwrap();
776 assert!(sel.is_empty());
777 assert_eq!(sel.len(), 0);
778 assert!(sel.first().is_none());
779 }
780
781 #[test]
782 fn document_index_o1() {
783 let doc = parse("<div id=\"a\">x</div><div id=\"b\">y</div>").unwrap();
784 let index = DocumentIndex::build(&doc);
785 let node = index.find_by_id(&doc, "b").unwrap();
786 assert_eq!(node.text_content(), "y");
787 }
788
789 #[test]
790 fn document_index_uses_document_it_was_built_from() {
791 let source_doc = parse("<div id=\"a\"><span>source</span></div>").unwrap();
792 let other_doc = parse("<p>other</p>").unwrap();
793 let index = DocumentIndex::build(&source_doc);
794
795 let node = index.find_by_id(&other_doc, "a").unwrap();
796 assert_eq!(node.text_content(), "source");
797 }
798
799 #[test]
800 fn document_index_find_by_class() {
801 let doc = parse("<div class=\"a b\">x</div><span class=\"b c\">y</span><p>z</p>").unwrap();
802 let index = DocumentIndex::build(&doc);
803
804 let class_b = index.find_by_class(&doc, "b");
805 assert_eq!(class_b.len(), 2);
806
807 let class_a = index.find_by_class(&doc, "a");
808 assert_eq!(class_a.len(), 1);
809 assert_eq!(class_a[0].text_content(), "x");
810
811 let class_missing = index.find_by_class(&doc, "nope");
812 assert!(class_missing.is_empty());
813 }
814
815 #[test]
816 fn document_index_find_by_tag() {
817 let doc = parse("<div>a</div><div>b</div><span>c</span>").unwrap();
818 let index = DocumentIndex::build(&doc);
819
820 let divs = index.find_by_tag(&doc, Tag::Div);
821 assert_eq!(divs.len(), 2);
822
823 let spans = index.find_by_tag(&doc, Tag::Span);
824 assert_eq!(spans.len(), 1);
825 assert_eq!(spans[0].text_content(), "c");
826
827 let links = index.find_by_tag(&doc, Tag::A);
828 assert!(links.is_empty());
829 }
830
831 #[test]
832 fn document_index_handles_uppercase_html_attributes() {
833 let doc = parse("<div ID=\"main\" CLASS=\"active hero\">x</div>").unwrap();
834 let index = DocumentIndex::build(&doc);
835
836 assert_eq!(index.find_by_id(&doc, "main").unwrap().text_content(), "x");
837 assert_eq!(index.find_by_class(&doc, "active").len(), 1);
838 assert_eq!(index.find_by_class(&doc, "hero").len(), 1);
839 }
840
841 #[test]
842 fn selection_into_iter() {
843 let doc = parse("<div><p>a</p><p>b</p></div>").unwrap();
844 let sel = doc.select("p").unwrap();
845 let texts: Vec<String> = (&sel).into_iter().map(|n| n.text_content()).collect();
846 assert_eq!(texts, vec!["a", "b"]);
847 }
848
849 #[test]
850 fn xpath_descendant() {
851 let doc = parse("<div><p>Hello</p></div>").unwrap();
852 let result = doc.xpath("//p").unwrap();
853 match result {
854 XPathResult::Nodes(nodes) => assert_eq!(nodes.len(), 1),
855 _ => panic!("expected Nodes"),
856 }
857 }
858
859 #[test]
860 fn xpath_text_extract() {
861 let doc = parse("<div><p>Hello</p></div>").unwrap();
862 let result = doc.xpath("//p/text()").unwrap();
863 match result {
864 XPathResult::Strings(texts) => {
865 assert_eq!(texts.len(), 1);
866 assert_eq!(texts[0], "Hello");
867 }
868 _ => panic!("expected Strings"),
869 }
870 }
871
872 #[test]
873 fn xpath_invalid() {
874 let doc = parse("<div>x</div>").unwrap();
875 assert!(doc.xpath("").is_err());
876 assert!(doc.xpath("bad").is_err());
877 }
878
879 #[test]
880 fn selection_xpath_chaining() {
881 let doc = parse("<ul><li>1</li><li>2</li></ul><ol><li>3</li></ol>").unwrap();
882 let sel = doc.select("ul").unwrap();
883 let result = sel.xpath("//li").unwrap();
884 match result {
885 XPathResult::Nodes(nodes) => assert_eq!(nodes.len(), 2),
886 _ => panic!("expected Nodes"),
887 }
888 }
889
890 #[test]
891 fn compiled_selector_basic() {
892 let sel = CompiledSelector::new("p").unwrap();
893 let doc = parse("<div><p>Hello</p></div>").unwrap();
894 let results = doc.select_compiled(&sel).unwrap();
895 assert_eq!(results.len(), 1);
896 assert_eq!(results.text(), "Hello");
897 }
898
899 #[test]
900 fn compiled_selector_first() {
901 let sel = CompiledSelector::new("p").unwrap();
902 let doc = parse("<div><p>a</p><p>b</p></div>").unwrap();
903 let first = doc.select_first_compiled(&sel).unwrap();
904 assert!(first.is_some());
905 assert_eq!(first.unwrap().text_content(), "a");
906 }
907
908 #[test]
909 fn compiled_selector_reuse_across_docs() {
910 let sel = CompiledSelector::new("span.active").unwrap();
911 let doc1 = parse("<span class=\"active\">one</span>").unwrap();
912 let doc2 = parse("<div><span class=\"active\">two</span></div>").unwrap();
913 assert_eq!(doc1.select_compiled(&sel).unwrap().text(), "one");
914 assert_eq!(doc2.select_compiled(&sel).unwrap().text(), "two");
915 }
916
917 #[test]
918 fn compiled_selector_chaining() {
919 let sel = CompiledSelector::new("a").unwrap();
920 let doc = parse("<ul><li><a>1</a></li><li><a>2</a></li></ul>").unwrap();
921 let lis = doc.select("li").unwrap();
922 let links = lis.select_compiled(&sel).unwrap();
923 assert_eq!(links.len(), 2);
924 }
925
926 #[test]
927 fn compiled_selector_invalid() {
928 assert!(CompiledSelector::new("").is_err());
929 }
930
931 #[test]
932 fn compiled_selector_clone() {
933 let sel = CompiledSelector::new("div").unwrap();
934 let sel2 = sel.clone();
935 let doc = parse("<div>ok</div>").unwrap();
936 assert_eq!(doc.select_compiled(&sel2).unwrap().len(), 1);
937 }
938}