philiprehberger-html_builder

Programmatic HTML builder with tag DSL, auto-escaping, form helpers, components, and output formatting
Requirements
- Ruby >= 3.1
Installation
Add to your Gemfile:
gem "philiprehberger-html_builder"
Or install directly:
gem install philiprehberger-html_builder
Usage
require "philiprehberger/html_builder"
html = Philiprehberger::HtmlBuilder.build do
div(class: 'card') do
h1 'Title'
p 'Content'
end
end
# => '<div class="card"><h1>Title</h1><p>Content</p></div>'
Auto-Escaping
Text content and attribute values are automatically escaped:
Philiprehberger::HtmlBuilder.build { p '<script>alert("xss")</script>' }
# => '<p><script>alert("xss")</script></p>'
Void Elements
Self-closing elements like br, hr, img, input, meta, and link render without closing tags:
Philiprehberger::HtmlBuilder.build do
img(src: 'photo.jpg', alt: 'Photo')
br
input(type: 'text', name: 'email')
end
# => '<img src="photo.jpg" alt="Photo"><br><input type="text" name="email">'
Attributes
Pass attributes as keyword arguments to any tag:
Philiprehberger::HtmlBuilder.build do
a(href: '/about', class: 'nav-link') { text 'About' }
input(type: 'checkbox', checked: true, disabled: false)
end
Data and Aria Attributes
Use hash syntax for HTML5 data-* and aria-* attributes:
Philiprehberger::HtmlBuilder.build do
div(data: { id: 1, action: 'click' }, aria: { label: 'Panel' }) do
('Toggle', aria: { expanded: 'false' })
end
end
# => '<div data-id="1" data-action="click" aria-label="Panel"><button aria-expanded="false">Toggle</button></div>'
Raw HTML
Insert pre-rendered HTML without escaping:
Philiprehberger::HtmlBuilder.build do
div { raw '<em>pre-rendered</em>' }
end
Form Builder Helpers
Streamlined helpers for building forms with automatic label generation:
Philiprehberger::HtmlBuilder.build do
form_for('/signup', class: 'form') do
field(:email, type: 'email')
field(:first_name)
select_field(:country, [%w[USA us], %w[Canada ca]], selected: 'us')
textarea_field(:bio, rows: '5')
submit('Sign Up', class: 'btn')
end
end
The field helper generates a <label> and <input> pair. The select_field helper generates a <label> and <select> with <option> tags. The textarea_field helper generates a <label> and <textarea>. Label text is auto-generated from the field name (underscores become spaces, words are capitalized).
Hidden Fields
Generate hidden input elements for tokens, CSRF fields, or any non-visible form data:
Philiprehberger::HtmlBuilder.build do
form_for('/update') do
hidden_field(:csrf_token, 'abc123')
hidden_field(:action, 'save')
field(:title)
submit
end
end
# hidden_field produces: <input type="hidden" name="csrf_token" value="abc123">
Submit Buttons
Generate submit buttons with optional text and attributes:
Philiprehberger::HtmlBuilder.build do
form_for('/login') do
field(:username)
submit # => <button type="submit">Submit</button>
submit('Log In', class: 'btn-primary') # => <button type="submit" class="btn-primary">Log In</button>
end
end
Lists
Build <ul> or <ol> lists from an array of items. Items are text-escaped by default. Pass ordered: true for an ordered list. Use a block for custom rendering of each item:
Philiprehberger::HtmlBuilder.build do
list(%w[Apple Banana Cherry])
end
# => '<ul><li>Apple</li><li>Banana</li><li>Cherry</li></ul>'
Philiprehberger::HtmlBuilder.build do
list(%w[First Second], ordered: true, class: 'steps')
end
# => '<ol class="steps"><li>First</li><li>Second</li></ol>'
Philiprehberger::HtmlBuilder.build do
list(%w[Alice Bob]) { |name| strong name }
end
# => '<ul><li><strong>Alice</strong></li><li><strong>Bob</strong></li></ul>'
Attribute Helpers
Merge multiple attribute hashes — concatenating :class (single space) and :style ('; ') values rather than overwriting — and build ARIA attribute hashes from snake_case keyword pairs:
Philiprehberger::HtmlBuilder.build do
base = { class: 'btn', style: 'color: red' }
variant = { class: 'btn-primary', style: 'font-weight: bold' }
attrs = merge_attrs(base, variant)
('Save', **attrs)
end
# => '<button class="btn btn-primary" style="color: red; font-weight: bold">Save</button>'
Philiprehberger::HtmlBuilder.build do
aria(label: 'Save', expanded: false, describedby: nil)
end
# => { 'aria-label' => 'Save', 'aria-expanded' => 'false' }
merge_attrs joins :class values with a single space and :style values with '; '. Other keys follow last-write-wins, and input hashes are not mutated. aria converts snake_case keys to aria-kebab-case string keys, stringifies values, and omits keys whose value is nil.
CSS Class Helpers
Build conditional CSS class strings from mixed arguments. Strings are included as-is, hash keys are included when their value is truthy:
Philiprehberger::HtmlBuilder.build do
div(class: class_names('btn', 'btn-lg', active: true, disabled: false)) do
text 'Click me'
end
end
# => <div class="btn btn-lg active">Click me</div>
Capturing Fragments
Render a block in a separate scope and capture the HTML as a string instead of appending it to the document. Useful when the same fragment is needed in more than one place or as a value:
Philiprehberger::HtmlBuilder.build do
badge = capture { strong 'NEW', class: 'badge' }
article do
h2 'Headline'
raw badge
p 'Body text'
raw badge
end
end
# => <article><h2>Headline</h2><strong class="badge">NEW</strong><p>Body text</p><strong class="badge">NEW</strong></article>
capture inherits any components defined on the parent builder, so component reuse works inside captured fragments.
Fragment Caching
Cache rendered block results by key. On subsequent calls with the same key, the cached HTML is returned without re-executing the block:
Philiprehberger::HtmlBuilder.build do
cache(:nav) do
nav { a 'Home', href: '/' }
end
main { p 'Content' }
cache(:nav) do
nav { a 'Home', href: '/' } # block is not re-executed; cached HTML is used
end
end
Conditional Rendering
Render blocks based on conditions:
logged_in = true
admin = false
Philiprehberger::HtmlBuilder.build do
render_if(logged_in) { p 'Welcome back!' }
render_unless(admin) { p 'Standard user' }
end
# => '<p>Welcome back!</p><p>Standard user</p>'
Components
Define reusable named blocks and render them anywhere:
Philiprehberger::HtmlBuilder.build do
define_component(:card) do |locals|
div(class: 'card') do
h2 locals[:title]
p locals[:body]
end
end
use_component(:card, title: 'First', body: 'Content 1')
use_component(:card, title: 'Second', body: 'Content 2')
end
Components without parameters use a simple block with no arguments. Components with parameters receive a hash of locals.
HTML5 Documents
Emit a standards-compliant <!DOCTYPE html> declaration via the doctype DSL helper, or use HtmlBuilder.document for a full HTML5 document shortcut that prefixes the doctype automatically. The block decides the root element, so no hardcoded <html> wrapper is added:
Philiprehberger::HtmlBuilder.build do
doctype
html { head { title 'Home' } }
end
# => '<!DOCTYPE html><html><head><title>Home</title></head></html>'
Philiprehberger::HtmlBuilder.document do
html do
head { title 'Home' }
body { h1 'Welcome' }
end
end
# => "<!DOCTYPE html>\n<html><head><title>Home</title></head><body><h1>Welcome</h1></body></html>"
Philiprehberger::HtmlBuilder.document(pretty: true) do
html { head { title 'Home' } }
end
# pretty-printed with the doctype on its own line
Output Modes
Choose between minified and pretty-printed output:
# Minified (default)
Philiprehberger::HtmlBuilder.build do
div { p 'Hello' }
end
# => '<div><p>Hello</p></div>'
# Pretty-printed
Philiprehberger::HtmlBuilder.build_pretty do
div { p 'Hello' }
end
# => "<div>\n <p>Hello</p>\n</div>"
# Pretty-printed with custom indent
Philiprehberger::HtmlBuilder.build_pretty(indent_size: 4) do
div { p 'Hello' }
end
Escape Helper
Escape arbitrary strings outside the DSL using the same entity encoding:
Philiprehberger::HtmlBuilder.escape('<script>alert("xss")</script>')
# => "<script>alert("xss")</script>"
Fragment Merging
Combine multiple builder outputs into a single HTML string:
header = Philiprehberger::HtmlBuilder.build { header { h1 'Title' } }
body = Philiprehberger::HtmlBuilder.build { main { p 'Content' } }
= Philiprehberger::HtmlBuilder.build { { p 'Copyright' } }
Philiprehberger::HtmlBuilder.merge(header, body, )
# => '<header><h1>Title</h1></header><main><p>Content</p></main><footer><p>Copyright</p></footer>'
API
| Method | Description |
|---|---|
HtmlBuilder.build { ... } |
Build minified HTML using the tag DSL, returns a string |
HtmlBuilder.build_pretty { ... } |
Build pretty-printed HTML with indentation |
HtmlBuilder.build_minified { ... } |
Alias for build, explicitly produces minified output |
HtmlBuilder.document(pretty:, indent_size:) { ... } |
Build an HTML5 document; prefixes <!DOCTYPE html> before the block output |
HtmlBuilder.merge(*fragments) |
Merge multiple HTML fragment strings into one |
HtmlBuilder.escape(value) |
Escape HTML special characters in a string using the DSL's escaper |
Builder#to_html |
Render builder contents to a minified HTML string |
Builder#to_pretty_html |
Render builder contents to a pretty-printed HTML string |
Builder#text(content) |
Add escaped text content to the current element |
Builder#raw(html) |
Add raw HTML without escaping |
Builder#doctype |
Emit an HTML5 <!DOCTYPE html> declaration |
Builder#render_if(condition) { ... } |
Conditionally render a block if condition is truthy |
Builder#render_unless(condition) { ... } |
Conditionally render a block if condition is falsy |
Builder#define_component(name) { ... } |
Define a reusable named block |
Builder#use_component(name, **locals) |
Render a previously defined component |
Builder#form_for(action, method_type:, **attrs) { ... } |
Build a form tag with common defaults |
Builder#field(name, label_text:, type:, **attrs) |
Build a label + input pair |
Builder#select_field(name, options, label_text:, selected:, **attrs) |
Build a label + select with options |
Builder#textarea_field(name, content, label_text:, **attrs) |
Build a label + textarea |
Builder#hidden_field(name, value) |
Generate a hidden input element |
Builder#submit(text, **attrs) |
Generate a submit button (default text "Submit") |
Builder#list(items, ordered:, **attrs, &block) |
Build a <ul> or <ol> from an array of items |
Builder#class_names(*args) |
Build a conditional CSS class string from strings and hashes |
Builder#merge_attrs(*hashes) |
Merge attribute hashes, concatenating :class (space) and :style ('; ') values |
Builder#aria(**pairs) |
Build an ARIA attribute hash from snake_case keys (rendered as aria-kebab-case); omits nil values |
Builder#cache(key) { ... } |
Cache rendered block output by key; return cached HTML on repeat calls |
Builder#capture { ... } |
Render the block in an isolated scope and return its HTML string (no append) |
Escape.html(value) |
Escape HTML special characters in a string |
Development
bundle install
bundle exec rspec
bundle exec rubocop
Support
If you find this project useful: