Low commit activity in last 3 years
Build HTML programmatically using a clean tag DSL with nested blocks, automatic content escaping, void element support, and attribute hashes.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies
 Project Readme

philiprehberger-html_builder

Tests Gem Version Last updated

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>&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;</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
    button('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)

  button('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>

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>')
# => "&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;"

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' } }
footer = Philiprehberger::HtmlBuilder.build { footer { p 'Copyright' } }

Philiprehberger::HtmlBuilder.merge(header, body, footer)
# => '<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
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:

Star the repo

🐛 Report issues

💡 Suggest features

❤️ Sponsor development

🌐 All Open Source Projects

💻 GitHub Profile

🔗 LinkedIn Profile

License

MIT