Repository: aantron/lambdasoup
Pull request #40 (2021-08-13 05:32:33)

Expose the matches_selector function in the interface
There's a way to select elements from a tree, but there's no way to check if a node would match a given selector.

That can be convenient for filtering element lists returned by `Soup.select`. The `:not()` pseudo-class makes that much harder to do from code since you need intricate string manipulation, and it still doesn't cover all use cases easily, so I think exposing the already existing but internal `matches_selector` function is justified.

The case where I wanted that was the new `ignore_selectors` option for soupault's table of contents widget. The `:not()` pseudo-class wouldn't cut it because the widget needs to select all headings (`h1, h2, ...`) and the user potentially would have to add a `:not` part to each of them, while a separate `ignore_selectors` option composes with heading selection much more nicely.

I made a [dirty hack](https://github.com/dmbaturin/soupault/blob/master/src/html_utils.ml#L89-L97) to allow ignoring simple selectors (without combinators), but a full solution requires exposing the now-internal functionality of the `Selector` module.



dmbaturin: @aantron What do you think of this PR?
aantron: This looks fine, except that I would suggest to reorder the arguments so that they are `root_node selector at_node`.
dmbaturin: Done.

I agree it's a better API, I just followed your argument order in the initial version, under the assumption that it's what you want it to be.

Patch:
diff --git a/src/soup.ml b/src/soup.ml
index 15b544f..96b6502 100644
--- a/src/soup.ml
+++ b/src/soup.ml
@@ -468,6 +468,7 @@ sig
 
   val parse : string -> t
   val select : (_ node) -> t -> element nodes
+  val matches_selector : t -> soup node -> soup node -> bool
 end =
 struct
   type type_ = Name of string | Universal
@@ -626,43 +627,40 @@ struct
       with_stop (fun stop ->
         sequence.eliminate (fun v node -> f v node |> stop.throw) init)}
 
+  let matches_selector selector root_node at_node =
+    with_stop (fun stop ->
+      let rec backwards_traversal at_node = function
+	| [] -> if at_node == root_node then stop.throw true else ()
+	| (combinator, simple_selectors)::rest ->
+	  if not (is_element at_node) then ()
+	  else
+	    if not (matches_simple_selectors at_node simple_selectors) then ()
+	    else
+	      let next_nodes =
+		match combinator with
+		| Descendant ->
+		  at_node |> simple_ancestors |> up_to root_node
+		| Child -> at_node |> ancestors |> one
+		| IndirectSibling ->
+		  at_node |> previous_siblings |> elements |> up_to root_node
+		| AdjacentSibling ->
+		  at_node |> previous_siblings |> elements |> one
+	      in
+	      next_nodes |> iter (fun node -> backwards_traversal node rest)
+      in
+      backwards_traversal at_node (List.rev selector);
+      false)
+
   let select root_node selector =
     let root_node = forget_type root_node in
-
-    let matches_selector at_node =
-      with_stop (fun stop ->
-        let rec backwards_traversal at_node = function
-          | [] -> if at_node == root_node then stop.throw true else ()
-          | (combinator, simple_selectors)::rest ->
-            if not (is_element at_node) then ()
-            else
-              if not (matches_simple_selectors at_node simple_selectors) then ()
-              else
-                let next_nodes =
-                  match combinator with
-                  | Descendant ->
-                    at_node |> simple_ancestors |> up_to root_node
-                  | Child -> at_node |> ancestors |> one
-                  | IndirectSibling ->
-                    at_node |> previous_siblings |> elements |> up_to root_node
-                  | AdjacentSibling ->
-                    at_node |> previous_siblings |> elements |> one
-                in
-                next_nodes |> iter (fun node -> backwards_traversal node rest)
-        in
-        backwards_traversal at_node (List.rev selector);
-        false)
-    in
-
     let candidates =
       match simple_parent root_node with
       | None -> descendants root_node
       | Some parent -> descendants parent
     in
-
     candidates
     |> elements
-    |> filter matches_selector
+    |> filter (matches_selector selector root_node)
 
   let is_decimal_char c =
     ((Char.code c) >= (Char.code '0')) && ((Char.code c) <= (Char.code '9'))
@@ -976,6 +974,12 @@ struct
     loop []
 end
 
+let matches_selector root_node selector node =
+  let root_node = forget_type root_node in
+  let node = forget_type node in
+  let selector = Selector.parse selector in
+  Selector.matches_selector selector root_node node
+
 let select selector node =
   selector |> Selector.parse |> Selector.select node
 
diff --git a/src/soup.mli b/src/soup.mli
index 2b197f8..dd9c80c 100644
--- a/src/soup.mli
+++ b/src/soup.mli
@@ -167,6 +167,13 @@ sig
   val ($$) : (_ node) -> string -> element nodes
 end
 
+val matches_selector : (_ node) -> string -> element node -> bool
+(** [matches_select selector root node] checks if [node] would match [selector]
+    in the [root_node] document.
+
+    [node] {e should} be a child of the [root_node].
+    If [node] is a standalone element node created with {!create_element}
+    or a child of some other root node, the result will always be [false]. *)
 
 (** {2 Options} *)
 


Test patch:
diff --git a/test/test.ml b/test/test.ml
index fbd44f4..e46eaca 100644
--- a/test/test.ml
+++ b/test/test.ml
@@ -340,6 +340,12 @@ let suites = [
       assert_equal (soup $ "#first" |> classes) [];
       assert_equal (soup $ "#second" |> classes) ["foo"; "bar"; "baz"]);
 
+    ("matches-selector" >:: fun _ ->
+      let soup = parse "<div> <p id='foo'>bar</p> </div>" in
+      let elem = select_one "div p#foo" soup |> Option.get in
+      assert_bool "element matches selector" (matches_selector soup "div p#foo" elem)
+    );
+
     ("fold_attributes" >:: fun _ ->
       let s =
         page "list"