Skip to content

Herb::Engine v0.7.0+

Herb::Engine is a drop-in replacement for Erubi::Engine that compiles HTML+ERB templates into Ruby code. It extends Erubi's functionality with HTML-aware parsing, validation, and security checks.

Usage

Basic usage (same as Erubi::Engine):

ruby
engine = Herb::Engine.new(source)
puts engine.src

With options:

ruby
engine = Herb::Engine.new(source,
  filename: "app/views/users/show.html.erb",
  escape: true,
)

Erubi Compatibility

Herb::Engine accepts all the same options as Erubi::Engine:

  • bufvar / outvar — Buffer variable name
  • bufval — Initial buffer value
  • escape / escape_html — Whether <%= %> escapes by default
  • escapefunc — Escape function name
  • filename — Template filename
  • freeze — Add frozen string literal comment
  • freeze_template_literals — Freeze template string literals
  • preamble / postamble — Custom preamble/postamble
  • chain_appends — Chain << calls for performance
  • ensure — Wrap in begin/ensure block
  • src — Initial source string

Herb-Specific Options

In addition to Erubi options, Herb::Engine supports:

OptionDefaultDescription
validation_mode:raiseHow to handle validation errors: :raise, :overlay, or :none
validators{}Per-validator overrides (e.g., { security: false })
parser_options{}Parser options forwarded to the parser (e.g., { strict: false })
visitors[]AST visitors to run before compilation
project_pathDir.pwdProject root for relative path resolution
content_for_headnilHTML injected before the closing </head> tag
validate_rubyfalseRaise if the compiled output isn't valid Ruby
optimizefalseCompile-time optimizations for Action View helpers (experimental)
debugfalseEnable debug mode

Strict parsing is a parser option rather than an engine option, so it is set through parser_options, together with any other parser option:

ruby
Herb::Engine.new(source, parser_options: { strict: false })

Validators

The engine runs validators on parsed templates to catch errors before compilation. Each validator can be enabled or disabled via .herb.yml configuration or per-instance overrides.

ValidatorDescription
SecurityDetects ERB output in unsafe positions (attribute names, attribute positions)
NestingValidates HTML nesting rules (e.g., no <div> inside <p>)
AccessibilityValidates accessibility-related attributes

Disable security validator for this template:

ruby
Herb::Engine.new(source, validators: { security: false })

See Engine Configuration for .herb.yml configuration.

Validation Mode

Controls how the engine presents validation results:

  • :raise — Raises SecurityError or CompilationError (default, used in tests and CLI)
  • :overlay — Renders errors as in-browser overlay (used by ReActionView in development)
  • :none — Skips validation entirely

Transform Visitors

The visitors option accepts visitors that run over the AST before compilation. Transform visitors rewrite the AST, which changes what the compiler emits.

Herb ships the following transform visitors:

VisitorDescription
AutoCloseOmittedTagsVisitorReplaces omitted closing tags with explicit ones
ComponentVisitorRewrites capitalized tags into render calls (experimental)

Transform visitors are not loaded when you require "herb". Require the ones you want and pass them to the engine:

ruby
require "herb/engine/auto_close_omitted_tags_visitor"

Herb::Engine.new(source, visitors: [Herb::Engine::AutoCloseOmittedTagsVisitor.new])

Your own visitors are passed the same way. See Visitors for how to write one.

AutoCloseOmittedTagsVisitor

Makes sure the compiled output always contains a closing tag, even when the template omits it.

Given this template:

html
<ul>
  <li>List Item 1
Element `<li>` at (2:3) has its closing tag omitted. While valid HTML, consider adding an explicit `</li>` closing tag at (3:2) for clarity, or set `strict: false` to allow this. (`OMITTED_CLOSING_TAG_ERROR`) (parser-no-errors)
<li>List Item 2
Missing explicit closing tag for `<li>`. Use `</li>` instead of relying on implicit tag closing. (html-require-closing-tags)
Element `<li>` at (3:3) has its closing tag omitted. While valid HTML, consider adding an explicit `</li>` closing tag at (4:0) for clarity, or set `strict: false` to allow this. (`OMITTED_CLOSING_TAG_ERROR`) (parser-no-errors)
</ul>
Missing explicit closing tag for `<li>`. Use `</li>` instead of relying on implicit tag closing. (html-require-closing-tags)

The engine renders:

html
<ul>
  <li>List Item 1
  </li><li>List Item 2
</li></ul>

The closing tag is inserted where the parser determined the element ends, which keeps the surrounding whitespace (and therefore the rendering of inline-block elements) identical to the template without the visitor.

ComponentVisitor

WARNING

ComponentVisitor is experimental and a proof of concept. The generated render calls, the attribute mapping, and the class itself may change or be removed without a major version bump. It prints a warning the first time it is instantiated in a process.

Rewrites capitalized tags into render calls, so a component can be written as a tag instead of an ERB expression.

ruby
require "herb/engine/component_visitor"

Herb::Engine.new(source, visitors: [Herb::Engine::ComponentVisitor.new])

A tag is transformed when its name is CamelCase in every segment. <DIV>, <BR> and <My-Component /> are left alone, since uppercase HTML tags are valid HTML.

How the tag is resolved is decided entirely from the tag name, with no lookup at compile time or at render time:

TagSeparatorResolves to
<Card />nonerender Card.new
<Users::Card />::, a constantrender Users::Card.new
<Users.Card />., a pathrender "users/card"
<Admin.Users.ProfileCard />., a pathrender "admin/users/profile_card"

Dot notation needs the dot_notation_tags parser option for the tag name to parse at all:

ruby
Herb::Engine.new(source,
  parser_options: { dot_notation_tags: true },
  visitors: [Herb::Engine::ComponentVisitor.new],
)

Attribute names are converted from kebab-case to snake_case and become keyword arguments:

AttributeBecomesNotes
name="hello"name: "hello"Quotes, backslashes and #{} are escaped
:count="@count"count: @countA : prefix is used as Ruby code
name="<%= @user.name %>"name: "#{@user.name}"ERB is interpolated into the string
disableddisabled: trueAn attribute without a value
item-id="7"item_id: "7"

An attribute whose name isn't a valid keyword argument, such as @click, is skipped, and the first of a repeated attribute wins.

html
<MyComponent name="hello" :count="@count" item-id="7" />
Opening tag name `<MyComponent>` should be lowercase. Use `<mycomponent>` instead. (html-tag-name-lowercase)
Use `<MyComponent></MyComponent>` instead of self-closing `<MyComponent />` for HTML compatibility. (html-no-self-closing)

Compiles to the equivalent of:

erb
<%= render MyComponent.new(name: "hello", count: @count, item_id: "7") %>

For a partial, the same attributes become locals instead of keyword arguments:

html
<Users.Card name="hello" :count="@count" />
Unexpected Token. Expected: an identifier, `@`, `<%`, whitespace, or a newline, found: a character. (`UNEXPECTED_ERROR`) (parser-no-errors)
erb
<%= render "users/card", name: "hello", count: @count %>

A tag with a body becomes a block, and the body is compiled as normal, so it can contain HTML, ERB, and further components:

html
<Card title="Hello">
Opening tag name `<Card>` should be lowercase. Use `<card>` instead. (html-tag-name-lowercase)
<div>Regular HTML</div> <%= @thing %> <Button>Nested component</Button>
Closing tag name `</Button>` should be lowercase. Use `</button>` instead. (html-tag-name-lowercase)
Opening tag name `<Button>` should be lowercase. Use `<button>` instead. (html-tag-name-lowercase)
</Card>
Closing tag name `</Card>` should be lowercase. Use `</card>` instead. (html-tag-name-lowercase)
erb
<%= render Card.new(title: "Hello") do %>
  <div>Regular HTML</div>
  <%= @thing %>
  <%= render Button.new do %>Nested component<% end %>
<% end %>

A partial with a body is rendered as a layout, so the body reaches the partial through yield:

html
<Users.Card title="Hello">Body</Users.Card>
Closing tag `</Users>` at (1:32) is missing closing `>`. (`UNCLOSED_CLOSE_TAG_ERROR`) (parser-no-errors)
Unexpected Token. Expected: an identifier, `@`, `<%`, whitespace, or a newline, found: a character. (`UNEXPECTED_ERROR`) (parser-no-errors)
erb
<%= render layout: "users/card", locals: { title: "Hello" } do %>Body<% end %>

ReActionView Integration

ReActionView registers Herb::Engine as the template handler for .html.erb and .html.herb files in Rails. It uses validation_mode: :overlay so validation errors appear as in-browser overlays during development instead of raising exceptions.

Validator settings from .herb.yml are respected automatically — no ReActionView-specific configuration needed.

ReActionView also lets you run transform visitors on every template it compiles, through config.transform_visitors:

ruby
# config/initializers/reactionview.rb
require "herb/engine/auto_close_omitted_tags_visitor"

ReActionView.configure do |config|
  config.transform_visitors = [
    Herb::Engine::AutoCloseOmittedTagsVisitor.new
  ]
end

Released under the MIT License.