selectorcheck

command
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 28, 2026 License: BSD-3-Clause Imports: 8 Imported by: 0

Documentation

Overview

Command selectorcheck reports every selector a rewrite cannot use, before the rewrite starts.

$ selectorcheck -f rules.txt
42 selectors: 37 usable, 5 rejected

A line beginning "//" is a comment. Not "#", because that begins an id selector.

li + li            an unsupported combinator: + and ~ need a sibling the rewriter has
                   not seen yet
:has(p)            :has, :is and :where are not implemented
::before           a pseudo-element is not an element
p:empty            :empty needs the element's content, which arrives later
:not(div p)        a combinator inside :not() is rejected, and the message blames the
                   pseudo-class - see B175

NewWriter names one bad selector, not all of them

A rewrite with a list of selectors and five bad ones learns about one: lolhtml.NewWriter returns on the first rejection, so fixing it reveals the next and a list of five costs five round trips. Measured - one call with ten selectors of which five are bad returns a lolhtml.SelectorError naming exactly one.

So this builds a one-selector writer per selector, which names all of them. That is the informative way round and, past about a thousand selectors, also the fast one.

Registering selectors together is superlinear

Building one writer with N selectors costs more than N writers with one each, and the gap grows. Measured on an M3 Pro, fastest of ten:

selectors      build   µs per selector   allocations   per selector
       10        8µs             0.700            73           7.30
      100       81µs             0.810           571           5.71
      500      615µs             1.228          2734           5.47
     1000    1.956ms             1.956          5408           5.41
     2000    5.896ms             2.948         10718           5.36
     4000   23.702ms             5.926         21524           5.38

The allocation count is linear - about 5.4 per selector, flat from five hundred up - and the time is not: four times the selectors cost twelve times the time. B172 recorded the allocation figure and this is the other half of it, which matters for the programs that have thousands of selectors: a stylesheet-coverage tool, a sanitiser with a per-element allowlist, a rule engine fed from configuration.

How superlinear depends on the machine, which is why the tests gate the allocation count and only log the durations. On the project's musl runner the per-selector cost went from 6872ns at a hundred selectors to 10063ns at two thousand - a factor of 1.46 rather than the 2.4 above - because there the fixed cost per selector is nine times larger and dominates. The shape is the same; the multiplier is the machine's.

One consequence is this program's own shape. Validating a thousand selectors one at a time took 1.55ms against 1.944ms for registering them together, so checking them separately is not a cost paid for better errors - past that size it is cheaper outright.

Where the message needs help

Four of the five rejections above arrive as "Unsupported pseudo-class or pseudo-element in selector", which is accurate for `::before` and `p:empty`, arguable for `:has(p)`, and wrong for `:not(div p)`, where the problem is the combinator inside the parentheses and not the pseudo-class - B175. The combinator case is the one lol-html words well: `li + li` says "Unsupported combinator `+`". So this adds its own line for the cases where the library's is misleading, and keeps the library's text as well, because that is what a search will match.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL