Skip to content

Commit a06c5e9

Browse files
authored
Engine: Add Support for ERB trimming (marcoroth#841)
This pull request implements support for left trimming ` <%-` and right trimming `-%>` in the `Herb::Engine`. This should now more closely match the compiled template `Erubi::Engine` produces and renders. Resolves marcoroth#649 Enables marcoroth#650
1 parent 941bce8 commit a06c5e9

File tree

242 files changed

+901
-722
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

242 files changed

+901
-722
lines changed

lib/herb/engine.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,9 @@ def initialize(input, properties = {})
8787
end
8888

8989
@src << "__herb = ::Herb::Engine; " if @escape && @escapefunc == "__herb.h"
90-
9190
@src << preamble
92-
@src << "\n" unless preamble.end_with?("\n")
9391

94-
parse_result = ::Herb.parse(input)
92+
parse_result = ::Herb.parse(input, track_whitespace: true)
9593
ast = parse_result.value
9694
parser_errors = parse_result.errors
9795

@@ -190,6 +188,8 @@ def add_code(code)
190188
if code.include?("=begin") || code.include?("=end")
191189
@src << "\n" << code << "\n"
192190
else
191+
@src.chomp! if @src.end_with?("\n") && code.start_with?(" ") && !code.end_with?("\n")
192+
193193
@src << " " << code
194194

195195
# TODO: rework and check for Prism::InlineComment as soon as we expose the Prism Nodes in the Herb AST

lib/herb/engine/compiler.rb

Lines changed: 125 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def visit_html_open_tag_node(node)
8686
end
8787

8888
def visit_html_attribute_node(node)
89-
add_text(" ")
89+
add_whitespace(" ")
9090

9191
visit(node.name)
9292

@@ -134,7 +134,7 @@ def visit_literal_node(node)
134134
end
135135

136136
def visit_whitespace_node(node)
137-
add_text(node.value.value) if node.value
137+
add_whitespace(node.value.value)
138138
end
139139

140140
def visit_html_comment_node(node)
@@ -168,7 +168,7 @@ def visit_erb_content_node(node)
168168
end
169169

170170
def visit_erb_control_node(node, &_block)
171-
add_code(node.content.value.strip)
171+
apply_trim(node, node.content.value.strip)
172172

173173
yield if block_given?
174174
end
@@ -325,17 +325,15 @@ def process_erb_tag(node, skip_comment_check: false)
325325
if erb_output?(opening)
326326
process_erb_output(opening, code)
327327
else
328-
add_code(code)
328+
apply_trim(node, code)
329329
end
330-
331-
handle_whitespace_trimming(node)
332330
end
333331

334332
def add_text(text)
335333
return if text.empty?
336334

337335
if @trim_next_whitespace
338-
text = text.lstrip
336+
text = text.sub(/\A[ \t]*\r?\n/, "")
339337
@trim_next_whitespace = false
340338
end
341339

@@ -344,6 +342,10 @@ def add_text(text)
344342
@tokens << [:text, text, current_context]
345343
end
346344

345+
def add_whitespace(whitespace)
346+
@tokens << [:whitespace, whitespace, current_context]
347+
end
348+
347349
def add_code(code)
348350
@tokens << [:code, code, current_context]
349351
end
@@ -359,11 +361,13 @@ def add_expression_escaped(code)
359361
def optimize_tokens(tokens)
360362
return tokens if tokens.empty?
361363

364+
compacted = compact_whitespace_tokens(tokens)
365+
362366
optimized = [] #: Array[untyped]
363367
current_text = ""
364368
current_context = nil
365369

366-
tokens.each do |type, value, context|
370+
compacted.each do |type, value, context|
367371
if type == :text
368372
current_text += value
369373
current_context ||= context
@@ -384,6 +388,56 @@ def optimize_tokens(tokens)
384388
optimized
385389
end
386390

391+
def compact_whitespace_tokens(tokens)
392+
return tokens if tokens.empty?
393+
394+
tokens.map.with_index { |token, index|
395+
next token unless token[0] == :whitespace
396+
397+
next nil if adjacent_whitespace?(tokens, index)
398+
next nil if whitespace_before_code_sequence?(tokens, index)
399+
400+
[:text, token[1], token[2]]
401+
}.compact
402+
end
403+
404+
def adjacent_whitespace?(tokens, index)
405+
prev_token = index.positive? ? tokens[index - 1] : nil
406+
next_token = index < tokens.length - 1 ? tokens[index + 1] : nil
407+
408+
trailing_whitespace?(prev_token) || leading_whitespace?(next_token)
409+
end
410+
411+
def trailing_whitespace?(token)
412+
return false unless token
413+
414+
token[0] == :whitespace || (token[0] == :text && token[1] =~ /\s\z/)
415+
end
416+
417+
def leading_whitespace?(token)
418+
token && token[0] == :text && token[1] =~ /\A\s/
419+
end
420+
421+
def whitespace_before_code_sequence?(tokens, current_index)
422+
previous_token = tokens[current_index - 1] if current_index.positive?
423+
424+
return false unless previous_token && previous_token[0] == :code
425+
426+
token_before_code = find_token_before_code_sequence(tokens, current_index)
427+
428+
return false unless token_before_code
429+
430+
trailing_whitespace?(token_before_code)
431+
end
432+
433+
def find_token_before_code_sequence(tokens, whitespace_index)
434+
search_index = whitespace_index - 1
435+
436+
search_index -= 1 while search_index >= 0 && tokens[search_index][0] == :code
437+
438+
search_index >= 0 ? tokens[search_index] : nil
439+
end
440+
387441
def process_erb_output(opening, code)
388442
should_escape = should_escape_output?(opening)
389443
add_expression_with_escaping(code, should_escape)
@@ -402,8 +456,69 @@ def add_expression_with_escaping(code, should_escape)
402456
end
403457
end
404458

405-
def handle_whitespace_trimming(node)
406-
@trim_next_whitespace = true if node.tag_closing&.value == "-%>"
459+
def at_line_start?
460+
@tokens.empty? ||
461+
@tokens.last[0] != :text ||
462+
@tokens.last[1].empty? ||
463+
@tokens.last[1].end_with?("\n") ||
464+
@tokens.last[1] =~ /\A[ \t]+\z/ ||
465+
@tokens.last[1] =~ /\n[ \t]+\z/
466+
end
467+
468+
def extract_lspace
469+
return "" unless @tokens.last && @tokens.last[0] == :text
470+
471+
text = @tokens.last[1]
472+
473+
return Regexp.last_match(1) if text =~ /\n([ \t]+)\z/ || text =~ /\A([ \t]+)\z/
474+
475+
""
476+
end
477+
478+
def extract_and_remove_lspace!
479+
lspace = extract_lspace
480+
return lspace if lspace.empty?
481+
482+
text = @tokens.last[1]
483+
if text =~ /\n[ \t]+\z/
484+
text.sub!(/[ \t]+\z/, "")
485+
elsif text =~ /\A[ \t]+\z/
486+
text.replace("")
487+
end
488+
@tokens.last[1] = text
489+
490+
lspace
491+
end
492+
493+
def apply_trim(node, code)
494+
has_left_trim = node.tag_opening.value.start_with?("<%-")
495+
node.tag_closing&.value
496+
497+
remove_trailing_whitespace_from_last_token! if has_left_trim
498+
499+
if at_line_start?
500+
lspace = extract_and_remove_lspace!
501+
rspace = " \n"
502+
503+
@tokens << [:code, "#{lspace}#{code}#{rspace}", current_context]
504+
@trim_next_whitespace = true
505+
else
506+
@tokens << [:code, code, current_context]
507+
end
508+
end
509+
510+
def remove_trailing_whitespace_from_last_token!
511+
return unless @tokens.last && @tokens.last[0] == :text
512+
513+
text = @tokens.last[1]
514+
515+
if text =~ /\n[ \t]+\z/
516+
text.sub!(/[ \t]+\z/, "")
517+
@tokens.last[1] = text
518+
elsif text =~ /\A[ \t]+\z/
519+
text.replace("")
520+
@tokens.last[1] = text
521+
end
407522
end
408523
end
409524
end

lib/herb/engine/debug_visitor.rb

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -180,9 +180,7 @@ def add_debug_attributes_to_element(open_tag_node)
180180
debug_attributes << create_debug_attribute("data-herb-debug-attach-to-parent", "true")
181181
end
182182

183-
debug_attributes.each do |attr|
184-
open_tag_node.children << attr
185-
end
183+
open_tag_node.children.concat(debug_attributes)
186184

187185
@debug_attributes_applied = true
188186
end
@@ -233,9 +231,7 @@ def create_debug_span_for_erb(erb_node)
233231
]
234232

235233
debug_attributes << create_debug_attribute("data-herb-debug-line", line.to_s) if line
236-
237234
debug_attributes << create_debug_attribute("data-herb-debug-column", (column + 1).to_s) if column
238-
239235
debug_attributes << create_debug_attribute("style", "display: contents;")
240236

241237
tag_name_token = create_token(:tag_name, "span")

sig/herb/engine/compiler.rbs

Lines changed: 23 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sig/herb_c_extension.rbs

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/engine/cli_test.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -274,9 +274,9 @@ def with_temp_file(content)
274274

275275
output = captured_output
276276

277-
assert_includes output, "'<ul>\n '.freeze"
278-
assert_includes output, "'\n <li>'.freeze"
279-
assert_includes output, "'\n</ul>\n'.freeze"
277+
assert_includes output, "'<ul>\n'.freeze"
278+
assert_includes output, "' <li>'.freeze"
279+
assert_includes output, "'</ul>\n'.freeze"
280280
end
281281
end
282282

test/engine/rails_compatibility_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ def flush_newline_if_pending(src)
153153

154154
engine = RailsHerb.new(template, escape: true)
155155

156-
assert_equal "\n @output_buffer.safe_append='<h1>'.freeze; @output_buffer.append=(@title); @output_buffer.safe_append='</h1>'.freeze;\n@output_buffer", engine.src
156+
assert_equal " @output_buffer.safe_append='<h1>'.freeze; @output_buffer.append=(@title); @output_buffer.safe_append='</h1>'.freeze;\n@output_buffer", engine.src
157157
end
158158

159159
test "drop-in replacement compatibility" do

0 commit comments

Comments
 (0)