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.