Building blocks for working with HTML in Phoenix - provides HTML safety mechanisms, form abstractions, and JavaScript enhancements
Phoenix.HTML's security foundation provides comprehensive protection against XSS attacks through automatic HTML escaping, safe content marking, and proper attribute handling. All user data is considered unsafe by default and must be explicitly marked as safe.
Marks content as safe HTML that should not be escaped, allowing raw HTML to be rendered directly in templates.
def raw(content) :: Phoenix.HTML.safe
# Parameters:
# - content: iodata | Phoenix.HTML.safe | nil - Content to mark as safe
#
# Returns:
# - Phoenix.HTML.safe - Safe content tuple {:safe, iodata}Usage Examples:
# Mark HTML string as safe
safe_html = raw("<p>Welcome <strong>User</strong></p>")
# Returns: {:safe, "<p>Welcome <strong>User</strong></p>"}
# Safe content passes through unchanged
already_safe = raw({:safe, "<div>Already safe</div>"})
# Returns: {:safe, "<div>Already safe</div>"}
# Nil becomes empty safe content
empty = raw(nil)
# Returns: {:safe, ""}Escapes HTML entities in content to prevent XSS attacks, converting potentially dangerous characters to their safe HTML entity equivalents.
def html_escape(content) :: Phoenix.HTML.safe
# Parameters:
# - content: Phoenix.HTML.unsafe - Content that may contain HTML entities
#
# Returns:
# - Phoenix.HTML.safe - Escaped content as safe iodataUsage Examples:
# Escape user input
user_input = "<script>alert('XSS')</script>"
safe_output = html_escape(user_input)
# Returns: {:safe, [[[] | "<"], "script", [] | ">", "alert(", ...]}
# Safe content passes through
already_safe = html_escape({:safe, "<p>Safe content</p>"})
# Returns: {:safe, "<p>Safe content</p>"}
# Numbers and other data types are converted
number_escaped = html_escape(123)
# Returns: {:safe, "123"}Converts safe iodata to regular strings, ensuring the content was properly marked as safe before conversion.
def safe_to_string(safe_content) :: String.t()
# Parameters:
# - safe_content: Phoenix.HTML.safe - Content marked as safe
#
# Returns:
# - String.t() - Regular string representation
#
# Raises:
# - If content is not marked as safeUsage Examples:
# Convert safe iodata to string
safe_content = {:safe, ["<p>", "Hello", "</p>"]}
result = safe_to_string(safe_content)
# Returns: "<p>Hello</p>"
# Use with html_escape for complete safety
user_data = "<script>alert('XSS')</script>"
safe_string = user_data |> html_escape() |> safe_to_string()
# Returns: "<script>alert('XSS')</script>"Escapes HTML attributes with special handling for common attribute patterns, supporting nested data structures and boolean attributes.
def attributes_escape(attrs) :: Phoenix.HTML.safe
# Parameters:
# - attrs: list | map - Enumerable of HTML attributes
#
# Returns:
# - Phoenix.HTML.safe - Escaped attributes as iodataSpecial Attribute Behaviors:
:class: Accepts list of classes, filters out nil and false values:data, :aria, :phx: Accepts keyword lists, converts to dash-separated attributes:id: Validates that numeric IDs are not used (raises ArgumentError)true values render as bare attributes, false/nil are omitted:phx_value_id → phx-value-id)Usage Examples:
# Basic attributes
attrs = [title: "Click me", id: "my-button", disabled: true]
escaped = attributes_escape(attrs)
# Returns: {:safe, [" title=\"Click me\" id=\"my-button\" disabled"]}
# Class list handling
attrs = [class: ["btn", "btn-primary", nil, "active"]]
escaped = attributes_escape(attrs)
# Returns: {:safe, [" class=\"btn btn-primary active\""]}
# Data attributes
attrs = [data: [confirm: "Are you sure?", method: "delete"]]
escaped = attributes_escape(attrs)
# Returns: {:safe, [" data-confirm=\"Are you sure?\" data-method=\"delete\""]}
# Phoenix-specific attributes
attrs = [phx: [value: [user_id: 123]]]
escaped = attributes_escape(attrs)
# Returns: {:safe, [" phx-value-user-id=\"123\""]}
# Combined usage
attrs = [
class: ["btn", "btn-danger"],
data: [confirm: "Delete user?"],
phx: [click: "delete_user"]
]
escaped_str = attributes_escape(attrs) |> safe_to_string()
# Returns: " class=\"btn btn-danger\" data-confirm=\"Delete user?\" phx-click=\"delete_user\""Escapes HTML content for safe inclusion in JavaScript strings, handling special characters that could break JavaScript syntax or enable XSS attacks.
def javascript_escape(content) :: binary | Phoenix.HTML.safe
# Parameters:
# - content: binary | Phoenix.HTML.safe - Content to escape for JavaScript
#
# Returns:
# - binary - Escaped string (for binary input)
# - Phoenix.HTML.safe - Escaped safe content (for safe input)Escaped Characters:
" → \", ' → \'\ → \\\n, \r, \r\n → \n</ → <\/\u2028 → \\u2028, \u2029 → \\u2029\u0000 → \\u0000` → `````Usage Examples:
# Escape user content for JavaScript
user_content = ~s(<script>alert("XSS")</script>)
escaped = javascript_escape(user_content)
# Returns: "<\\/script>alert(\\\"XSS\\\")<\\/script>"
# Use in template JavaScript
html_content = render("user_profile.html", user: @user)
javascript_code = """
$("#container").html("#{javascript_escape(html_content)}");
"""
# Safe content handling
safe_content = {:safe, ~s(<div class="user">'John'</div>)}
escaped_safe = javascript_escape(safe_content)
# Returns: {:safe, "<div class=\\\"user\\\">\\'John\\'</div>"}Escapes strings for safe use as CSS identifiers, following CSS specification for character escaping in selectors and property values.
def css_escape(value) :: String.t()
# Parameters:
# - value: String.t() - String to escape for CSS usage
#
# Returns:
# - String.t() - CSS-safe identifier stringUsage Examples:
# Escape problematic CSS characters
css_class = css_escape("user-123 name")
# Returns: "user-123\\ name"
# Handle numeric prefixes
css_id = css_escape("123-user")
# Returns: "\\31 23-user"
# Use in dynamic CSS generation
user_id = "user@domain.com"
safe_selector = "##{css_escape(user_id)}"
# Returns: "#user\\@domain\\.com"Core functions from the Phoenix.HTML.Engine module that handle template processing and HTML safety in EEx templates.
Converts various content types to HTML-safe iodata for use in template rendering.
def encode_to_iodata!(content) :: iodata
# Parameters:
# - content: term - Content to encode (safe tuples, binaries, lists, etc.)
#
# Returns:
# - iodata - HTML-safe iodata representationContent Type Handling:
{:safe, body}: Returns the body directly without encodingnil or "": Returns empty stringPhoenix.HTML.Safe.List.to_iodata/1Phoenix.HTML.Safe.to_iodata/1Usage Examples:
# Safe content passes through
Phoenix.HTML.Engine.encode_to_iodata!({:safe, "<p>Safe content</p>"})
# Returns: "<p>Safe content</p>"
# Binaries are HTML-escaped
Phoenix.HTML.Engine.encode_to_iodata!("<script>alert('XSS')</script>")
# Returns: HTML-escaped iodata
# Empty values become empty strings
Phoenix.HTML.Engine.encode_to_iodata!(nil)
# Returns: ""
Phoenix.HTML.Engine.encode_to_iodata!("")
# Returns: ""
# Lists are processed via Safe protocol
Phoenix.HTML.Engine.encode_to_iodata!(["<p>", "content", "</p>"])
# Returns: HTML-safe iodata
# Other types use Safe protocol
Phoenix.HTML.Engine.encode_to_iodata!(123)
# Returns: "123"
Phoenix.HTML.Engine.encode_to_iodata!(:hello)
# Returns: "hello" (HTML-escaped)Performs direct HTML escaping on binary strings with optimized performance.
def html_escape(binary) :: iodata
# Parameters:
# - binary: binary - String to HTML-escape
#
# Returns:
# - iodata - HTML-escaped iodata structureEscaped Characters:
< → <> → >& → &" → "' → 'Usage Examples:
# Basic HTML escaping
Phoenix.HTML.Engine.html_escape("<script>alert('XSS')</script>")
# Returns: HTML-escaped iodata
# Preserves non-HTML content
Phoenix.HTML.Engine.html_escape("Hello World")
# Returns: "Hello World"
# Handles quotes and ampersands
Phoenix.HTML.Engine.html_escape(~s(The "quick" & 'brown' fox))
# Returns: HTML-escaped iodata with quotes and ampersand escapedFetches template assigns with comprehensive error handling and debugging information.
def fetch_assign!(assigns, key) :: term
# Parameters:
# - assigns: map - Template assigns map
# - key: atom - Assign key to fetch
#
# Returns:
# - term - The assign value
#
# Raises:
# - ArgumentError - With detailed error message if assign not foundUsage Examples:
# Successful assign access
assigns = %{user: %{name: "John"}, title: "Welcome"}
user = Phoenix.HTML.Engine.fetch_assign!(assigns, :user)
# Returns: %{name: "John"}
# Missing assign raises informative error
Phoenix.HTML.Engine.fetch_assign!(assigns, :missing)
# Raises: ArgumentError with message:
# "assign @missing not available in template.
# Available assigns: [:user, :title]"The Phoenix.HTML.Safe protocol defines how different data types are converted to HTML-safe iodata. This protocol is automatically implemented for common Elixir types.
defprotocol Phoenix.HTML.Safe do
@spec to_iodata(t) :: iodata
def to_iodata(data)
endAll implementations ensure data is properly escaped for HTML context:
# Atom implementation - converts to escaped string
defimpl Phoenix.HTML.Safe, for: Atom do
def to_iodata(nil) :: ""
def to_iodata(atom) :: iodata # HTML-escaped string conversion
end
# BitString implementation - HTML escapes binary content
defimpl Phoenix.HTML.Safe, for: BitString do
def to_iodata(binary) :: iodata # HTML-escaped content
end
# Date/Time implementations - ISO8601 conversion
defimpl Phoenix.HTML.Safe, for: Date do
def to_iodata(date) :: binary # ISO8601 date string
end
defimpl Phoenix.HTML.Safe, for: DateTime do
def to_iodata(datetime) :: iodata # HTML-escaped ISO8601 string
end
# Numeric implementations - string conversion
defimpl Phoenix.HTML.Safe, for: Integer do
def to_iodata(integer) :: binary # String representation
end
# List implementation - recursive HTML escaping
defimpl Phoenix.HTML.Safe, for: List do
def to_iodata(list) :: iodata # Recursively escaped list content
end
# Tuple implementation - handles {:safe, content} tuples
defimpl Phoenix.HTML.Safe, for: Tuple do
def to_iodata({:safe, data}) :: iodata # Extracts safe data
def to_iodata(other) :: no_return # Raises Protocol.UndefinedError
endCustom Protocol Implementation:
# Example: Custom struct implementation
defmodule User do
defstruct [:name, :email]
end
defimpl Phoenix.HTML.Safe, for: User do
def to_iodata(%User{name: name, email: email}) do
Phoenix.HTML.Engine.html_escape("#{name} (#{email})")
end
end
# Usage
user = %User{name: "John <script>", email: "john@example.com"}
safe_output = Phoenix.HTML.Safe.to_iodata(user)
# Returns HTML-escaped: "John <script> (john@example.com)"# ArgumentError: Numeric ID values
attributes_escape([id: 123])
# Raises: "attempting to set id attribute to 123, but setting the DOM ID to a number..."
# ArgumentError: Invalid list content in templates
Phoenix.HTML.Safe.to_iodata([1000]) # Integer > 255 in list
# Raises: "lists in Phoenix.HTML templates only support iodata..."
# Protocol.UndefinedError: Unsupported tuple format
Phoenix.HTML.Safe.to_iodata({:unsafe, "content"})
# Raises: Protocol.UndefinedError for Phoenix.HTML.Safe protocolraw/1 with user-provided contentjavascript_escape/1 for content inserted into JavaScript stringscss_escape/1 for dynamic CSS identifier generationInstall with Tessl CLI
npx tessl i tessl/hex-phoenix-html