Class: Hanami::View::ERB::Parser Private
- 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.
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.
/(\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.
/\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.
/\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.
/\bend\b/
Instance Method Summary collapse
- #call(input) ⇒ Object private
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.
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 |