class Hanami::View::ERB::Parser

ERB parser for Hanami views.

This is a [Temple] parser that prepares a Temple [s-expression] (sexp) for later generating as HTML via {ERB::Engine}.

[temple]: github.com/judofyr/temple [expressions]: github.com/judofyr/temple/blob/master/EXPRESSIONS.md

The key features of this parser are:

To support implicit block capture, this parser differs somewhat from Temple’s example ERB parser, as well as most other Temple parsers. Typical parsers prepare a single ‘result` sexp up front, like so:

result = [:multi]

As parsing occurs, new Temple sexps are then pushed onto this ‘result`.

In this parser, however, we prepare a stack of results:

results = [[:multi]]

The first item in the stack (‘[:multi]`) is the final result that will be returned at the end of parsing. Every sexp that is generated during parsing will still be added to this result, directly or indirectly.

How this happens is that during parsing, every new sexp is push to the last result in this stack, representing the “current” result.

The nature of stack becomes important when we encounter an ERB expression tag that opens a code block, such as ‘<%= form_for(:post) do %>`.

In this case, we push an ‘[:erb, :block, …, [:multi]]` sexp to the last result, with its `[:multi]` representing the contents of that code block. We then also push this particular `[:multi]` onto the `results` stack, so that any subsequent sexps are added to the block’s own contents.

Then, when we encounter the ‘<% end %>` closing tag for that block, we pop the block’s ‘[:multi]` off the results stack. This `[:multi]` isn’t lost, however, because it is still referenced inside the ‘[:erb, :block, …, [:multi]]` sexp.

Taking this approach (along with the ‘on_erb_block` sexp-handling code in `Hanami::View::ERB::Filters::Block`) allows us to implicitly capture the contents of the block and output it in place. This means that helpers that expect blocks do not need to explicitly call a `capture` helper (or similar) internally. Instead they can just `yield`, per idiomatic Ruby.

In fact, we pop a result off the stack every time we encounter an ‘<% end %>` tag. To acount for this, every time we encounter an ERB code tag that will have a matching closing tag (such as `<% if some_cond %>` or `<% 5.times do %>`), we push another reference to the current last result onto the `results` stack. This allows subsequent sexps to be added to the same item on the stack (they need no special handling; the special handling is for blocks with ERB expression tags only) while still allowing it to be popped again when the matching `<% end %>` is encountered.

In this way, this stack of results will grow every time a new scope requiring an ‘end` is opened, and then will shrink again as each `end` is encountered; think of it as matching the level of LHS indentation if you were writing such code by hand. This allows each new generated sexp to be pushed onto `results.last`, knowing that it will go into the right place in the overall sexp tree. By the time we finish parsing, just a single result will remain, which is the value returned.

@api private @since 2.1.0

Constants

BLOCK_LINE_RE
END_LINE_RE
ERB_PATTERN
IF_UNLESS_CASE_LINE_RE

Public Instance Methods

call(input) click to toggle source
# File lib/hanami/view/erb/parser.rb, line 89
def call(input)
  results = [[:multi]]
  pos = 0

  input.scan(ERB_PATTERN) do |token, indicator, code|
    # Capture any text between the last ERB tag and the current one, and update the position
    # to match the end of the current tag for the next iteration of text collection.
    text = input[pos...$~.begin(0)]
    pos  = $~.end(0)

    if token
      # First, handle certain static tokens picked up by our ERB_PATTERN regexp. These are
      # newlines as well as the special codes for literal `<%` and `%>` values.
      case token
      when "\n"
        results.last << [:static, "#{text}\n"] << [:newline]
      when "<%%", "%%>"
        results.last << [:static, text] unless text.empty?
        token.slice!(1)
        results.last << [:static, token]
      end
    else
      # Next, handle actual ERB tags. Start by adding any static text between this match and
      # the last.
      results.last << [:static, text] unless text.empty?

      case indicator
      when "#"
        # Comment tags: <%# this is a comment %>
        results.last << [:code, "\n" * code.count("\n")]
      when %r{=}
        # Expression tags: <%= "hello (auto-escaped)" %> or <%== "hello (not escaped)" %>
        if code =~ BLOCK_LINE_RE
          # See Hanami::View::Erb::Filters::Block for the processing of `:erb, :block` sexps
          block_node = [:erb, :block, indicator.size == 1, code, (block_content = [:multi])]
          results.last << block_node

          # For blocks opened in ERB expression tags, push this `[:multi]` sexp
          # (representing the content of the block) onto the stack of resuts. This allows
          # subsequent results to be appropriately added inside the block, until its closing
          # tag is encountered, and this `block_content` multi is subsequently popped off
          # the results stack.
          results << block_content
        else
          results.last << [:escape, indicator.size == 1, [:dynamic, code]]
        end
      else
        # Code tags: <% if some_cond %>
        if code =~ BLOCK_LINE_RE || code =~ IF_UNLESS_CASE_LINE_RE
          results.last << [:code, code]

          # For ERB code tags that will result in a matching `end`, push the last result
          # back onto the stack of results. This might seem redundant, but it allows
          # subsequent sexps to continue to be pushed onto the same result while also
          # allowing it to be safely popped again when the matching `end` is encountered.
          results << results.last
        elsif code =~ END_LINE_RE
          results.last << [:code, code]
          results.pop
        else
          results.last << [:code, code]
        end
      end
    end
  end

  # Add any text after the final ERB tag
  results.last << [:static, input[pos..-1]]
end