Selector Extensions
Pagelove extends CSS selectors with pseudo-classes and functions for querying HTML as data. These extensions enable text matching, microdata comparison, numeric filtering, structure validation, and cross-document queries.
In addition to the extensions below, Pagelove supports CSS Selectors Level 4 features including :has(), :is(), :where(), :not(), and all standard structural pseudo-classes. Namespace-aware selectors ([prefix|attr], prefix|tagname) are supported via the Namespace header.
Quick reference
| Pseudo-class | Operates on | Match type | Case flag |
|---|---|---|---|
:contains(text) |
text content | substring | , i |
:equals(text) |
text content | exact | , i |
:value-contains(text) |
microdata value | substring | , i |
:value-equals(text) |
microdata value | exact | , i |
:less-than(n) |
text content | numeric < |
-- |
:greater-than(n) |
text content | numeric > |
-- |
:value-less-than(n) |
microdata value | numeric < |
-- |
:value-greater-than(n) |
microdata value | numeric > |
-- |
:only(selectors) |
descendants | structure | -- |
:isa(url) |
itemtype | type inheritance | -- |
| Function | Arguments | Returns |
|---|---|---|
count(selector) |
CSS selector | number |
text-of(selector) |
CSS selector | quoted text content |
value-of(selector) |
CSS selector | quoted microdata value |
attr-of(attr, selector) |
attribute name, CSS selector | quoted attribute value |
Text content vs microdata values
Text content is the concatenated, normalized text inside an element and its descendants.
Microdata value follows the WHATWG HTML Microdata specification. The value source depends on the element type:
| Element | Value source |
|---|---|
meta |
content attribute |
audio, embed, iframe, img, source, track, video |
src attribute |
a, area, link |
href attribute |
object |
data attribute |
data, meter |
value attribute |
time |
datetime attribute, falls back to text content |
| Everything else | Descendant text content |
When a spec-listed element is missing its designated attribute, the microdata value is an empty string — except <time>, which falls back to text content.
<!-- Text content: empty. Microdata value: "John Doe" (content attr) -->
<meta itemprop="name" content="John Doe">
<!-- Text content: "Click here". Microdata value: "/page" (href attr) -->
<a itemprop="url" href="/page">Click here</a>
<!-- Text content: "Forty-two". Microdata value: "42" (value attr) -->
<data itemprop="score" value="42">Forty-two</data>
The :contains() and :equals() families operate on text content. The :value-contains() and :value-equals() families operate on microdata values.
Text matching
:contains()
Matches elements whose text content includes a substring.
:contains('text')
:contains('text', i)
The i flag enables case-insensitive matching.
| Selector | Matches (given <h1>Hello World</h1> and <h2>hello world</h2>) |
|---|---|
h1:contains('Hello') |
<h1> |
h1:contains('hello') |
nothing (case-sensitive) |
:contains('hello', i) |
<h1> and <h2> |
:equals()
Matches elements whose text content exactly equals the given text. No substring matching.
:equals('text')
:equals('text', i)
| Selector | Matches (given <p>Title</p> and <p>Title Bar</p>) |
|---|---|
p:equals('Title') |
First <p> only |
p:contains('Title') |
Both |
Value matching
:value-contains()
Matches elements whose microdata value includes a substring.
:value-contains('text')
:value-contains('text', i)
:value-equals()
Matches elements whose microdata value exactly equals the given text.
:value-equals('text')
:value-equals('text', i)
Useful when microdata values differ from visible text:
| Selector | Matches | Reason |
|---|---|---|
[itemprop=hostname]:value-equals('localhost') |
<meta itemprop="hostname" content="localhost"> |
content attr |
[itemprop=url]:value-equals('/about') |
<a itemprop="url" href="/about">About Us</a> |
href attr |
[itemprop=score]:value-equals('42') |
<data itemprop="score" value="42">Forty-two</data> |
value attr |
:value-equals() combines naturally with :has() for parent-level queries:
[itemtype*=HostConfig]:has(
[itemprop=hostname]:value-equals('localhost'),
[itemprop=alias]:value-equals('127.0.0.1')
)
Comma-separated arguments inside :has() use OR semantics, per the CSS specification.
Numeric comparison
:less-than() and :greater-than()
Match elements whose text content, parsed as a number, is less than or greater than the threshold.
:less-than('number')
:greater-than('number')
:value-less-than() and :value-greater-than()
Match elements whose microdata value, parsed as a number, is less than or greater than the threshold.
:value-less-than('number')
:value-greater-than('number')
Numeric parsing rules
- Leading and trailing whitespace is trimmed before parsing.
- Decimal numbers are supported:
"9.99"parses as 9.99. - Non-numeric text never matches any numeric comparison.
- Empty text never matches.
When text cannot be parsed as a number, the comparison returns false. The element is silently skipped.
Structure validation
:only()
Matches elements where every descendant matches at least one of the given selectors.
:only(selector1, selector2, ...)
Use the > prefix to require direct children rather than any descendant:
| Selector | <ul><li>A</li><li>B</li></ul> |
<ul><li>A</li><div>X</div></ul> |
|---|---|---|
ul:only(> li) |
matches | does not match |
Multiple selectors combine with OR — each descendant must match at least one:
ul:only(> li, > li span) /* direct children are li; span inside li is allowed */
div:only(> ul, > ul *) /* direct child is ul; anything inside ul is allowed */
Elements with no descendants match vacuously.
Selector functions
Selector functions evaluate cross-document queries and replace themselves with the result. They operate across the entire site graph.
count()
Returns the number of elements matching a selector across all documents.
div:nth-child(count(h1))
If the site contains three h1 elements, this evaluates to div:nth-child(3).
text-of()
Returns the text content of a single matching element, wrapped in quotes.
[data-name=text-of(#title)]
Zero matches produce an empty string. More than one match produces an error.
value-of()
Returns the microdata value of a single matching element, wrapped in quotes. Value extraction follows the WHATWG element-specific rules above.
[data-name=value-of([itemprop='name'])]
Zero matches produce an empty string. More than one match produces an error.
attr-of()
Returns the value of a specific attribute from a single matching element, wrapped in quotes. The first argument is the attribute name (in single quotes). The second is a CSS selector.
[data-link=attr-of('href', #home)]
Zero matches produce an empty string. More than one match produces an error.
Text normalization
All text-based operations normalize text content before matching:
- Unicode NFC — canonical composition is applied.
- Trim — leading and trailing whitespace is removed.
- Collapse — consecutive whitespace (spaces, tabs, newlines) becomes a single space.
This means <p> Hello World\n</p> has normalized text content Hello World.
Case sensitivity
All text and value matching pseudo-classes default to case-sensitive comparison. Append , i as the last argument for case-insensitive matching. The i flag means ASCII case-insensitivity (the same semantics as the CSS attribute-selector i flag), uniformly across all four case-insensitive pseudo-classes (:contains, :equals, :value-contains, :value-equals): A-Z fold to a-z; non-ASCII letters compare exactly (é does not match É).
Numeric comparison pseudo-classes do not support the i flag — numeric comparison is inherently case-insensitive.
Quoting
Arguments to pseudo-classes accept single or double quotes:
:contains('hello')
:contains("hello")
Type inheritance matching
:isa()
Matches elements whose itemtype is the target URL or any descendant type per the schema inheritance hierarchy.
:isa('itemtype-url')
This pseudo-class is schema-aware: it consults the inheritance hierarchy defined by your schemas to determine whether an element's type is a descendant of the target type. The match is reflexive -- an element whose itemtype exactly equals the target also matches.
| Selector | Matches |
|---|---|
:isa('https://schema.org/Thing') |
Elements with itemtype="https://schema.org/Thing" or any schema-defined descendant (e.g., Person, Organization) |
div:isa('https://example.com/Base') |
<div> elements whose itemtype is Base or any descendant of Base |
Without a schema inheritance hierarchy (e.g., when no schemas are loaded), :isa() matches nothing. Reflexive matching requires the inheritance map to be present.
:isa() is particularly useful for polymorphic queries -- finding all elements of a given type family without listing every descendant type explicitly:
/* Instead of listing every authorization rule type: */
[itemtype='https://pagelove.org/AuthorizationRule'],
[itemtype='https://pagelove.org/PathAuthorizationRule'],
[itemtype='https://pagelove.org/TypeAuthorizationRule']
/* Use :isa() for a single polymorphic query: */
:isa('https://pagelove.org/AuthorizationRule')
The query planner recognizes :isa() and emits itemtype index lookups for the target and all descendants, so polymorphic queries benefit from the same index acceleration as explicit [itemtype] selectors.
See also
- Sessel — the expression language that can embed selector queries
- Resource Binding — use extended selectors in resource binding queries
- Property — schema properties that selectors can match against
Non-ASCII text in selectors
A selector sent in the Range header may contain non-ASCII text directly —
the platform decodes header values as UTF-8, so p:contains('café') works
as written. If your client or an intermediary mangles non-ASCII header
bytes, use CSS escape sequences instead (p:contains('caf\E9 ') — note the
trailing space terminating the escape).