Querying HTML
View SourcePureHTML provides a CSS selector-based querying API for finding and traversing HTML nodes. The API is designed to be familiar to users of Floki.
Basic Usage
# Parse HTML and query for elements
html = PureHTML.parse("""
<div class="container">
<h1 id="title">Welcome</h1>
<p class="intro">Hello, world!</p>
<p>Another paragraph</p>
</div>
""")
# Find all paragraphs
PureHTML.query(html, "p")
#=> [{"p", [{"class", "intro"}], ["Hello, world!"]}, {"p", [], ["Another paragraph"]}]
# Find by class
PureHTML.query(html, ".intro")
#=> [{"p", [{"class", "intro"}], ["Hello, world!"]}]
# Find by ID
PureHTML.query(html, "#title")
#=> [{"h1", [{"id", "title"}], ["Welcome"]}]Supported Selectors
Tag Selector
Match elements by tag name.
PureHTML.query(html, "div") # All <div> elements
PureHTML.query(html, "p") # All <p> elements
PureHTML.query(html, "a") # All <a> elementsUniversal Selector
Match all elements.
PureHTML.query(html, "*") # All elementsClass Selector
Match elements by class name. Multiple classes can be specified.
PureHTML.query(html, ".intro") # Elements with class "intro"
PureHTML.query(html, ".btn.primary") # Elements with both "btn" AND "primary" classesID Selector
Match elements by ID.
PureHTML.query(html, "#title") # Element with id="title"
PureHTML.query(html, "#nav") # Element with id="nav"Attribute Selectors
Match elements by attribute presence or value.
# Attribute existence
PureHTML.query(html, "[href]") # Elements with href attribute
# Exact match
PureHTML.query(html, "[type=text]") # Elements where type="text"
PureHTML.query(html, "[type='text']") # Same, with quotes
# Prefix match (^=)
PureHTML.query(html, "[href^=https]") # href starts with "https"
# Suffix match ($=)
PureHTML.query(html, "[href$=.pdf]") # href ends with ".pdf"
# Substring match (*=)
PureHTML.query(html, "[href*=example]") # href contains "example"Compound Selectors
Combine multiple selectors to match elements that satisfy all conditions.
PureHTML.query(html, "p.intro") # <p> elements with class "intro"
PureHTML.query(html, "input#email") # <input> with id="email"
PureHTML.query(html, "a.btn.primary") # <a> with both classes
PureHTML.query(html, "input[type=text]") # <input> with type="text"
PureHTML.query(html, "a.external[href^=https]") # <a> with class and attributeSelector Lists
Match elements that satisfy any of the selectors (OR logic).
PureHTML.query(html, "h1, h2, h3") # All h1, h2, or h3 elements
PureHTML.query(html, ".error, .warning") # Elements with either class
PureHTML.query(html, "input, textarea") # All input or textarea elementsCombinators
Combinators express relationships between elements.
html = PureHTML.parse("""
<article>
<section>
<h2>Title</h2>
<p class="intro">First paragraph</p>
<p>Second paragraph</p>
</section>
</article>
""")
# Descendant combinator (space) - matches anywhere inside
PureHTML.query(html, "article p")
#=> [{"p", [{"class", "intro"}], ["First paragraph"]}, {"p", [], ["Second paragraph"]}]
# Child combinator (>) - matches direct children only
PureHTML.query(html, "article > p")
#=> [] # No <p> directly inside <article>
PureHTML.query(html, "section > p")
#=> [{"p", [{"class", "intro"}], ["First paragraph"]}, {"p", [], ["Second paragraph"]}]
# Adjacent sibling (+) - matches the next sibling
PureHTML.query(html, "h2 + p")
#=> [{"p", [{"class", "intro"}], ["First paragraph"]}]
# General sibling (~) - matches any following sibling
PureHTML.query(html, "h2 ~ p")
#=> [{"p", [{"class", "intro"}], ["First paragraph"]}, {"p", [], ["Second paragraph"]}]Combinators can be chained:
# Find links inside list items inside nav
PureHTML.query(html, "nav ul li a")
# Find paragraphs that are direct children of articles
PureHTML.query(html, "article > section > p")
# Mix combinators as needed
PureHTML.query(html, "article section > h2 + p")Traversing Children
Use children/2 to get the immediate children of an element.
html = PureHTML.parse("<ul><li>One</li><li>Two</li></ul>")
[ul] = PureHTML.query(html, "ul")
PureHTML.children(ul)
#=> [{"li", [], ["One"]}, {"li", [], ["Two"]}]Filtering Text Nodes
By default, children/2 includes text nodes. Use include_text: false to exclude them.
html = PureHTML.parse("<div><p>Hello</p> Some text <span>World</span></div>")
[div] = PureHTML.query(html, "div")
# Include text nodes (default)
PureHTML.children(div)
#=> [{"p", [], ["Hello"]}, " Some text ", {"span", [], ["World"]}]
# Exclude text nodes
PureHTML.children(div, include_text: false)
#=> [{"p", [], ["Hello"]}, {"span", [], ["World"]}]Non-Element Nodes
children/2 returns nil for non-element nodes like text or comments.
PureHTML.children("text node") #=> nil
PureHTML.children({:comment, "comment"}) #=> nilWorking with Results
Query results are lists of nodes in the same format as PureHTML.parse/1 output.
Chaining Queries
You can query within query results.
html = PureHTML.parse("""
<div class="sidebar">
<a href="/home">Home</a>
</div>
<div class="content">
<a href="/about">About</a>
<a href="/contact">Contact</a>
</div>
""")
# Find links only within .content
[content] = PureHTML.query(html, ".content")
PureHTML.query(content, "a")
#=> [{"a", [{"href", "/about"}], ["About"]}, {"a", [{"href", "/contact"}], ["Contact"]}]Extracting Text
Use text/2 to extract text content from nodes.
html = PureHTML.parse("<p>Hello <strong>World</strong>!</p>")
# Extract all text (deep by default)
PureHTML.text(html)
#=> "Hello World!"
# With a separator between text segments
PureHTML.text(html, separator: " ")
#=> "Hello World !"Text Options
html = PureHTML.parse("""
<div>
<p> Paragraph one </p>
<p> Paragraph two </p>
</div>
""")
# Strip whitespace and remove empty segments
PureHTML.text(html, strip: true, separator: ", ")
#=> "Paragraph one, Paragraph two"
# Shallow extraction (direct text children only)
html = PureHTML.parse("<div>Direct <span>nested</span> text</div>")
[div] = PureHTML.query(html, "div")
PureHTML.text(div, deep: false)
#=> "Direct text"Script and Style Content
By default, <script> and <style> content is excluded:
html = PureHTML.parse("<div>Hello<script>alert('x')</script></div>")
PureHTML.text(html)
#=> "Hello"
# Include script content
PureHTML.text(html, include_script: true)
#=> "Helloalert('x')"Form Input Values
Extract values from form inputs:
html = PureHTML.parse("<input type='text' value='Hello'>")
PureHTML.text(html, include_inputs: true)
#=> "Hello"Extracting Attributes
Single Node
Use attr/2 to get an attribute from a single node:
html = PureHTML.parse("<a href='/home' class='nav-link'>Home</a>")
[link] = PureHTML.query(html, "a")
PureHTML.attr(link, "href")
#=> "/home"
PureHTML.attr(link, "title")
#=> nilMultiple Nodes
Use attribute/2 to extract an attribute from a list of nodes:
html = PureHTML.parse("""
<nav>
<a href="/home">Home</a>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</nav>
""")
html
|> PureHTML.query("a")
|> PureHTML.attribute("href")
#=> ["/home", "/about", "/contact"]Query and Extract
Use attribute/3 to query and extract in one step:
html
|> PureHTML.attribute("a", "href")
#=> ["/home", "/about", "/contact"]
# Equivalent to:
html
|> PureHTML.query("a")
|> PureHTML.attribute("href")Converting Back to HTML
Use to_html/1 to convert query results back to HTML strings.
html = PureHTML.parse("<div><p class='keep'>Keep</p><p>Remove</p></div>")
html
|> PureHTML.query(".keep")
|> PureHTML.to_html()
#=> "<p class=keep>Keep</p>"Selectors Not Yet Supported
Pseudo-classes such as :first-child, :last-child, :nth-child(n), and :not(selector) are on the roadmap. A selector using one is invalid today and matches nothing. Pseudo-elements (::before, ::after) do not apply to a static tree.
API Reference
PureHTML.query/2
@spec query(html_tree | html_node, String.t()) :: [html_node]Finds all nodes matching the CSS selector, in document order. Returns an empty list if no matches. An invalid selector represents, and therefore matches, nothing, so it also returns an empty list; PureHTML.query!/2 raises ArgumentError instead.
PureHTML.query_one/2
@spec query_one(html_tree | html_node, String.t()) :: html_node | nilFinds the first node matching the CSS selector, in document order, and stops there. Returns nil if no match, and nil for an invalid selector; PureHTML.query_one!/2 raises ArgumentError instead.
PureHTML.text/2
@spec text(html_tree | html_node, keyword()) :: String.t()Extracts text content from an HTML tree or node. Options:
:deep- Traverse all descendants (default:true):separator- String between text segments (default:""):strip- Strip whitespace from segments, remove empty (default:false):include_script- Include<script>content (default:false):include_style- Include<style>content (default:false):include_inputs- Include<input>and<textarea>values (default:false)
PureHTML.attr/2
@spec attr(html_node, String.t()) :: String.t() | nilGets an attribute value from a single node. Returns nil if not found.
PureHTML.attribute/2
@spec attribute(html_tree | html_node, String.t()) :: [String.t()]Extracts an attribute from a list of nodes. Skips nodes without the attribute.
PureHTML.attribute/3
@spec attribute(html_tree | html_node, String.t(), String.t()) :: [String.t()]Finds elements matching a selector and extracts an attribute. Combines query/2 and attribute/2.
PureHTML.children/2
@spec children(html_node, keyword()) :: [html_node] | nilReturns the immediate children of a node. Options:
:include_text- Include text nodes (default:true)
Returns nil for non-element nodes.