A CSS selector parser library for Elixir. Parses CSS selector strings into an Abstract Syntax Tree (AST) that can be analyzed, manipulated, and rendered back to CSS.
- CSS Selectors Level 1 - Complete support
- CSS Selectors Level 2 - Complete support
- CSS Selectors Level 3 - Complete support
- CSS Selectors Level 4 - Extensive support for stable features
Feature | Status | Example |
---|---|---|
Type selectors | β | h1 , p , div |
Class selectors | β | .warning , .note |
ID selectors | β | #header , #footer |
Descendant combinator | β | div p , ul li |
:link pseudo-class |
β | a:link |
:visited pseudo-class |
β | a:visited |
:active pseudo-class |
β | a:active |
::first-line pseudo-element |
β | p::first-line |
::first-letter pseudo-element |
β | p::first-letter |
Multiple selectors (grouping) | β | h1, h2, h3 |
Feature | Status | Example |
---|---|---|
Universal selector | β | * |
Attribute selectors | β | [title] , [class="example"] |
Attribute operators | β | [class~="warning"] , [lang\|="en"] |
Child combinator | β | body > p |
Adjacent sibling combinator | β | h1 + p |
:hover pseudo-class |
β | a:hover |
:focus pseudo-class |
β | input:focus |
:before pseudo-element |
β | p:before (legacy syntax) |
:after pseudo-element |
β | p:after (legacy syntax) |
:first-child pseudo-class |
β | li:first-child |
:lang() pseudo-class |
β | :lang(fr) |
Multiple attribute selectors | β | input[type="text"][required] |
Descendant combinator with universal | β | div * |
Feature | Status | Example |
---|---|---|
Namespace selectors | β | svg\|rect , *\|* |
Substring matching attribute selectors | β | [href^="https"] , [src$=".png"] , [title*="hello"] |
General sibling combinator | β | h1 ~ p |
:root pseudo-class |
β | :root |
:nth-child() pseudo-class |
β | :nth-child(2n+1) |
:nth-last-child() pseudo-class |
β | :nth-last-child(2) |
:nth-of-type() pseudo-class |
β | p:nth-of-type(odd) |
:nth-last-of-type() pseudo-class |
β | div:nth-last-of-type(2n) |
:last-child pseudo-class |
β | li:last-child |
:first-of-type pseudo-class |
β | p:first-of-type |
:last-of-type pseudo-class |
β | h2:last-of-type |
:only-child pseudo-class |
β | p:only-child |
:only-of-type pseudo-class |
β | img:only-of-type |
:empty pseudo-class |
β | div:empty |
:target pseudo-class |
β | :target |
:enabled pseudo-class |
β | input:enabled |
:disabled pseudo-class |
β | input:disabled |
:checked pseudo-class |
β | input:checked |
:not() pseudo-class |
β | :not(.active) |
::before pseudo-element |
β | div::before |
::after pseudo-element |
β | div::after |
::first-line pseudo-element |
β | p::first-line |
::first-letter pseudo-element |
β | p::first-letter |
Feature | Status | Example |
---|---|---|
Case-sensitivity flag | β | [attr=value i] , [attr=value s] |
Column combinator | β | col \|\| td |
:is() pseudo-class |
β | :is(h1, h2, h3) |
:where() pseudo-class |
β | :where(article, section) p |
:has() pseudo-class |
β | :has(> img) |
:not() with complex selectors |
β | :not(div.active) |
:matches() pseudo-class |
β | :matches(h1, h2, h3) |
:focus-within |
β | :focus-within |
:focus-visible |
β | :focus-visible |
:any-link |
β | :any-link |
:read-write pseudo-class |
β | input:read-write |
:read-only pseudo-class |
β | input:read-only |
:placeholder-shown pseudo-class |
β | input:placeholder-shown |
:default pseudo-class |
β | option:default |
:valid pseudo-class |
β | input:valid |
:invalid pseudo-class |
β | input:invalid |
:in-range pseudo-class |
β | input:in-range |
:out-of-range pseudo-class |
β | input:out-of-range |
:required pseudo-class |
β | input:required |
:optional pseudo-class |
β | input:optional |
::placeholder pseudo-element |
β | input::placeholder |
::selection pseudo-element |
β | ::selection |
::backdrop pseudo-element |
β | dialog::backdrop |
::marker pseudo-element |
β | li::marker |
::cue pseudo-element |
β | ::cue |
::slotted() pseudo-element |
β | ::slotted(span) |
Vendor-specific pseudo-elements | β | ::-webkit-input-placeholder |
:nth-child(An+B of S) |
β | :nth-child(2n of .important) |
:nth-col() |
β | :nth-col(2n+1) |
:nth-last-col() |
β | :nth-last-col(2n+1) |
Attribute namespace wildcards | β | [*\|attr=value] |
Add selector
to your list of dependencies in mix.exs
:
def deps do
[
{:selector, "~> 0.1.0"}
]
end
Parse CSS selectors into an AST:
# Simple tag selector
Selector.parse("div")
# => [[{:rule, [{:tag_name, "div", []}], []}]]
# ID selector
Selector.parse("#header")
# => [[{:rule, [{:id, "header"}], []}]]
# Class selector
Selector.parse(".button")
# => [[{:rule, [{:class, "button"}], []}]]
# Multiple selectors
Selector.parse("div, .button")
# => [
# [{:rule, [{:tag_name, "div", []}], []}],
# [{:rule, [{:class, "button"}], []}]
# ]
# Combined selectors
Selector.parse("div#main.container")
# => [[{:rule, [{:tag_name, "div", []}, {:id, "main"}, {:class, "container"}], []}]]
# Attribute selectors
Selector.parse("input[type='text']")
# => [[{:rule, [{:tag_name, "input", []}, {:attribute, {:equal, "type", "text", []}}], []}]]
# Pseudo-classes
Selector.parse("a:hover")
# => [[{:rule, [{:tag_name, "a", []}, {:pseudo_class, {"hover", []}}], []}]]
# Pseudo-elements
Selector.parse("p::first-line")
# => [[{:rule, [{:tag_name, "p", []}, {:pseudo_element, {"first-line", []}}], []}]]
Namespaces are useful when working with XML documents or SVG elements within HTML:
# Element with namespace prefix
Selector.parse("svg|rect")
# => [[{:rule, [{:tag_name, "rect", namespace: "svg"}], []}]]
# Any namespace (wildcard)
Selector.parse("*|circle")
# => [[{:rule, [{:tag_name, "circle", namespace: "*"}], []}]]
# No namespace (elements without namespace)
Selector.parse("|path")
# => [[{:rule, [{:tag_name, "path", namespace: ""}], []}]]
# Default namespace with universal selector
Selector.parse("*|*")
# => [[{:rule, [{:tag_name, "*", namespace: "*"}], []}]]
# Namespace in attribute selectors
Selector.parse("[xlink|href]")
# => [[{:rule, [{:attribute, {:exists, "href", nil, namespace: "xlink"}}], []}]]
# Namespace with attribute value
Selector.parse("[xml|lang='en']")
# => [[{:rule, [{:attribute, {:equal, "lang", "en", namespace: "xml"}}], []}]]
# Complex example with SVG
Selector.parse("svg|svg > svg|g svg|rect.highlight")
# => [[
# {:rule, [{:tag_name, "svg", namespace: "svg"}], []},
# {:rule, [{:tag_name, "g", namespace: "svg"}], combinator: ">"},
# {:rule, [{:tag_name, "rect", namespace: "svg"}, {:class, "highlight"}], []}
# ]]
# MathML namespace example
Selector.parse("math|mrow > math|mi + math|mo")
# => [[
# {:rule, [{:tag_name, "mrow", namespace: "math"}], []},
# {:rule, [{:tag_name, "mi", namespace: "math"}], combinator: ">"},
# {:rule, [{:tag_name, "mo", namespace: "math"}], combinator: "+"}
# ]]
# Descendant combinator (space)
Selector.parse("article p")
# => [[
# {:rule, [{:tag_name, "article", []}], []},
# {:rule, [{:tag_name, "p", []}], []}
# ]]
# Child combinator (>)
Selector.parse("ul > li")
# => [[
# {:rule, [{:tag_name, "ul", []}], []},
# {:rule, [{:tag_name, "li", []}], combinator: ">"}
# ]]
# Adjacent sibling combinator (+)
Selector.parse("h1 + p")
# => [[
# {:rule, [{:tag_name, "h1", []}], []},
# {:rule, [{:tag_name, "p", []}], combinator: "+"}
# ]]
# General sibling combinator (~)
Selector.parse("h1 ~ p")
# => [[
# {:rule, [{:tag_name, "h1", []}], []},
# {:rule, [{:tag_name, "p", []}], combinator: "~"}
# ]]
# Column combinator (||) - CSS Level 4
Selector.parse("col || td")
# => [[
# {:rule, [{:tag_name, "col", []}], []},
# {:rule, [{:tag_name, "td", []}], combinator: "||"}
# ]]
# Existence
Selector.parse("[disabled]")
# => [[{:rule, [{:attribute, {:exists, "disabled", nil, []}}], []}]]
# Exact match
Selector.parse("[type=submit]")
# => [[{:rule, [{:attribute, {:equal, "type", "submit", []}}], []}]]
# Whitespace-separated list contains
Selector.parse("[class~=primary]")
# => [[{:rule, [{:attribute, {:includes, "class", "primary", []}}], []}]]
# Dash-separated list starts with
Selector.parse("[lang|=en]")
# => [[{:rule, [{:attribute, {:dash_match, "lang", "en", []}}], []}]]
# Starts with
Selector.parse("[href^='https://']")
# => [[{:rule, [{:attribute, {:prefix, "href", "https://", []}}], []}]]
# Ends with
Selector.parse("[src$='.png']")
# => [[{:rule, [{:attribute, {:suffix, "src", ".png", []}}], []}]]
# Contains substring
Selector.parse("[title*='important']")
# => [[{:rule, [{:attribute, {:substring, "title", "important", []}}], []}]]
# Case-insensitive matching (CSS Level 4)
Selector.parse("[type=email i]")
# => [[{:rule, [{:attribute, {:equal, "type", "email", case_sensitive: false}}], []}]]
# Case-sensitive matching (CSS Level 4)
Selector.parse("[class=Button s]")
# => [[{:rule, [{:attribute, {:equal, "class", "Button", case_sensitive: true}}], []}]]
# Simple pseudo-classes
Selector.parse(":hover")
# => [[{:rule, [{:pseudo_class, {"hover", []}}], []}]]
# Structural pseudo-classes
Selector.parse(":first-child")
# => [[{:rule, [{:pseudo_class, {"first-child", []}}], []}]]
# :nth-child with various formulas
Selector.parse(":nth-child(2n+1)")
# => [[{:rule, [{:pseudo_class, {"nth-child", [[a: 2, b: 1]]}}], []}]]
Selector.parse(":nth-child(odd)")
# => [[{:rule, [{:pseudo_class, {"nth-child", [[a: 2, b: 1]]}}], []}]]
Selector.parse(":nth-child(even)")
# => [[{:rule, [{:pseudo_class, {"nth-child", [[a: 2, b: 0]]}}], []}]]
Selector.parse(":nth-child(5)")
# => [[{:rule, [{:pseudo_class, {"nth-child", [[a: 0, b: 5]]}}], []}]]
# Language pseudo-class
Selector.parse(":lang(en-US)")
# => [[{:rule, [{:pseudo_class, {"lang", ["en-US"]}}], []}]]
# Negation pseudo-class
Selector.parse(":not(.disabled)")
# => [[{:rule, [{:pseudo_class, {"not", [
# [[{:rule, [{:class, "disabled"}], []}]]
# ]}}], []}]]
# CSS Level 4 pseudo-classes
Selector.parse(":is(h1, h2, h3)")
# => [[{:rule, [{:pseudo_class, {"is", [
# [
# [{:rule, [{:tag_name, "h1", []}], []}],
# [{:rule, [{:tag_name, "h2", []}], []}],
# [{:rule, [{:tag_name, "h3", []}], []}]
# ]
# ]}}], []}]]
Selector.parse(":where(article, section) > p")
# => [[
# {:rule, [{:pseudo_class, {"where", [
# [
# [{:rule, [{:tag_name, "article", []}], []}],
# [{:rule, [{:tag_name, "section", []}], []}]
# ]
# ]}}], []},
# {:rule, [{:tag_name, "p", []}], combinator: ">"}
# ]]
Selector.parse(":has(> img)")
# => [[{:rule, [{:pseudo_class, {"has", [
# [[{:rule, [{:tag_name, "img", []}], combinator: ">"}]]
# ]}}], []}]]
# Standard pseudo-elements
Selector.parse("::before")
# => [[{:rule, [{:pseudo_element, {"before", []}}], []}]]
Selector.parse("::after")
# => [[{:rule, [{:pseudo_element, {"after", []}}], []}]]
Selector.parse("::first-line")
# => [[{:rule, [{:pseudo_element, {"first-line", []}}], []}]]
Selector.parse("::first-letter")
# => [[{:rule, [{:pseudo_element, {"first-letter", []}}], []}]]
# CSS Level 4 pseudo-elements
Selector.parse("::placeholder")
# => [[{:rule, [{:pseudo_element, {"placeholder", []}}], []}]]
Selector.parse("::selection")
# => [[{:rule, [{:pseudo_element, {"selection", []}}], []}]]
# Pseudo-elements with parameters
Selector.parse("::slotted(span)")
# => [[{:rule, [{:pseudo_element, {"slotted", [[[{:rule, [{:tag_name, "span", []}], []}]]]}}], []}]]
# Legacy single-colon syntax (still supported)
Selector.parse(":before")
# => [[{:rule, [{:pseudo_element, {"before", []}}], []}]]
# Vendor-specific pseudo-elements
Selector.parse("::-webkit-input-placeholder")
# => [[{:rule, [{:pseudo_element, {"-webkit-input-placeholder", []}}], []}]]
# Complex selector with multiple features
Selector.parse("article.post:not(.draft) > h1 + p:first-of-type")
# => [
# {:rule, [
# {:tag_name, "article", []},
# {:class, "post"},
# {:pseudo_class, {:not, [[{:rule, [{:class, "draft"}], []}]]}}
# ], []},
# {:rule, [{:tag_name, "h1", []}], combinator: ">"},
# {:rule, [
# {:tag_name, "p", []},
# {:pseudo_class, {:first_of_type, []}}
# ], combinator: "+"}
# ]
# Multiple attribute selectors
Selector.parse("input[type='email'][required][placeholder^='Enter']")
# => [{:rule, [
# {:tag_name, "input", []},
# {:attribute, {:equal, "type", "email", []}},
# {:attribute, {:exists, "required", nil, []}},
# {:attribute, {:prefix, "placeholder", "Enter", []}}
# ], []}]
# Nested pseudo-classes
Selector.parse(":not(:first-child):not(:last-child)")
# => [{:rule, [
# {:pseudo_class, {:not, [[{:rule, [{:pseudo_class, {:first_child, []}}], []}]]}},
# {:pseudo_class, {:not, [[{:rule, [{:pseudo_class, {:last_child, []}}], []}]]}}
# ], []}]
ast = Selector.parse("div#main > p.text")
Selector.render(ast)
# => "div#main > p.text"
# Strict mode (default: true)
# Disables identifiers starting with double hyphens
Selector.parse("#--custom-id", strict: false)
# => [{:rule, [{:id, "--custom-id"}], []}]
# Custom syntax options
# Limit which CSS features are allowed
Selector.parse("div:hover", syntax: %{tag: true})
# Raises ArgumentError: "Pseudo-classes are not enabled."
The parser generates an AST with the following structure:
- Each selector is wrapped in a
{:rule, selectors, options}
tuple - Multiple selectors are returned as a list of rules
- Combinators are stored in the options of the following rule
{:tag_name, "div", []}
- Element selector{:tag_name, "div", namespace: "svg"}
- Namespaced element{:id, "header"}
- ID selector{:class, "button"}
- Class selector{:attribute, {operation, name, value, options}}
- Attribute selector{:pseudo_class, {name, arguments}}
- Pseudo-class{:pseudo_element, {name, arguments}}
- Pseudo-element
:exists
-[attr]
:equal
-[attr=value]
:includes
-[attr~=value]
:dash_match
-[attr|=value]
:prefix
-[attr^=value]
:suffix
-[attr$=value]
:substring
-[attr*=value]
The parser raises ArgumentError
for invalid selectors:
try do
Selector.parse(".")
rescue
ArgumentError -> "Invalid selector"
end
# => "Invalid selector"
MIT License - Copyright (c) 2024 DockYard, Inc. See LICENSE.md for details.