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

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:

  1. Unicode NFC — canonical composition is applied.
  2. Trim — leading and trailing whitespace is removed.
  3. 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

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).