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

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

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.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#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:

Star the repo

🐛 Report issues

💡 Suggest features

❤️ Sponsor development

🌐 All Open Source Projects

💻 GitHub Profile

🔗 LinkedIn Profile

License

MIT