cascadia 0.6.9
cascadia: ^0.6.9 copied to clipboard
A Dart CSS selector library Cascadia project, extended with modern CSS features.
Cascadia Dart #
A Dart implementation of the Cascadia CSS selector library, originally written in Go and extended with full CSS Selectors Level 3, Level 4, and beyond coverage per the MDN Web Docs.
Flutter-compatible — works in Dart VM, browsers, and Flutter apps. No browser-specific APIs; pure Dart.
Features #
- Complete CSS Selector Support — 60+ selectors, 70+ pseudo-classes, and 25+ pseudo-elements from MDN
- Full parsing with robust error handling
- Selector matching against DOM trees (package:html)
- Specificity calculation per CSS Selectors spec
- Serialization back to CSS string representation
- Combinators — descendant, child, adjacent sibling, general sibling, namespace
- Pseudo-elements via
parseWithPseudoElements() - Nesting selector (
&) for CSS Nesting Module - Shadow DOM selectors (
:host,:host(),:host-context(),:has-slotted(),::slotted(),::part()) - View Transitions pseudo-classes and pseudo-elements
- Container Queries and scoping selectors
- Anchor Positioning pseudo-classes
- Media state pseudo-classes (:playing, :paused, :muted, etc.)
- Text-content selectors (
:contains(),:containsown(),:matches()) - Form validation pseudo-classes
- All with null safety and Dart best practices
Installation #
Add to your pubspec.yaml:
dependencies:
cascadia: ^0.1.0
Usage #
Basic Queries #
import 'package:cascadia/cascadia.dart';
import 'package:html/parser.dart';
void main() {
final html = '''
<div class="container">
<p class="intro">Hello</p>
<p>World</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
</div>
''';
final doc = parse(html);
// Parse a selector
final sel = parse('p.intro');
// Find all matching nodes
final results = queryAll(doc, sel);
print('Found ${results.length} matching nodes'); // 1
// Or use compile for repeated matching
final matcher = compile('li');
final allLis = queryAll(doc, matcher);
print('List items: ${allLis.length}'); // 2
}
Working with Pseudo-elements #
// Enable pseudo-elements in parsing
final sel = parseWithPseudoElements('p::first-letter');
Combinators #
// Descendant
final descendants = parse('div p'); // all p inside div
// Child
const children = parse('ul > li'); // direct li children of ul
// Adjacent sibling
final adjacent = parse('h1 + p'); // p immediately after h1
// General sibling
const general = parse('h1 ~ p'); // any p after h1
Pseudo-classes #
// Structural
parse(':first-child');
parse(':nth-child(2n+1)');
parse(':nth-of-type(3)');
parse(':only-of-type');
// Relational
parse(':has(.error)'); // has descendant with class "error"
parse(':has-child(input)'); // has immediate child input
parse(':is(h1, h2, h3)'); // matches any of these
parse(':where(.foo, .bar)'); // zero specificity
parse(':not(.disabled)'); // negation
// Link
parse(':any-link'); // both visited & unvisited
parse(':local-link'); // same-origin links
parse(':link'); // unvisited
parse(':visited'); // visited (stub)
// Form
parse(':enabled');
parse(':disabled');
parse(':checked');
parse(':required');
parse(':optional');
parse(':read-only');
parse(':read-write');
parse(':valid');
parse(':invalid');
parse(':in-range');
parse(':out-of-range');
parse(':placeholder-shown');
parse(':autofill');
parse(':indeterminate');
parse(':blank');
parse(':user-valid');
parse(':user-invalid');
// Interaction
parse(':focus');
parse(':focus-visible');
parse(':focus-within');
parse(':hover'); // stub
parse(':active'); // stub
parse(':target');
parse(':target-within'); // stub
// Element state
parse(':open'); // details, select, dialog[open]
parse(':modal'); // dialog[open] modal
parse(':fullscreen'); // fullscreen element
parse(':popover-open'); // popover showing
parse(':default'); // default option/button
// Linguistic
parse(':lang(en)');
parse(':dir(ltr)');
parse(':dir(rtl)');
// Media playback
parse(':playing');
parse(':paused');
parse(':buffering');
parse(':seeking');
parse(':stalled');
parse(':muted');
parse(':volume-locked');
parse(':picture-in-picture');
// Temporal (view timelines)
parse(':current');
parse(':past');
parse(':future');
// View transitions
parse(':target-current');
parse(':target-before');
parse(':target-after');
parse(':active-view-transition');
parse(':active-view-transition-type(slide)');
// Paged media
parse(':left'); // left-hand page
parse(':right'); // right-hand page
parse(':first'); // first page
// Custom element
parse(':defined');
// Other
parse(':root');
parse(':empty');
parse(':heading'); // any h1–h6
parse(':heading(2n+1)'); // headings by position
parse(':interest-source'); // experimental
parse(':interest-target');
parse(':state(private)'); // custom state
parse(':xr-overlay'); // XR overlay
// Shadow DOM
parse(':host');
parse(':host(.foo)');
parse(':host-context(.theme-dark)');
parse(':has-slotted(*)');
Pseudo-elements #
// Classic typographic
parseWithPseudoElements('p::first-line');
parseWithPseudoElements('p::first-letter');
// Generated content
parseWithPseudoElements('a::before');
parseWithPseudoElements('a::after');
// Form controls
parseWithPseudoElements('input::file-selector-button');
parseWithPseudoElements('input::placeholder');
parseWithPseudoElements('select::picker-icon');
// Tree-abiding
parseWithPseudoElements('li::marker');
parseWithPseudoElements('details::details-content');
// Shadow DOM
parseWithPseudoElements('::part(header)');
parseWithPseudoElements('::slotted(span)');
// Highlights
parseWithPseudoElements('::selection');
parseWithPseudoElements('::spelling-error');
parseWithPseudoElements('::grammar-error');
parseWithPseudoElements('::highlight(custom)');
parseWithPseudoElements('::search-text');
Specificity #
final sel = parse('div.foo#bar[href]');
print(sel.specificity); // (1, 1, 1) — IDs, classes/attributes/pseudo-classes, type selectors
final low = parse(':where(.a, .b)');
print(low.specificity); // (0, 0, 0) — zero specificity
Serialization #
final sel = parse('div.foo > p::first-line');
final css = serialize(sel); // "div.foo > p::first-line"
Namespace Support #
// Namespace prefix with pipe separator
parse('svg|rect'); // rect in any namespace
parse('|a'); // a with no namespace
parse('*|a'); // a in any namespace
Nesting Selector #
// The & selector references parent selector(s) in nested CSS
final nesting = parse('.card &'); // parent selector inside nested rule
Flutter Integration #
Cascadia works seamlessly in Flutter apps—pure Dart, no browser dependencies. Use it to parse and query HTML/XML data fetched from networks, assets, or generated locally.
With package:html (Pure Dart) #
import 'package:cascadia/cascadia.dart';
import 'package:html/parser.dart';
Future<List<Element>> fetchArticles(String htmlString) async {
final document = parse(htmlString);
// Select all article elements with class "post"
return queryAll(document, 'article.post');
}
// Use in a Flutter widget:
FutureBuilder<List<Element>>(
future: fetchArticles(htmlResponse),
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView(
children: snapshot.data!
.map((el) => ListTile(
title: Text(el.querySelector('h2')?.text ?? ''),
subtitle: Text(el.querySelector('p')?.text ?? ''),
))
.toList(),
);
}
return CircularProgressIndicator();
},
);
With flutter_html package #
If you're using the flutter_html package to render HTML in Flutter, you can pre-process the HTML with Cascadia to manipulate or extract data before rendering.
import 'package:cascadia/cascadia.dart';
import 'package:html/parser.dart';
import 'package:flutter_html/flutter_html.dart';
HtmlData processHtml(String rawHtml) {
final doc = parse(rawHtml);
// Remove all ads using CSS selector
final ads = queryAll(doc, '.ad, .Advertisement, [id*="ad-"]');
for (final ad in ads) {
ad.remove();
}
// Extract all links
final links = queryAll(doc, 'a[href]');
final hrefs = links.map((a) => a.attributes['href']).toList();
// Convert back to HTML string for flutter_html
final cleanedHtml = doc.outerHtml;
return HtmlData(
html: cleanedHtml,
linkCount: hrefs.length,
);
}
Cascadia's selector engine is fast, supports full CSS selectors, and works entirely offline—ideal for Flutter mobile/desktop apps.
Supported Selectors #
Type, Class, ID, Universal #
| Selector | Example | Description |
| ------------------- | ---------- | ------------------------ | ------------------------ |
| Type selector | div | Element by tag name |
| Universal selector | * | Any element |
| Class selector | .warning | Class attribute contains |
| ID selector | #header | ID attribute matches |
| Namespace separator | svg | circle | Namespace-qualified name |
Attribute Selectors #
| Selector | Example | Description |
| --------------- | ------------------- | ------------------------------- | ------- | ------------------------- |
| [attr] | [disabled] | Attribute present |
| [attr=value] | [type="text"] | Exact value |
| [attr~=value] | [class~="active"] | Whitespace-separated word |
| [attr | =value] | [lang | ="en"] | Value or value-* prefix |
| [attr^=value] | [href^="https"] | Starts with |
| [attr$=value] | [src$=".png"] | Ends with |
| [attr*=value] | [title*="info"] | Contains substring |
| [attr!=value] | [lang!="fr"] | Not equal |
| [attr#=regex] | [id#="^test-"] | Regex match (non-standard) |
| [attr=i] | [lang=i] | Case-insensitive (non-standard) |
Combinators #
| Selector | Example | Description |
|---|---|---|
| Descendant | A B |
B anywhere inside A |
| Child | A > B |
B direct child of A |
| Adjacent sibling | A + B |
B immediately after A |
| General sibling | A ~ B |
B anywhere after A |
| Selector list | A, B, C |
Matches if any matches |
Pseudo-classes #
Tree-structural
:root, :empty, :first-child, :last-child, :only-child, :nth-child(an+b), :nth-last-child(an+b), :first-of-type, :last-of-type, :only-of-type, :nth-of-type(an+b), :nth-last-of-type(an+b)
Relational
:has(selector), :haschild(selector), :is(selector), :where(selector), :not(selector), :matches(regex) / :matchesown(regex), :contains(text) / :containsown(text)
Link & Location
:link, :visited, :any-link, :local-link, :target, :scope
User Interaction
:hover, :active, :focus, :focus-visible, :focus-within
Input & Form
:enabled, :disabled, :checked, :default, :indeterminate, :placeholder-shown, :autofill, :required, :optional, :read-only, :read-write, :valid, :invalid, :in-range, :out-of-range, :user-valid, :user-invalid, :blank, :input
Element Display State
:open, :modal, :fullscreen, :popover-open, :picture-in-picture
Linguistic
:lang(language), :dir(ltr|rtl)
Media Playback
:playing, :paused, :buffering, :seeking, :stalled, :muted, :volume-locked
Temporal (View Timelines)
:current, :past, :future
View Transitions
:target-current, :target-before, :target-after, :active-view-transition, :active-view-transition-type(type)
Paged Media
:left, :right, :first
Custom State
:state(state-name)
Custom Element
:defined
XR (AR/VR)
:xr-overlay
Anchor Positioning
:anchor(name?), :has-anchor
Container Queries & Scoping
:in-container, :ancestor, :parent, :prev-sibling, :next-sibling
Miscellaneous
:heading (any h1–h6) and :heading(an+b) (position-based)
Pseudo-elements #
Typographic
::first-line, ::first-letter, ::cue / ::cue(name)
Generated Content
::before, ::after
Form-related
::placeholder, ::file-selector-button, ::picker / ::picker(), ::picker-icon, ::checkmark, ::details-content
Tree-abiding
::marker, ::backdrop, ::column, ::scroll-button() / ::scroll-button(axis?), ::scroll-marker, ::scroll-marker-group
Shadow DOM
::part(name), ::slotted(selector?)
Highlight
::selection, ::spelling-error, ::grammar-error, ::target-text, ::search-text, ::highlight(name)
View Transitions
::view-transition, ::view-transition-group(name), ::view-transition-image-pair(name), ::view-transition-old(name), ::view-transition-new(name)
API Reference #
Top-level Functions #
parse(String selector) → Sel
Parse a CSS selector string (without pseudo-elements). Returns a selector object.
parseGroup(String selector) → Sel
Parse a comma-separated selector list. Returns a SelectorGroup.
parseWithPseudoElements(String selector) → Sel
Parse with pseudo-elements enabled. Throws if pseudo-elements appear without this API.
compile(String selector) → Sel
Shorthand for parse(). Pre-compiles selector for repeated matching.
query(Node root, Sel selector) → Node?
Find first matching node in the tree.
queryAll(Node root, Sel selector) → List<Node>
Find all matching nodes in the tree.
Classes #
Sel
Base interface for all selector objects:
bool match(Node node)— test if a node matchesSpecificity get specificity— specificity weightString toString()— CSS representation
Specificity
Triple (a, b, c) representing selector weight:
a— ID selectorsb— class, attribute, pseudo-class selectorsc— type selectors and pseudo-elements
SelectorGroup
Combines multiple selectors; matches if any constituent matches.
CompoundSelector
Multiple simple selectors on the same element (e.g., div.foo#bar).
CombinedSelector
Two selectors joined by a combinator (descendant, child, +, ~).
Selector Types #
TagSelector— type selector (div,svg|rect,*)ClassSelector—.classNameIdSelector—#idValueAttributeSelector—[attr=value],[attr~=value], etc.PseudoClassSelector— all pseudo-classesPseudoElement— pseudo-elements (viapseudoElementgetter)
Limitations #
-
Runtime-dependent pseudo-classes such as
:hover,:focus,:active,:valid,:indeterminate,:autofill,:in-range,:out-of-range,:buffering,:seeking,:stalled,:current,:past,:future,:target,:scope,:local-link,:anchor,:has-anchor,:in-container,:parent,:prev-sibling,:next-sibling,:xr-overlay,:state(), and view transition pseudo-classes stub tofalsebecause static DOM analysis cannot determine runtime state. These are correctly rejected at compile-time rather than causing runtime exceptions. -
Shadow DOM traversal for
:host,:host(),:host-context(),:has-slotted()and::slotted()returns false without a shadow root context. -
Namespace prefix resolution in
TagSelectoruses the element's prefix where available; forpackage:htmlthis is typicallynull. Fully correct namespace matching may require extensions. -
Custom element definition (
:defined) returns false without access to the custom element registry. -
Interest API, XR overlay, paged media, container queries require runtime context unavailable in static analysis.
-
Pseudo-elements represent rendered fragments; matching them directly against a node returns
false. You can detect their presence in selectors and serialize them.
Design Notes #
-
Specificity follows CSS Selectors Level 3:
:not()and:is()take the maximum specificity of their arguments;:where()is zero;:has()takes the specificity of its contents. -
Combinator parsing treats whitespace as descendant combinator; explicit combinators are
>,+,~. -
Selector list (
, separated) groups alternatives with maximum specificity across all. -
String escaping supports hex escapes (
\1234), standard backslash escapes, and quoted strings for attribute values. -
Text-content selectors (
:contains,:containsown,:matches,:matchesown) collect text via DOM traversal. -
The library cover almost complete MDN coverage.
Contributing #
Contributions are welcome! Please open issues and PRs on the GitHub repository.
Testing #
dart test
License #
BSD 3-Clause