-
Notifications
You must be signed in to change notification settings - Fork 566
Proposal: DQL CSS Selector Support
-
Zero native syntax impact: all CSS syntax lives inside quoted strings
"..."or thecss()method entry point. -
Desugar at compile time where possible (static quoted selectors); fall back to a runtime parser for
css(). -
Faithful CSS semantics: in particular,
.foomeanshasClass("foo")(token-list membership, i.e.[class~="foo"]), not$class == "foo".
The existing ns."elem-name" syntax is extended to parse CSS-flavored simple selectors.
| CSS | Quoted DQL | Equivalent native DQL |
|---|---|---|
div |
ns."div" |
ns.div (already works) |
.foo |
ns.".foo" |
ns.*@hasClass("foo") |
#bar |
ns."#bar" |
ns.*@($id == "bar") |
div.foo |
ns."div.foo" |
ns.div@hasClass("foo") |
div#bar |
ns."div#bar" |
ns.div@($id == "bar") |
div.foo#bar |
ns."div.foo#bar" |
ns.div@hasClass("foo") @($id == "bar") |
.a.b |
ns.".a.b" |
ns.*@hasClass("a") @hasClass("b") |
hasClass("foo") matches nodes where the class attribute contains "foo" as a whitespace-delimited token — not a substring match. This is identical to the CSS [class~="foo"] rule:
class="foo bar" → hasClass("foo") ✅
class="foo" → hasClass("foo") ✅
class="foobar" → hasClass("foo") ❌ (not an independent token)
class="bar baz" → hasClass("foo") ❌
All standard CSS attribute selector operators are supported, composable with tag/class/id:
| CSS syntax | Semantics | Equivalent native DQL |
|---|---|---|
[disabled] |
attribute exists | @($disabled.$exists) |
[type=text] |
exact match | @($type == "text") |
[type="text"] |
exact match (quoted) | @($type == "text") |
[href^=https] |
prefix match | @($href.hasPrefix("https")) |
[src$=.png] |
suffix match | @($src.hasSuffix(".png")) |
[class*=icon] |
substring match | @($class.contains("icon")) |
[class~=btn] |
token-list match | @hasClass("btn") |
Note: [class~=foo] and .foo are equivalent and both desugar to hasClass.
[class*=foo] is a plain substring match and is distinct from hasClass.
Combining examples:
"div[type=text]" → ns.div@($type == "text")
"input.form[required]" → ns.input@hasClass("form") @($required.$exists)
"a[href^=https].active" → ns.a@($href.hasPrefix("https")) @hasClass("active")
| CSS combinator | Semantics | DQL mapping | Status |
|---|---|---|---|
div span (space) |
descendant | ns.**.div.**.span |
✅ supported |
ul > li |
direct child | ns.**.ul.li |
✅ supported |
h1 + p |
adjacent sibling | requires sibling axis | |
h1 ~ p |
general sibling | requires sibling axis |
Sibling combinators require traversal across a parent's child list, which has no natural representation in DQL's parent-child tree model. They may be implemented in domain-specific NodeSet implementations (e.g. HTML) but are out of scope for the core language.
Combinator examples via css():
doc.css("ul > li") // → doc.**.ul.li
doc.css("div span.active") // → doc.**.div.**.span@hasClass("active")
doc.css("form > input[required]")
// → doc.**.form.input@($required.$exists)Only structural pseudo-classes with a clear static tree equivalent are supported.
| Pseudo-class | Equivalent DQL | Status |
|---|---|---|
:first-child |
[0] |
✅ |
:last-child |
last index | ✅ |
:nth-child(n) |
[n-1] (0-based) |
✅ |
:nth-child(2n+1) |
step filter | |
:not(.foo) |
@(!hasClass("foo")) |
✅ simple argument only |
:not([disabled]) |
@(!$disabled.$exists) |
✅ |
:hover, :focus, etc. |
rendering state, no tree equivalent | ❌ |
:not() is supported only for simple selectors (single tag, class, id, or attribute). Nested or compound :not() arguments are out of scope.
A comma-separated selector list returns the union of all matched NodeSets:
doc.css("h1, h2, h3") // union of all h1, h2, h3 descendants
doc.css("input, select, textarea")Union is only available via css(), not via the quoted-name syntax (which maps to a single chained expression). The resulting NodeSet preserves document order and deduplicates nodes.
Two complementary entry points are provided:
Best for simple, static selectors. Parsed and desugared by the compiler into native DQL operations — no runtime overhead.
doc."div.card"
doc.".active"
doc."input[required]"
doc."#main"Best for complex selectors, combinators, selector groups, or dynamically constructed strings.
doc.css("div.card > .title")
doc.css("h1, h2, h3")
doc.css("input:not([disabled])")
doc.css(mySelector) // dynamic stringBoth entry points return a standard lazy NodeSet and compose freely with the rest of DQL:
for item in doc.css("ul.menu > li")._all {
echo item.$"data-value"
}
titles := [el.text for el in doc.css("h1, h2, h3")]| Feature | Reason |
|---|---|
+ / ~ sibling combinators |
No sibling axis in DQL's tree model |
::before, ::after pseudo-elements |
Rendering layer, not tree nodes |
:hover, :focus, :checked, etc. |
Dynamic/interactive state |
:nth-child(An+B) step expressions |
Low priority; opt-in per domain |
Nested :not() / :is() / :where()
|
Scope creep; defer to later revision |
High priority — low cost, clear semantics, no conflicts
├── tag, .class, #id, tag.class, tag#id in quoted-name syntax
├── Attribute selectors: [attr], [attr=val], [attr^=], [attr$=], [attr*=], [attr~=]
└── :nth-child(n), :first-child, :last-child, :not(simple)
Medium priority — requires css() runtime parser
├── Descendant and child combinators (space, >)
└── Selector groups (,) returning union NodeSet
Not supported
└── Sibling combinators, dynamic pseudo-classes, pseudo-elements