A parsed CSS-like selector, and the rules for matching one against a widget.
A selector constrains five things, all optional. A constraint that is nil or
empty is satisfied by anything.
:widget_type— the widget's type, written bare ("button"). A CamelCase name is converted withMacro.underscore/1, so"DataTable"and"data_table"parse alike.:id— written#name:classes— written.name, repeatable. Every class in the selector must be present on the widget; the widget may carry others.:pseudo_classes— written:name. Recognised names are:hover,:focus,:active,:disabled,:checked,:selected,:expandedand:collapsed; any other:nameis dropped during parsing rather than reported.:part— written::name, naming a sub-region of a widget such as::border. A selector naming a part matches only a context whose:partis equal to it; a selector naming no part matches whether or not the context has one, which is what lets a widget's base rules apply underneath its part rules.
Match context
matches?/2 takes a map, not a widget. It reads :widget_type, :id,
:classes and :part directly, and derives the active pseudo-classes from the
boolean keys :hovered, :focused, :active, :disabled, :checked,
:selected and :expanded. :collapsed is active when :expanded is exactly
false. Those keys must hold true, false or nil; any other value raises
FunctionClauseError.
iex> alias Drafter.Style.Selector
iex> [selector] = Selector.parse("button:focus")
iex> Selector.matches?(selector, %{widget_type: :button, focused: true})
true
iex> Selector.matches?(selector, %{widget_type: :button, focused: false})
falseNames as atoms or strings
parse/1 converts each name with String.to_existing_atom/1 and keeps the
string when no such atom exists. A selector therefore holds a mix, and matching
compares names rather than terms — see same_name?/2.
Descendant selectors
parse/1 splits a space-separated selector into one selector per token and
returns them as a list. No ancestor relationship is recorded and none is checked:
Drafter.Style.Stylesheet matches a rule when any selector in the list
matches, so "container button" behaves as the selector group
"container, button" rather than as a descendant selector.
Summary
Types
A widget description matches?/2 is run against.
A selector's weight, as {ids, classes, types}.
Functions
Compare two selectors by specificity, as :gt, :lt or :eq.
Whether context satisfies every constraint selector sets.
Build a selector from its parts.
Parse a selector into a list of selectors, one per space-separated token.
Whether two selector tokens name the same thing.
The selector's weight, as {ids, classes, types}.
Types
@type context() :: %{ optional(:widget_type) => atom() | String.t() | nil, optional(:id) => atom() | String.t() | nil, optional(:classes) => [atom() | String.t()], optional(:part) => atom() | nil, optional(:hovered) => boolean() | nil, optional(:focused) => boolean() | nil, optional(:active) => boolean() | nil, optional(:disabled) => boolean() | nil, optional(:checked) => boolean() | nil, optional(:selected) => boolean() | nil, optional(:expanded) => boolean() | nil }
A widget description matches?/2 is run against.
Every key is optional. Absent keys are treated as unset, except that a selector
naming classes never matches a context without a :classes key.
@type pseudo_class() ::
:hover
| :focus
| :active
| :disabled
| :checked
| :selected
| :expanded
| :collapsed
@type specificity() :: {non_neg_integer(), non_neg_integer(), non_neg_integer()}
A selector's weight, as {ids, classes, types}.
Ordered as an Erlang term, so the tuples compare in CSS specificity order.
@type t() :: %Drafter.Style.Selector{ classes: [atom()], id: atom() | nil, part: atom() | nil, pseudo_classes: [pseudo_class()], widget_type: atom() | nil }
Functions
Compare two selectors by specificity, as :gt, :lt or :eq.
Examples
iex> alias Drafter.Style.Selector
iex> Selector.compare_specificity(Selector.new(id: :save), Selector.new(widget_type: :button))
:gt
iex> alias Drafter.Style.Selector
iex> Selector.compare_specificity(Selector.new(widget_type: :button), Selector.new(widget_type: :label))
:eq
Whether context satisfies every constraint selector sets.
All five constraints must hold. An unset constraint is satisfied by anything, so
new/0 matches every context. Names are compared by same_name?/2, so a context
may use strings where the selector uses atoms.
A selector naming classes needs context to have a :classes key; a context
without one never matches. A selector naming a part needs context.part to be
equal to it, compared with == rather than by name. A selector naming no part
matches whether or not the context has one.
Examples
iex> alias Drafter.Style.Selector
iex> Selector.matches?(Selector.new(), %{})
true
iex> alias Drafter.Style.Selector
iex> Selector.matches?(Selector.new(widget_type: :button), %{widget_type: "button"})
true
iex> alias Drafter.Style.Selector
iex> Selector.matches?(Selector.new(classes: [:primary]), %{classes: [:primary, :large]})
true
iex> Selector.matches?(Selector.new(classes: [:primary]), %{classes: [:large]})
false
iex> Selector.matches?(Selector.new(classes: [:primary]), %{})
false
iex> alias Drafter.Style.Selector
iex> Selector.matches?(Selector.new(pseudo_classes: [:collapsed]), %{expanded: false})
true
iex> Selector.matches?(Selector.new(pseudo_classes: [:collapsed]), %{})
false
iex> alias Drafter.Style.Selector
iex> Selector.matches?(Selector.new(part: :border), %{part: :border})
true
iex> Selector.matches?(Selector.new(part: :border), %{})
false
iex> Selector.matches?(Selector.new(), %{part: :border})
true
Build a selector from its parts.
Options, all defaulting to the struct default:
:widget_type— atom or string, defaultnil:id— atom or string, defaultnil:classes— list of atoms or strings, default[]:pseudo_classes— list ofpseudo_class/0, default[]:part— atom, defaultnil
Nothing is validated: an unrecognised pseudo-class is stored as given and then never matches.
Examples
iex> Drafter.Style.Selector.new()
%Drafter.Style.Selector{widget_type: nil, id: nil, classes: [], pseudo_classes: [], part: nil}
iex> Drafter.Style.Selector.new(widget_type: :button, pseudo_classes: [:focus])
%Drafter.Style.Selector{widget_type: :button, id: nil, classes: [], pseudo_classes: [:focus], part: nil}
Parse a selector into a list of selectors, one per space-separated token.
An atom is taken as a bare widget type and gives a one-element list. Order within
a token does not matter: type, #id, .class, ::part and :pseudo_class are
each extracted independently, so "button:focus::border" and
"button::border:focus" parse alike.
Unrecognised pseudo-class names are dropped, and an unrecognised part name
becomes nil, which drops the part constraint rather than failing the match.
Examples
iex> Drafter.Style.Selector.parse(:button)
[%Drafter.Style.Selector{widget_type: :button, id: nil, classes: [], pseudo_classes: [], part: nil}]
iex> [selector] = Drafter.Style.Selector.parse("DataTable::row:selected")
iex> {selector.widget_type, selector.part, selector.pseudo_classes}
{:data_table, :row, [:selected]}
iex> Drafter.Style.Selector.parse("container button") |> length()
2
iex> [selector] = Drafter.Style.Selector.parse(":no_such_pseudo_class")
iex> selector.pseudo_classes
[]
Whether two selector tokens name the same thing.
A token is an atom or a string; the two forms compare equal when their names are
equal, so :button and "button" match. nil on either side is never a match.
Parsed selectors hold a string wherever the corresponding atom does not yet exist in the VM — a widget type whose module has not been loaded, or an id belonging to a widget that has not rendered — so callers must not assume atoms.
A term that is neither an atom nor a binary is named by inspect/1, so two equal
such terms match and unequal ones do not.
Examples
iex> Drafter.Style.Selector.same_name?(:button, "button")
true
iex> Drafter.Style.Selector.same_name?("button", "label")
false
iex> Drafter.Style.Selector.same_name?(nil, nil)
false
@spec specificity(t()) :: specificity()
The selector's weight, as {ids, classes, types}.
ids is 1 when the selector names an id. classes counts the classes, the
pseudo-classes and the part together. types is 1 when the selector names a
widget type. The tuple orders as an Erlang term, so comparing two of them
compares specificity.
Examples
iex> alias Drafter.Style.Selector
iex> Selector.specificity(Selector.new())
{0, 0, 0}
iex> alias Drafter.Style.Selector
iex> Selector.specificity(Selector.new(widget_type: :button))
{0, 0, 1}
iex> alias Drafter.Style.Selector
iex> [selector] = Selector.parse("button.primary:focus::border")
iex> Selector.specificity(selector)
{0, 3, 1}