Skip to content

Proposal: DQL CSS Selector Support

xushiwei edited this page Mar 16, 2026 · 1 revision

Design Principles

  • Zero native syntax impact: all CSS syntax lives inside quoted strings "..." or the css() 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, .foo means hasClass("foo") (token-list membership, i.e. [class~="foo"]), not $class == "foo".

1. Simple Selectors — Quoted-Name Extension

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 semantics

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") ❌

2. Attribute Selectors [...]

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")

3. Combinators

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 ⚠️ not supported
h1 ~ p general sibling requires sibling axis ⚠️ not supported

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)

4. Pseudo-classes

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 ⚠️ optional
: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.


5. Selector Groups (Comma)

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.


6. Entry Points

Two complementary entry points are provided:

Quoted-name extension (compile-time desugar)

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"

css() method (runtime parser)

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 string

Both 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")]

7. What Is Explicitly Not Supported

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

8. Summary

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

Clone this wiki locally