Skip to content

liveview-native/selector

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

12 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🎯 Selector

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.

✨ Features

  • 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

🎨 CSS Compatibility

CSS Selectors Level 1

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

CSS Selectors Level 2

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 *

CSS Selectors Level 3

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

CSS Selectors Level 4

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]

πŸ“¦ Installation

Add selector to your list of dependencies in mix.exs:

def deps do
  [
    {:selector, "~> 0.1.0"}
  ]
end

πŸš€ Usage

πŸ“ Basic Parsing

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"}], []}]
#    ]

πŸ”§ Complex Selectors

# 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

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: "+"}
#    ]]

πŸ”— Combinators

# 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: "||"}
#    ]]

🏷️ Attribute Selectors

# 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}}], []}]]

🎭 Pseudo-classes

# 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: ">"}]]
#      ]}}], []}]]

🎨 Pseudo-elements

# 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", []}}], []}]]

πŸ’ͺ Advanced Examples

# 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, []}}], []}]]}}
# ], []}]

πŸ”„ Rendering AST back to CSS

ast = Selector.parse("div#main > p.text")
Selector.render(ast)
# => "div#main > p.text"

βš™οΈ Parser Options

# 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."

🌳 AST Structure

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

🎯 Selector Types

  • {: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

πŸ”§ Attribute Operations

  • :exists - [attr]
  • :equal - [attr=value]
  • :includes - [attr~=value]
  • :dash_match - [attr|=value]
  • :prefix - [attr^=value]
  • :suffix - [attr$=value]
  • :substring - [attr*=value]

⚠️ Error Handling

The parser raises ArgumentError for invalid selectors:

try do
  Selector.parse(".")
rescue
  ArgumentError -> "Invalid selector"
end
# => "Invalid selector"

πŸ“„ License

MIT License - Copyright (c) 2024 DockYard, Inc. See LICENSE.md for details.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages