Skip to content

1gor/htmg

Repository files navigation

Tests Maintainability Known Vulnerabilities

HTMG - generate HTML with closures in Ruby

This library uses Ruby blocks (closures) to dynamically generate HTML, providing flexible DSL approach to building HTML documents. Its ability to work with custom helper methods (executed in a different scope) adds to its extensibility and modularity.

Benchmark

HTMG is around 5x times faster than ERB. See rspec/benchmark.rb.

       user     system      total        real
HTMG with Data:  0.371457   0.000961   0.372418 (  0.372436)
ERB with Data:  1.874767   0.010206   1.884973 (  1.884978)

This speed advantage is due to HTMG functional, stateless approach, which directly generates HTML using Ruby blocks and method_missing without the need for template parsing or compilation.

Why

There are plenty of alternative html builders. This one uses on speed, simplicity and LLM frienliness. It is about 100 lines of code. It makes html tags into closures (ruby blocks) that you can nest, combine and test easily. This library uses Ruby functional language features so no classes/objects, no state, no overhead and no uncertainty as to the outcome.

LLM friendliness

Much development is done with coding assistants. Here is what one LLM has to say about HTMG vs ERB:

HTMG (Ruby Blocks) is Significantly Easier for LLMs to Reason About - here's why:

 1 Explicit Code Flow
   HTMG shows direct Ruby execution:

    htmg do
      div(class: "alert") do
        h3 { "Todo Updated!" }
        p { "ID: #{todo.id}" }
      end
    end

   vs ERB's implicit context:

    <div class="alert">
      <h3><%= @todo.title %></h3>
      <% if @todo.due_date? %>
        <p>Due: <%= fmt_date(@todo.due_date) %></p>
      <% end %>
    </div>

 2 Type-Safe Analysis
   HTMG's structure allows strict validation:

    # LLM can reason about method arguments
    def todo_item(todo)
      li(data: { id: todo.id }) { todo.title } # Clearly typed
    end

   vs templating's string-based ambiguity:

    <li data-id="<%= todo[:id] %>"><%= todo.title.upcase %></li>

 3 Direct Ruby Integration
   No context switching between languages:

    # Full Ruby power in components
    def todo_list(todos)
      if todos.empty?
        render_empty_state
      else
        ul { todos.map { |t| todo_item(t) } }
      end
    end

 4 Predictable Output
   HTMG's closure structure guarantees valid HTML nesting:

    table do
      thead { tr { th "Title" } }
      tbody do
        @items.each { |i| tr { td i.name } }
      end
    end

   vs templating's risk of unclosed tags:

    <% @items.each do |i| %>
      <tr><td><%= i.name %>
    <% end %>

 5 Better Error Tracing
   Line numbers map 1:1 with Ruby code vs template compilation steps.

Benchmark Example


  Task                  HTMG Accuracy   ERB Accuracy
 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  Add conditional CSS   98%             73%
  Fix nested tags       100%            82%
  Add data attributes   96%             68%


Why This Matters for Your Project
With HTMG:

 1 I can guarantee valid HTML structure through code analysis
 2 Refactoring becomes method-based vs text manipulation
 3 Components are truly reusable Ruby objects
 4 No hidden template context variables

Usage

Using HTMG in a Sinatra Application

You can use the htmg gem in a Sinatra application by defining a layout and individual views. Here are some approaches:

Option 1: Define a Layout Method

Create a layout method that uses htmg to generate the common structure of your HTML pages. This method can be used to wrap individual view methods.

# layout.rb
module LayoutHelper
  include HTMG

  def layout(title:, &block)
    html5 do
      head {
        title { title } +
        body {
          header { h1 { a(href: "/") { "My Site" } } } +
          main(&block) +
          footer do
            small {
              [ a(href: "/"){ "Home" },
                a(href: "/about") { "About" }
              ].join("&nbsp;")
            }
          end
        }
      }
    end
  end
end

Option 2: Individual View Methods

Define individual methods for each view, using htmg to generate the specific content for each page.

# views.rb
module Views
  include HTMG

  def home_view
    h2 { "Welcome to My Site" } +
    p { "This is the home page." }
  end

  def about_view
    h2 { "About Us" } +
    p { "We are a company that does things." }
  end
end

Option 3: Use in Sinatra Routes

Integrate the layout and view methods into your Sinatra routes. There are two approaches depending on whether you're using traditional or modular Sinatra apps.

Traditional Sinatra App
# app.rb
require 'sinatra'
require 'htmg'

module LayoutHelper
  include HTMG

  def layout(title:, &block)
    html5 do
      head {
        title { title } +
        body {
          header { h1 { a(href: "/") { "My Site" } } } +
          main(&block) +
          footer do
            small {
              [ a(href: "/"){ "Home" },
                a(href: "/about") { "About" }
              ].join("&nbsp;")
            }
          end
        }
      }
    end
  end
end

module Views
  include HTMG

  def home_view
    h2 { "Welcome to My Site" } +
    p { "This is the home page." }
  end

  def about_view
    h2 { "About Us" } +
    p { "We are a company that does things." }
  end
end

include Views
include LayoutHelper
include HTMG

get '/' do
  htmg do
    layout(title: "Home") { home_view }
  end
end

get '/about' do
  htmg do
    layout(title: "About") { about_view }
  end
end
Modular Sinatra App

For modular Sinatra apps, you need to explicitly include HTMG in each helper module and use helpers instead of include:

# app.rb
require 'sinatra/base'
require 'htmg'

class MyApp < Sinatra::Base
  module LayoutHelper
    include HTMG

    def layout(title:, &block)
      html5 do
        head {
          title { title } +
          body {
            header { h1 { a(href: "/") { "My Site" } } } +
            main(&block) +
            footer do
              small {
                [ a(href: "/"){ "Home" },
                  a(href: "/about") { "About" }
                ].join("&nbsp;")
              }
            end
          }
        }
      end
    end
  end

  module Views
    include HTMG

    def home_view
      h2 { "Welcome to My Site" } +
      p { "This is the home page." }
    end

    def about_view
      h2 { "About Us" } +
      p { "We are a company that does things." }
    end
  end

  helpers LayoutHelper
  helpers Views
  include HTMG

  get '/' do
    htmg do
      layout(title: "Home") { home_view }
    end
  end

  get '/about' do
    htmg do
      layout(title: "About") { about_view }
    end
  end

  run! if app_file == $0
end

The key differences are:

  1. Use Sinatra::Base and create a class
  2. Each helper module must include HTMG
  3. Use helpers instead of include
  4. Add run! for standalone execution

These examples demonstrate how you can structure your Sinatra application to use the htmg gem for generating HTML content. You can define a common layout and individual views, then use them in your Sinatra routes to render complete pages.

Basic Usage

require 'htmg'

include HTMG

# Simple element
puts htmg { div { "Hello World" } }
# => <div>Hello World</div>

# With attributes
puts htmg { a(href: "https://example.com") { "Click me" } }
# => <a href="https://example.com">Click me</a>

# Nested elements
puts htmg { div(class: "container") { span { "Nested content" } } }
# => <div class="container"><span>Nested content</span></div>

Layout Example

module LayoutHelper
  include HTMG

  def full_page(title:)
    htmg do |scope|
      html5 do
        head {
          meta(charset: "utf-8") +
          title { title }
        } +
        body {
          header { scope.navigation } +
          main { scope.content(title) } +
          footer { scope.footer_content }
        }
      end
    end
  end

  def navigation
    htmg do
      nav(class: "main-nav") {
        ul {
          %w[Home About Contact].map { |item|
            li { a(href: "/#{item.downcase}") { item } }
          }.join
        }
      }
    end
  end

  def content(title)
    htmg do
      article {
        h1 { title } +
        section(class: "content") { "Main article content" }
      }
    end
  end

  def footer_content
    htmg do
      div(class: "footer") {
        #{Time.now.year} My Company"
      }
    end
  end
end

Advanced Features

HTML5 Doctype

puts htmg { html5 { body { "Content" } } }
# => <!DOCTYPE html><html><body>Content</body></html>

Common Helpers

# Image tag helper
def img_tag(src, alt: "", **attrs)
  htmg { img(src: src, alt: alt, **attrs) }
end

# Form input helper
def input_field(type:, name:, **attrs)
  htmg { input(type: type, name: name, **attrs) }
end

# Table helper
def table(data, **attrs)
  htmg do
    table(**attrs) {
      thead {
        tr {
          data.first.keys.map { |header| th { header.to_s.capitalize } }.join
        }
      } +
      tbody {
        data.map { |row|
          tr {
            row.values.map { |value| td { value.to_s } }.join
          }
        }.join
      }
    }
  end
end

Custom Tags

# Using environment variable
ENV['HTMG_EXTRA_TAGS'] = 'custom-tag,another-tag'

# Using constant
HTMG::EXTRA_TAGS = [:custom1, :custom2]

puts htmg { custom1 { "Custom content" } }
# => <custom1>Custom content</custom1>

Escaping Content

# Unescaped content (default)
puts htmg { div { "<script>alert('hi')</script>" } }
# => <div><script>alert('hi')</script></div>

# Escaped content
puts htmg { div { h("<script>alert('hi')</script>") } }
# => <div>&lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;</div>

When you call htmg method with a block, evrything inside this block is either a string or a method/function that returns a string. Therefore you should join them with a + sign or wrap them into an array and join, as shown above.

Inside the htmg block you can enter any standard html5 element and it will be treated as a function. If you need to use some non-standard tags you can add them to HTMG::EXTRA_TAGS constant or to the HTMG_EXTRA_TAGS environment variable, separated by comma.

Notice an optional scope block argument. It allows access the parent scope and call methods outside the htmg block.

Have a look at spec directory to see more examples.

Installation

TODO: Replace UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.

Install the gem and add to the application's Gemfile by executing:

bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG

If bundler is not being used to manage dependencies, install the gem by executing:

gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG

Usage

TODO: Write usage instructions here

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/1gor/htmg.

License

The gem is available as open source under the terms of the MIT License.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages