Match a compound selector against a tree
Writing this once demystifies querySelectorAll completely. There are two halves: turning a selector string into a structure, and testing that structure against a node.
A node is a plain object { tag, attrs, children }, where attrs and children may be missing and a child may be a plain string of text. Implement matchAll(root, selector) returning an array of the matching **nodes** in document order — that is, depth-first pre-order, with the root itself as a candidate. String children are text and never match.
The selector is a single compound selector with no combinators: an optional leading tag name followed by any number of #id, .class and attribute parts. A node matches only when every part matches.
Tag names are compared case-insensitively, so div matches a node whose tag is "DIV". An #id part matches when attrs.id is exactly equal. A .class part treats attrs.class as a whitespace-separated list and requires the class as a whole token, so .item does not match class="itemized". Attribute parts are [attr] for presence with any value, and [attr="v"], [attr^="v"], [attr$="v"] and [attr*="v"] for exact, prefix, suffix and substring comparison against String(attrs[attr]); the value may be wrapped in double quotes, single quotes, or neither. Return an empty array when nothing matches.
Arguments
root{ tag: string; attrs?: object; children?: Array }- The root node of a plain-object element tree. The root itself is a candidate for matching.
selectorstring- A compound selector with no combinators: optional tag plus any number of
#id,.classand[attr]parts.
Returns
Array<Node> — The matching nodes in depth-first pre-order (document order). [] when nothing matches.
Examples
const tree = { tag: "DIV", attrs: { class: "page" }, children: [
{ tag: "ul", attrs: { class: "list" }, children: [
{ tag: "li", attrs: { id: "a", class: "item active" }, children: ["A"] },
{ tag: "li", attrs: { id: "b", class: "item" }, children: ["B"] },
{ tag: "li", attrs: { id: "c", class: "itemized" }, children: ["C"] }
] }
] };
matchAll(tree, "li.item").map((n) => n.attrs.id);
// => ["a", "b"]Classes are matched as whole tokens; itemized does not match .item.
const tree = { tag: "div", attrs: {}, children: [
{ tag: "A", attrs: { id: "x", href: "https://aniui.test/docs" } },
{ tag: "A", attrs: { id: "y", href: "/local" } }
] };
matchAll(tree, 'a[href^="https"]').map((n) => n.attrs.id);
// => ["x"]^= is a prefix test, and the tag name is matched case-insensitively.
matchAll({ tag: "div", attrs: {}, children: ["only text"] }, "li.missing");
// => []No match returns [] rather than throwing.
What it has to do
- Return matching nodes in depth-first pre-order, including the root when it matches.
- Require every part of the compound selector to match.
- Compare tag names case-insensitively and match classes as whole whitespace-separated tokens.
- Support
[attr],[attr="v"],[attr^="v"],[attr$="v"]and[attr*="v"], with optional quotes. - Skip string children and tolerate missing
attrsorchildren, returning[]when nothing matches.
Notes
- Parse the selector once before you walk the tree — turning the string into a
{ tag, ids, classes, attrs }spec object avoids re-parsing it at every node. - The real
querySelectorAllsupports combinators (>,+,~, descendant whitespace) and pseudo-classes. Adding>and descendants to this would turn it into a short state machine over the compound parts.
Stuck? The javascript course covers everything this challenge needs.
- 1.matches a tag and class together, as whole tokens
- 2.matches the root by id
- 3.matches on attribute presence
- 4.matches a prefix comparison with a case insensitive tag
- 5.returns an empty array when nothing matches
These assertions run in your browser the moment you hit Run. Pass all of them to claim the XP.
Discussion
Sign in to postNo comments yet. Be the first to say something.