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
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>
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.
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
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.merge(*fragments) |
Merge multiple HTML fragment strings into one |
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#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#class_names(*args) |
Build a conditional CSS class string from strings and hashes |
Builder#cache(key) { ... } |
Cache rendered block output by key; return cached HTML on repeat calls |
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: