Skip to content

Commit 864e6f4

Browse files
authored
Engine: Add parser_options passthrough and engine config support (#1657)
This pull request adds `parser_options` passthrough support to `Herb::Engine` and makes engine options configurable via `.herb.yml`. Previously, parser options like `strict` had to be individually wired through the engine as dedicated properties. Now, any parser option can be passed through directly: ```ruby Herb::Engine.new(template, parser_options: { strict: false, render_nodes: true }) ``` Engine options can also be configured at the project level in `.herb.yml`: ```yaml engine: debug: true optimize: true parser_options: strict: false ``` The priority chain is: per-call properties > `.herb.yml` config > parser defaults. Additionally, debug now reads from `.herb.yml` configuration like `optimize` already does.
1 parent 2b7c77d commit 864e6f4

59 files changed

Lines changed: 358 additions & 90 deletions

File tree

Some content is hidden

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

lib/herb/configuration.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ def engine
6262
@config["engine"] || {}
6363
end
6464

65+
#: (String, untyped) -> untyped
66+
def engine_option(key, default = nil)
67+
engine.fetch(key.to_s, default)
68+
end
69+
6570
def enabled_validators(overrides = {})
6671
config = dig("engine", "validators") || {}
6772

lib/herb/engine.rb

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,13 @@ def initialize(input, properties = {})
7474
@src = properties[:src] || String.new
7575
@chain_appends = properties[:chain_appends]
7676
@buffer_on_stack = false
77-
@debug = properties.fetch(:debug, false)
77+
@debug = properties.fetch(:debug, Herb.configuration.engine_option("debug", false))
7878
@content_for_head = properties[:content_for_head]
7979
@validation_error_template = nil
8080
@validation_mode = properties.fetch(:validation_mode, :raise)
8181
@enabled_validators = Herb.configuration.enabled_validators(properties[:validators] || {})
82-
@strict = properties.fetch(:strict, true)
83-
@optimize = properties.fetch(:optimize, false)
82+
@optimize = properties.fetch(:optimize, Herb.configuration.engine_option("optimize", false))
83+
@parser_options = properties.fetch(:parser_options, default_parser_options).transform_keys(&:to_sym)
8484

8585
if @optimize && !self.class.optimize_warning_issued
8686
self.class.optimize_warning_issued = true
@@ -130,7 +130,9 @@ def initialize(input, properties = {})
130130

131131
action_view_helpers = @optimize && source_may_contain_action_view_helpers?(input)
132132
transform_conditionals = @optimize && action_view_helpers
133-
parse_result = ::Herb.parse(input, track_whitespace: true, strict: @strict, action_view_helpers: action_view_helpers, transform_conditionals: transform_conditionals)
133+
parse_result = ::Herb.parse(input, **@parser_options, track_whitespace: true,
134+
action_view_helpers: action_view_helpers,
135+
transform_conditionals: transform_conditionals)
134136
ast = parse_result.value
135137
parser_errors = parse_result.errors
136138

@@ -516,6 +518,13 @@ def default_visitors
516518
[]
517519
end
518520

521+
#: () -> Hash[Symbol, untyped]
522+
def default_parser_options
523+
fallback = {} #: Hash[Symbol, untyped]
524+
525+
Herb.configuration.engine_option("parser_options", fallback)
526+
end
527+
519528
def ensure_valid_ruby!(source)
520529
RubyVM::InstructionSequence.compile(source)
521530
rescue SyntaxError => e

sig/herb/configuration.rbs

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sig/herb/engine.rbs

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/configuration_test.rb

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,4 +567,127 @@ def write_config(content, filename = ".herb.yml")
567567
assert_equal false, result[:nesting]
568568
assert_equal true, result[:accessibility]
569569
end
570+
571+
test "engine_option returns default when no config" do
572+
config = Herb::Configuration.load(@temp_dir)
573+
574+
assert_equal false, config.engine_option("optimize", false)
575+
assert_equal true, config.engine_option("strict", true)
576+
end
577+
578+
test "engine_option reads from config file" do
579+
write_config(<<~YAML)
580+
engine:
581+
optimize: true
582+
strict: false
583+
YAML
584+
585+
config = Herb::Configuration.load(@temp_dir)
586+
587+
assert_equal true, config.engine_option("optimize", false)
588+
assert_equal false, config.engine_option("strict", true)
589+
end
590+
591+
test "engine_option returns default for missing keys" do
592+
write_config(<<~YAML)
593+
engine:
594+
optimize: true
595+
YAML
596+
597+
config = Herb::Configuration.load(@temp_dir)
598+
599+
assert_equal true, config.engine_option("optimize", false)
600+
assert_equal true, config.engine_option("strict", true)
601+
end
602+
603+
test "engine parser_options defaults to empty hash" do
604+
config = Herb::Configuration.load(@temp_dir)
605+
606+
assert_equal({}, config.engine_option("parser_options", {}))
607+
end
608+
609+
test "engine parser_options reads from config file" do
610+
write_config(<<~YAML)
611+
engine:
612+
parser_options:
613+
strict: false
614+
render_nodes: true
615+
YAML
616+
617+
config = Herb::Configuration.load(@temp_dir)
618+
619+
parser_options = config.engine_option("parser_options", {})
620+
assert_equal false, parser_options["strict"]
621+
assert_equal true, parser_options["render_nodes"]
622+
end
623+
624+
test "engine parser_options are passed through to Herb.parse" do
625+
engine = Herb::Engine.new("<div>Hello</div>", parser_options: { strict: false })
626+
result = engine.src
627+
628+
assert_kind_of String, result
629+
refute_empty result
630+
end
631+
632+
test "engine parser_options from config are used" do
633+
write_config(<<~YAML)
634+
engine:
635+
parser_options:
636+
strict: false
637+
YAML
638+
639+
Herb.configure(@temp_dir)
640+
641+
engine = Herb::Engine.new("<div>Hello</div>")
642+
result = engine.src
643+
644+
assert_kind_of String, result
645+
refute_empty result
646+
end
647+
648+
test "engine parser_options from properties override config" do
649+
write_config(<<~YAML)
650+
engine:
651+
parser_options:
652+
strict: true
653+
YAML
654+
655+
Herb.configure(@temp_dir)
656+
657+
engine = Herb::Engine.new("<div>Hello</div>", parser_options: { strict: false })
658+
659+
assert_kind_of String, engine.src
660+
end
661+
662+
test "engine debug defaults to false" do
663+
config = Herb::Configuration.load(@temp_dir)
664+
665+
assert_equal false, config.engine_option("debug", false)
666+
end
667+
668+
test "engine debug reads from config file" do
669+
write_config(<<~YAML)
670+
engine:
671+
debug: true
672+
YAML
673+
674+
Herb.configure(@temp_dir)
675+
676+
engine = Herb::Engine.new("<div>Hello</div>")
677+
678+
assert_equal true, engine.debug
679+
end
680+
681+
test "engine debug property overrides config" do
682+
write_config(<<~YAML)
683+
engine:
684+
debug: true
685+
YAML
686+
687+
Herb.configure(@temp_dir)
688+
689+
engine = Herb::Engine.new("<div>Hello</div>", debug: false)
690+
691+
assert_equal false, engine.debug
692+
end
570693
end

test/engine/engine_test.rb

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,5 +277,65 @@ class EngineTest < Minitest::Spec
277277
assert_compiled_snapshot("<script><![CDATA[ alert(1) ]]></script>", enforce_erubi_equality: true)
278278
assert_evaluated_snapshot("<script><![CDATA[ alert(1) ]]></script>")
279279
end
280+
281+
test "compilation with optimize for tag helper" do
282+
assert_compiled_snapshot('<%= tag.div class: "container" do %>Content<% end %>', optimize: true)
283+
end
284+
285+
test "compilation with optimize for void element" do
286+
assert_compiled_snapshot("<%= tag.br %>", optimize: true)
287+
end
288+
289+
test "compilation with optimize for tag with data attributes" do
290+
assert_compiled_snapshot('<%= tag.div data: { controller: "content", count: 42 } %>', optimize: true)
291+
end
292+
293+
test "compilation with optimize for nested tag helpers" do
294+
assert_compiled_snapshot(<<~ERB, optimize: true)
295+
<%= tag.div class: "outer" do %>
296+
<%= tag.span "Inner" %>
297+
<% end %>
298+
ERB
299+
end
300+
301+
test "compilation with optimize for link_to" do
302+
assert_compiled_snapshot('<%= link_to "Home", "/" %>', optimize: true)
303+
end
304+
305+
test "compilation with optimize for image_tag" do
306+
assert_compiled_snapshot('<%= image_tag "photo.jpg", alt: "Photo" %>', optimize: true)
307+
end
308+
309+
test "compilation with optimize for content_tag" do
310+
assert_compiled_snapshot('<%= content_tag :div, "Content", class: "box" %>', optimize: true)
311+
end
312+
313+
test "compilation with optimize preserves non-helper ERB" do
314+
assert_compiled_snapshot(<<~ERB, optimize: true)
315+
<h1><%= title %></h1>
316+
<%= tag.div class: "content" do %>
317+
<p><%= body %></p>
318+
<% end %>
319+
ERB
320+
end
321+
322+
test "compilation with optimize for tag with dynamic attribute" do
323+
assert_compiled_snapshot("<%= tag.div class: css_class %>", optimize: true)
324+
end
325+
326+
test "compilation without optimize leaves tag helpers as Ruby calls" do
327+
template = '<%= tag.div class: "container" do %>Content<% end %>'
328+
329+
without_optimize = Herb::Engine.new(template).src
330+
with_optimize = Herb::Engine.new(template, optimize: true).src
331+
332+
refute_equal without_optimize, with_optimize
333+
assert_includes without_optimize, "tag.div"
334+
refute_includes with_optimize, "tag.div"
335+
end
336+
337+
test "compilation with parser_options strict false" do
338+
assert_compiled_snapshot("<div>Hello</div>", parser_options: { strict: false })
339+
end
280340
end
281341
end

test/engine/evaluation/case_test.rb

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ class CaseTest < Minitest::Spec
2020
ERB
2121

2222
error = assert_raises(Herb::Engine::CompilationError) do
23-
Herb::Engine.new(template, strict: true)
23+
Herb::Engine.new(template, parser_options: { strict: true })
2424
end
2525

2626
assert_includes error.message, "ERBCaseWithConditions"
2727

28-
assert_evaluated_snapshot(template, { status: "pending" }, { escape: false, strict: false })
29-
assert_evaluated_snapshot(template, { status: "approved" }, { escape: false, strict: false })
30-
assert_evaluated_snapshot(template, { status: "other" }, { escape: false, strict: false })
28+
assert_evaluated_snapshot(template, { status: "pending" }, { escape: false, parser_options: { strict: false } })
29+
assert_evaluated_snapshot(template, { status: "approved" }, { escape: false, parser_options: { strict: false } })
30+
assert_evaluated_snapshot(template, { status: "other" }, { escape: false, parser_options: { strict: false } })
3131
end
3232

3333
test "case in pattern in same ERB tag" do
@@ -42,7 +42,7 @@ class CaseTest < Minitest::Spec
4242
ERB
4343

4444
error = assert_raises(Herb::Engine::CompilationError) do
45-
Herb::Engine.new(template, strict: true)
45+
Herb::Engine.new(template, parser_options: { strict: true })
4646
end
4747

4848
assert_includes error.message, "ERBCaseWithConditions"
@@ -59,13 +59,13 @@ class CaseTest < Minitest::Spec
5959
ERB
6060

6161
error = assert_raises(Herb::Engine::CompilationError) do
62-
Herb::Engine.new(template, strict: true)
62+
Herb::Engine.new(template, parser_options: { strict: true })
6363
end
6464

6565
assert_includes error.message, "ERBCaseWithConditions"
6666

67-
assert_evaluated_snapshot(template, { status: "pending" }, { escape: false, strict: false })
68-
assert_evaluated_snapshot(template, { status: "approved" }, { escape: false, strict: false })
67+
assert_evaluated_snapshot(template, { status: "pending" }, { escape: false, parser_options: { strict: false } })
68+
assert_evaluated_snapshot(template, { status: "approved" }, { escape: false, parser_options: { strict: false } })
6969
end
7070

7171
test "case in on newline in same ERB tag" do
@@ -79,13 +79,13 @@ class CaseTest < Minitest::Spec
7979
ERB
8080

8181
error = assert_raises(Herb::Engine::CompilationError) do
82-
Herb::Engine.new(template, strict: true)
82+
Herb::Engine.new(template, parser_options: { strict: true })
8383
end
8484

8585
assert_includes error.message, "ERBCaseWithConditions"
8686

87-
assert_evaluated_snapshot(template, { count: 1 }, { escape: false, strict: false })
88-
assert_evaluated_snapshot(template, { count: 2 }, { escape: false, strict: false })
87+
assert_evaluated_snapshot(template, { count: 1 }, { escape: false, parser_options: { strict: false } })
88+
assert_evaluated_snapshot(template, { count: 2 }, { escape: false, parser_options: { strict: false } })
8989
end
9090
end
9191
end

0 commit comments

Comments
 (0)