Working with Collections

CSS selector expressions always return lists — even when only one element matches, the result is a list. This guide covers everything you need to work with those lists: counting, accessing specific positions, filtering, transforming, checking conditions, aggregating numbers, and validating list properties.

Counting

The simplest thing you can do with a list is count it. .count() returns the number of elements the selector matched.

${div.item}.count()

Counting becomes useful as soon as you compare it to something:

${div.item}.count() > 3

That expression is true when more than three .item elements exist. It is false when there are three or fewer. You can also require an exact number:

${h1}.count() == 1

This is true only when the page has exactly one <h1> — a common requirement for well-structured documents.

Accessing elements

.count() tells you how many elements exist, but sometimes you want a specific one. Sessel provides four access methods:

${h1}.first()
${p}.last()
${li}.at(2)
${li}.slice(0, 5)

.first() is the most common: you use it to extract the single element you actually want to read text or attribute values from.

${h1}.first().text()
${[itemprop="price"]}.first().value()
${a.external}.first().attr("href")

.at(n) gives you arbitrary positional access. The first element is .at(0), the second is .at(1), and so on.

.slice(start, end) is useful when you want to work with a page of results — for example, the first five items in a list, or items 10 through 20.

Filtering

.filter(el => predicate) returns a new list containing only the elements for which the predicate is true. You pick a variable name, then write a condition using it.

${li}.filter(el => el.text().Integer() > 10)

This keeps only the <li> elements whose text content, parsed as an integer, is greater than 10. Every element in the original list is tested; those that pass appear in the result.

${[itemprop="price"]}.filter(el => el.value().Number() > 50)

Here the variable is named el, but you can choose any name — what matters is that you use the same name inside the predicate:

${[itemprop="rating"]}.filter(r => r.value().Number() >= 4)
${div.card}.filter(card => card.attr("data-featured") == "true")

The variable refers to the element being tested. From there, .text(), .value(), and .attr() extract whatever you need to compare.

The callback can also receive the element's index and the source list as additional parameters:

${li}.filter((el, i) => i < 5)

This keeps only the first five elements. The second parameter i is the zero-based index.

Filtering is composable. You can filter a list, then count the result:

${li}.filter(el => el.text().Integer() > 10).count()

Or filter and then access the first match:

${[itemprop="price"]}.filter(el => el.value().Number() > 50).first()

Transforming

.map(el => expression) builds a new list by evaluating the expression once for each element. The result list has the same length as the input, but each position holds the computed value instead of the original element.

Mapping to values:

${[itemprop="item"]}.map(e => e.value())

Mapping to numbers:

${li}.map(el => el.text().Integer())

Like filter(), map() can accept an index parameter:

${li}.map((el, i) => i.String() + ". " + el.text())

Mapping to dictionaries lets you assemble structured data from a collection of elements:

${[itemprop="product"]}.map(p => { name: p.text(), price: p.attr("data-price") })

Each <[itemprop="product"]> element becomes a dictionary with a name key (from its text content) and a price key (from its data-price attribute). The result is a list of dictionaries — one per matching element.

You can combine .filter() and .map() to select a subset and then extract specific values from it:

${[itemprop="price"]}.filter(el => el.value().Number() > 50).map(el => el.value())

Checking conditions

Sometimes you do not want to see the elements themselves — you want a yes/no answer about them as a group. Sessel provides two methods for that:

${div.item}.all(el => el.text().count() > 0)

This is true only when every .item element has non-empty text. Use .all() when you need a guarantee that holds across the entire collection — for example, that every required field has been filled in.

${[itemprop="price"]}.any(el => el.value().Number() > 100)

This is true as soon as at least one price exceeds 100. Use .any() when you want to know whether a condition is possible rather than universal — for example, whether a page contains any featured items.

Aggregation

For numeric data, Sessel can compute aggregates directly:

${[itemprop="price"]}.sum()
${[itemprop="rating"]}.min()
${[itemprop="rating"]}.max()

These methods read the microdata value of each element, so they work naturally with <data> and <meter> elements that carry their value in the value attribute.

Computing an average combines .sum() and .count():

${[itemprop="price"]}.sum() / ${[itemprop="price"]}.count()

You can also aggregate across multiple documents using the from clause. This sums prices from every page under /products/:

(${[itemprop="price"]} from "/products/*").sum()

Collection validation

Beyond counting and aggregating, Sessel can answer structural questions about a list's values:

.unique() removes duplicate entries:

[1, 2, 2, 3].unique()
// [1, 2, 3]
${[itemprop="category"]}.unique()

This returns a new list with duplicates removed — useful for deduplicating tags, identifiers, or any collection where repeated values are unwanted.

.subset() checks that a list's values are all drawn from an allowed set. The from clause lets the allowed set come from a different document:

(${[itemprop="tag"]} from self).subset(${[itemprop="allowed-tag"]} from "/config/tags")

Every tag on the current page must appear in the allowed-tags configuration document. If any tag is absent from that list, the expression is false.

.disjoint() checks for the absence of overlap — useful when two sets of values must remain separate:

(${[itemprop="tag"]} from self).disjoint(${[itemprop="tag"]} from "/published/*")

Null handling

.first(), .last(), and .at(n) all return null when the list is empty or the position is out of range. Null propagates through subsequent method calls, so a chain that starts from a null element does not crash — it simply produces null at each step.

When you need a fallback, the ?? (null-coalescing) operator provides one:

${#subtitle}.first().text() ?? "Untitled"

If the #subtitle element does not exist, .first() returns null, .text() returns null, and ?? substitutes "Untitled". The fallback can be any expression.

For conditional logic that goes beyond a simple fallback, use the ternary operator for simple two-way choices:

${div.item}.count() > 0 ? "has items" : "empty"

Ternary and ?? can be combined. The ternary controls which branch to evaluate; ?? handles null within a branch:

${div.item}.count() > 0
    ? ${div.item}.first().text() ?? "unnamed item"
    : "no items"

When you need multiple branches or multi-statement logic, use if expressions instead:

let count = ${div.item}.count();
if (count > 10) {
  "many items"
} else if (count > 0) {
  ${div.item}.first().text() ?? "unnamed item"
} else {
  "no items"
}

if is an expression — it returns the value of the taken branch, so it composes naturally with let, construction, and method chains.

Sorting, reversing, and flattening

Lists can be sorted by a computed key, reversed, and flattened from nested structures into a single list:

${[itemprop="product"]}.sort(el => el.attr("data-price").Number())
${li}.reverse()
${section}.map(s => ${li} from s).flatten()

For more complex sort orders, pass a two-parameter comparator lambda with the spaceship operator <=>:

${li}.sort((a, b) => a.text() <=> b.text())

And list values can be joined into a single string:

${[itemprop="tag"]}.map(el => el.text()).join(", ")

Reducing

.reduce(initial, callback) collapses a list into a single value by applying a two-parameter callback to an accumulator and each element:

[1, 2, 3, 4].reduce(0, (acc, el) => acc + el)

This produces 10 -- the sum of all elements. .reduceRight() works the same way but processes elements from right to left.

These methods and more are covered in full in the List reference.

Next steps