Class: Hanami::View::ERB::Parser Private

Inherits:
Temple::Parser
  • Object
show all
Defined in:
lib/hanami/view/erb/parser.rb

Overview

This class is part of a private API. You should avoid using this class if possible, as it may be removed or be changed in the future.

ERB parser for Hanami views.

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

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

The key features of this parser are:

  • Auto-escaping any non-‘html_safe?` values given to `<%=` ERB expression tags, with auto-escaping disabled when using `<%==` tags.

  • Implicitly capturing and correctly outputting block content without the need for special helpers. This allows helpers like ‘<%= form_for(:post) do %>` to be used, with the `form_for` helper itself doing nothing more special than a `yield`.

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.

Since:

  • 2.1.0

Constant Summary collapse

ERB_PATTERN =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Since:

  • 2.1.0

/(\n|<%%|%%>)|<%(==?|\#)?(.*?)?-?%>/m
IF_UNLESS_CASE_LINE_RE =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Since:

  • 2.1.0

/\A\s*(if|unless|case)\b/
BLOCK_LINE_RE =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Since:

  • 2.1.0

/\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/
END_LINE_RE =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Since:

  • 2.1.0

/\bend\b/

Instance Method Summary collapse

Instance Method Details

#call(input) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 2.1.0



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# 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