Find elements with QuerySelector, QuerySelectorAll, Matches and Closest using Selectors Level 4, including :has(), :is(), :not() with selector lists, and form-state pseudo-classes.
The query members are on Node, so you can query the whole document (doc.Root) or any element. They follow the DOM standard: QuerySelector returns the first match in document order, and QuerySelectorAll returns every match. When nothing matches, QuerySelector returns the null node. Check IsNull rather than comparing to null, because Node is a struct.
using RecursiveAI.OneHtml; using RecursiveAI.OneHtml.Selectors; Document doc = Document.Parse(File.ReadAllBytes("events.html")); Node root = doc.Root; // First match, or the null node Node current = root.QuerySelector("nav a[aria-current=page]"); Console.WriteLine($"Current page: {current.TextContent}"); Node none = root.QuerySelector("aside"); Console.WriteLine($"Has an aside: {!none.IsNull}"); // All matches, in document order; Selectors Level 4 is supported foreach (Node card in root.QuerySelectorAll("article:has(img:not([alt]))")) { Console.WriteLine($"Card without alt text: {card.GetAttribute("data-event-id")}"); } foreach (Node link in root.QuerySelectorAll(":is(header, footer) a[href]")) { Console.WriteLine($"{link.LocalName} -> {link.GetAttribute("href")}"); } // Matches and Closest test an element you already have Node details = root.QuerySelector("a[href$='/story-time/']"); Console.WriteLine($"Inside a card: {details.Matches(".card a")}, card: {details.Closest(".card").GetAttribute("data-event-id")}"); // Form state pseudo-classes are decided from attributes Console.WriteLine($"Fields showing a placeholder: {root.QuerySelectorAll("input:placeholder-shown").Count}"); // Compile a selector once when you run it many times SelectorList external = SelectorList.Parse("a[href^='http']:not([href*='harborstreet'])"); foreach (Node link in root.QuerySelectorAll(external)) { Console.WriteLine($"External: {link.GetAttribute("href")}"); } // A bad selector throws SelectorSyntaxException try { root.QuerySelector("nav >"); } catch (SelectorSyntaxException e) { Console.WriteLine($"Rejected: {e.Message}"); }
Current page: Events Has an aside: False Card without alt text: 114 a -> / a -> /books/ a -> /events/ a -> /visit/ a -> mailto:[email protected] Inside a card: True, card: 114 Fields showing a placeholder: 1 External: https://example.org/repair-cafe Rejected: Not a valid selector list.
What the matcher does, and doesn’t, decide:
- Type selectors and attribute names match HTML elements case-insensitively, and the HTML list of case-insensitive attribute values (
type,langand others) is honored. Addifor anything else, as in[href^='javascript:' i]. :checked,:disabled,:required,:placeholder-shown,:in-rangeand the other form pseudo-classes are decided from attributes.:validand:invalidare decided where that is possible without script:required,email,url,pattern,min,maxandstep.- No script runs, so
:definedmatches every element that isn’t a custom element.:visited,:autofill,:popover-openand:modalnever match. :hover,:focus,:focus-visible,:activeand:targetmatch only when you give an interaction state inSelectorMatchOptions. Otherwise they match nothing. The cascade takes the same state throughStyleContext(see Compute CSS styles).- Pseudo-elements parse but never match an element.
- An unclosed
[or(at the end of the input is closed for you, as CSS Syntax says, soa[hrefis valid. A selector that is really invalid throwsSelectorSyntaxException.
SelectorList.Parse compiles a selector once. Matching against a compiled list allocates nothing, and QuerySelectorAll allocates only the result list.
There is no XPath API. The full rules, limits and the web-platform-tests results are in Selectors.
Sixteen selectors measured against AngleSharp, Fizzler and Chrome: CSS selectors in C#.