cascadia 0.8.0
cascadia: ^0.8.0 copied to clipboard
A Dart CSS selector engine — parse, match and serialize CSS Selectors Level 3 and 4 against HTML documents.
Changelog #
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
0.8.0 - 2026-08-06 #
Correctness release following a full engineering audit. See AUDIT.md in the
repository for the defect register and reproduction harness.
Fixed — crashes and hangs #
- Infinite loop in the parser (P0-1). Any character the parser did not
recognise caused it to loop forever, allocating until the VM died. This was
reachable from documented input:
parse('svg|rect')— the namespace syntax shown in the README — never returned. The parser now guarantees forward progress and raises aFormatExceptionwith the offending offset. - Crashes on ordinary HTML (P0-2/3/4).
queryAll(doc, ':first-child')threw_TypeErroron markup containing text nodes, and:rootthrew when a doctype was present. TheElementtype guard is now applied centrally inSel.matchWith, so no selector body can receive a non-element node. - Unbounded selector cache (P0-5). The cache claimed to be an LRU but
flushed itself entirely when full and cached selectors of any length. It is
now a real bounded LRU; added
clearSelectorCache()andselectorCacheSize.
Fixed — incorrect matching #
:has()now honours relative combinators::has(> p),:has(+ p)and:has(~ p)were previously treated as plain descendant searches (P1-1).- Class matching is now case-sensitive, per HTML standards mode (P1-2).
:opennow requires theopenattribute instead of matching every<details>/<dialog>(P1-3).:defaultnow matches<option selected>and the first submit control of a form; it previously looked for a non-existentdefaultattribute (P1-4).:enabledand:optionalno longer match arbitrary elements such as<div>; both are restricted to form controls (P1-5, P1-6).- A bare pseudo-element such as
::beforeno longer matches every element (P1-7). - CSS escapes are decoded on parse and re-escaped on serialization, so
.foo\.barmatchesclass="foo.bar"(P1-9). - Namespace selectors resolve against the element's real namespace URI rather
than a hardcoded
null(P1-10). :rootmatches only the document element, not a doctype or comment (P1-11).:dir()resolves inherited directionality instead of comparing the literal attribute.
Fixed — serialization and specificity #
[attr$="value"]serialized to the literal text[$name$="$value"]because of a raw-string bug, producing output that could not be reparsed (P2-1).- The descendant combinator emitted three spaces (P2-2).
an+bserialization dropped thenand left unbalanced parentheses::nth-child(2n+1)became:nth-child(2+1)(P2-3).- The universal selector
*no longer contributes specificity (P2-4). :matches(/re/)is parsed as a real regex instead of being round-tripped through a string and rejected (P2-6).- Malformed selectors (
div >,'',div,,,:not(),p::before span) are now rejected instead of silently producing a phantom universal selector (P2-8). Unknown pseudo-classes are errors; useparseLenient()to accept them.
Fixed — found by fuzzing and generated cross-checks #
These six were found during remediation hardening rather than by the original audit, and are fixed in this release:
- String pseudo-class arguments grew on every round trip.
:contains("x")stored the raw source slice including its quotes, so serializing re-quoted it and the selector text roughly doubled each time. Arguments are now parsed as a string or identifier, which also makes malformed input such as:contains("x"s)a proper error. Affected:contains(),:containsown(),:lang()and:state(). - A quote inside a regex broke parsing.
:matches(/a"b/)failed as an unterminated string. - Control characters serialized to invalid CSS. An identifier containing a
decoded control character (for example from
\a) was emitted with the raw character, producing output that could not be reparsed. Control characters are now re-emitted as hex escapes in both identifiers and strings. p::beforematched every<p>element. The earlier fix for bare pseudo-elements only covered the form with an empty selector list; a qualified pseudo-element still matched. A pseudo-element denotes a rendered fragment and never matches an element node in either form.CompoundSelector.supportignored the pseudo-element, reportingdecidableforp::before.- Removed unreachable code in
dom_compat.dart: the sibling and child getters were shadowed bypackage:html's ownElementmembers and could never execute.tagNameremains, and the extension is now exported from the public library rather than reachable only viapackage:cascadia/src/.
Changed — breaking #
queryAll/queryno longer return the root element (P1-8). This matches the DOM'squerySelectorAll. PreviouslyqueryAll(divElement, 'div')returned the element itself.- The
Matcherinterface has been removed (P2-7). It collided withpackage:matcher/package:test, forcing users to writehide Matcher, and added onlytoString()overSel. Implement or referenceSelinstead. - The
Serializerclass has been removed;serialize(sel)andsel.toString()are now the single implementation. - Thirty-one unreachable
*PseudoElementclasses have been deleted. They were exported and documented but never constructed by the parser. Pseudo-elements are now the single validatedPseudoElementSelector, so::bogusis a parse error.
Added #
MatchContext— supply runtime state (hover, focus, target, visited URLs, custom element registry, custom states) so selectors that a static DOM cannot decide now work:queryAll(doc, ':focus', MatchContext(focused: el)).MatchSupportandSel.undecidableParts— inspect whether a selector isdecidable,requiresContextorneverDecidable, and which parts need state. Replaces ~90 stub classes whose bodies werereturn false, where a non-match was indistinguishable from an unimplemented feature.MatchContext.strictthrowsUndecidableSelectorErrorinstead of silently returningfalse.closest(),parseLenient(),clearSelectorCache(),selectorCacheSize.- Full
an+bsupport in:nth-child()and friends, includingodd/even.
Internal #
pseudo_classes.dartreduced from 2107 lines to a set of focused modules; 122return falsebodies collapsed into one parameterised type.- Tree traversal in
query/queryAllis iterative, removing the stack-depth ceiling on deep documents. - Test suite expanded from 48 to 170 tests: a per-defect regression suite, a serialization round-trip property test, behavioural coverage of every pseudo-class module, and a fuzz suite running ~65k random and mutated selectors per run.
- Line coverage raised from 70% to 87.4%, enforced at 85% in CI by
tool/check_coverage.dart. - The README's "Limitations" prose is replaced by a capability matrix
generated from
MatchSupportbytool/generate_capability_matrix.dart; CI fails if it drifts from the code. Currently 61 fully supported, 31 context-dependent, 12 parse-only. - Package archive reduced from 411 KB to 43 KB by excluding the generated
doc/tree, which shipped despite being git-ignored (.pubignoreoverrides.gitignoreper directory). - README examples are now compile-checked and executed in
example/so they cannot rot. - Added CI: format, analyze, tests with coverage, coverage gate, audit harness, example analysis, capability-matrix check and a publish dry run, across Dart stable and 3.0.0.
0.7.6 - 2026-05-10 #
Documentation #
- Added comprehensive dartdoc comments for all pseudo-class and pseudo-element classes, improving pub.dev documentation score from 10/20 to 20/20
- Fixed markdown table formatting in README for proper pub.dev rendering
- Updated documentation to use consistent code formatting and notation
Build #
- Declared supported platforms (android, ios, linux, macos, windows, web) in pubspec.yaml to improve platform support score from 0/20 to 20/20
- Added
.pubignoreto exclude tool/ directory from package publication - Updated
.gitignoreto properly exclude build artifacts
0.6.9 - 2026-05-04 #
Added #
- Full CSS selector parsing (Selectors Level 3 and many Level 4+)
- 78 pseudo-classes (
:first-child,:nth-child,:not,:has,:is,:where,:focus-visible,:dir,:has-slotted,:heading,:active-view-transition, etc.) - 31 pseudo-elements (
::before,::after,::first-line,::first-letter,::selection,::part,::slotted,::view-transition-*, etc.) - Combinators: descendant, child, adjacent sibling, general sibling
- Attribute selectors:
[attr],[attr=value],[attr~=value],[attr|=value],[attr^=value],[attr$=value],[attr*=value] - Namespace support (
svg|circle) - Nesting selector (
&) - Specificity calculation per CSS spec
- Serialization to CSS string
query()andqueryAll()DOM traversal utilitiescompile()for reusable matcher functionsparseWithPseudoElements()for pseudo-element support- Comprehensive test suite (48 tests)
- Full Flutter compatibility
- BSD 3-Clause license
Documentation #
- Complete README with selector reference table
- Usage examples for Dart and Flutter
- API documentation with dartdoc comments