diff --git a/lib/herb/engine.rb b/lib/herb/engine.rb
index db193a916..5bfb8e4d5 100644
--- a/lib/herb/engine.rb
+++ b/lib/herb/engine.rb
@@ -131,9 +131,17 @@ def initialize(input, properties = {})
action_view_helpers = @optimize && source_may_contain_action_view_helpers?(input)
transform_conditionals = @optimize && action_view_helpers
- parse_result = ::Herb.parse(input, **@parser_options, track_whitespace: true,
- action_view_helpers: action_view_helpers,
- transform_conditionals: transform_conditionals)
+ render_nodes = @optimize && input.include?("render")
+
+ parse_result = ::Herb.parse(
+ input,
+ **@parser_options,
+ track_whitespace: true,
+ action_view_helpers: action_view_helpers,
+ transform_conditionals: transform_conditionals,
+ render_nodes: render_nodes
+ )
+
ast = parse_result.value
parser_errors = parse_result.errors
@@ -159,7 +167,19 @@ def initialize(input, properties = {})
ast.accept(visitor)
end
- compiler = Compiler.new(self, properties)
+ compiler_options = properties.dup
+
+ if @optimize && render_nodes && properties.fetch(:inline_render, false)
+ require_relative "engine/render_inliner"
+
+ compiler_options[:render_inliner] = RenderInliner.new(
+ self,
+ project_path: @project_path,
+ filename: @relative_file_path
+ )
+ end
+
+ compiler = Compiler.new(self, compiler_options)
ast.accept(compiler)
diff --git a/lib/herb/engine/compiler.rb b/lib/herb/engine/compiler.rb
index bdf551baa..201a074b5 100644
--- a/lib/herb/engine/compiler.rb
+++ b/lib/herb/engine/compiler.rb
@@ -18,6 +18,7 @@ def initialize(engine, options = {})
@engine = engine
@escape = options.fetch(:escape) { options.fetch(:escape_html, false) }
+ @render_inliner = options[:render_inliner]
@tokens = [] #: Array[untyped]
@element_stack = [] #: Array[String]
@context_stack = [:html_content]
@@ -215,6 +216,18 @@ def visit_erb_content_node(node)
process_erb_tag(node)
end
+ def visit_erb_render_node(node)
+ if @render_inliner&.can_inline?(node)
+ if @render_inliner.collection?(node)
+ inline_collection(node)
+ else
+ inline_partial(node)
+ end
+ else
+ process_erb_tag(node)
+ end
+ end
+
def visit_erb_control_node(node, &)
if node.content
apply_trim(node, node.content.value.strip)
@@ -358,6 +371,51 @@ def visit_erb_control_with_parts(node, *parts)
private
+ def inline_partial(node)
+ resolved_path = @render_inliner.resolve_path(node)
+ return process_erb_tag(node) unless resolved_path
+
+ source = File.read(resolved_path)
+ locals = @render_inliner.local_assignments(node)
+
+ @render_inliner.push(resolved_path)
+
+ add_code("begin")
+ locals.each { |name, value| add_code("; #{name} = (#{value})") }
+ add_code(";")
+
+ partial_ast = @render_inliner.parse(source)
+ partial_ast&.accept(self)
+
+ add_code("; end;")
+
+ @render_inliner.pop(resolved_path)
+ end
+
+ def inline_collection(node)
+ resolved_path = @render_inliner.resolve_path(node)
+ return process_erb_tag(node) unless resolved_path
+
+ source = File.read(resolved_path)
+ collection_expr = @render_inliner.collection_expression(node)
+ item_name = @render_inliner.collection_item_name(node)
+ counter_name = "#{item_name}_counter"
+ locals = @render_inliner.local_assignments(node)
+
+ @render_inliner.push(resolved_path)
+
+ add_code("; __herb_collection = (#{collection_expr}); __herb_collection.each_with_index do |#{item_name}, #{counter_name}|")
+ locals.each { |name, value| add_code("; #{name} = (#{value})") }
+ add_code(";")
+
+ partial_ast = @render_inliner.parse(source)
+ partial_ast&.accept(self)
+
+ add_code("; end;")
+
+ @render_inliner.pop(resolved_path)
+ end
+
def check_for_escaped_erb_tag!(opening)
return unless opening.start_with?("<%%")
diff --git a/lib/herb/engine/render_inliner.rb b/lib/herb/engine/render_inliner.rb
new file mode 100644
index 000000000..b67c168de
--- /dev/null
+++ b/lib/herb/engine/render_inliner.rb
@@ -0,0 +1,166 @@
+# frozen_string_literal: true
+
+module Herb
+ class Engine
+ class RenderInliner
+ def initialize(engine, options = {})
+ @engine = engine
+ @project_path = options[:project_path] || Pathname.new(Dir.pwd)
+ @filename = options[:filename]
+ @view_root = find_view_root
+ @source_directory = find_source_directory
+ @inlining_stack = Set.new
+ end
+
+ def can_inline?(node)
+ return false unless @view_root
+ return false unless node.static_partial?
+ return false if node.body&.any?
+
+ return can_inline_collection?(node) if node.keywords&.collection
+
+ resolved = resolve_path(node)
+ return false unless resolved
+ return false if @inlining_stack.include?(resolved.to_s)
+ return false unless safe_to_inline?(resolved)
+
+ true
+ end
+
+ def can_inline_collection?(node)
+ return false unless node.static_partial?
+ return false if node.keywords&.spacer_template
+
+ resolved = resolve_path(node)
+ return false unless resolved
+ return false if @inlining_stack.include?(resolved.to_s)
+ return false unless safe_to_inline?(resolved)
+
+ true
+ end
+
+ def collection?(node)
+ !!node.keywords&.collection
+ end
+
+ def collection_expression(node)
+ node.keywords&.collection&.value
+ end
+
+ def collection_item_name(node)
+ as_name = node.keywords&.as_name&.value
+ return as_name if as_name
+
+ partial = node.partial_path
+ return nil unless partial
+
+ File.basename(partial)
+ end
+
+ def resolve_path(node)
+ node.resolve(view_root: @view_root, source_directory: @source_directory)
+ end
+
+ def optimizable_renders(ast)
+ collector = OptimizableRenderCollector.new(self)
+ ast.accept(collector)
+ collector.results
+ end
+
+ def local_assignments(node)
+ locals = {} #: Hash[String, String]
+
+ node.keywords&.locals&.each do |local|
+ name = local.name&.value
+ value = local.value&.content
+
+ next unless name && value
+
+ value = name if value == "#{name}:" # Shorthand hash syntax
+
+ locals[name] = value
+ end
+
+ locals
+ end
+
+ def parse(source)
+ result = ::Herb.parse(
+ source,
+ render_nodes: true,
+ track_whitespace: true,
+ action_view_helpers: true,
+ transform_conditionals: true
+ )
+
+ result.value
+ end
+
+ def push(path)
+ @inlining_stack.add(path.to_s)
+ end
+
+ def pop(path)
+ @inlining_stack.delete(path.to_s)
+ end
+
+ private
+
+ def safe_to_inline?(file_path)
+ source = File.read(file_path)
+
+ return false if source.include?("content_for")
+ return false if source.match?(/\byield\b/)
+ return false if source.include?("local_assigns")
+
+ true
+ end
+
+ def find_view_root
+ candidates = [
+ @project_path.join("app", "views")
+ ]
+
+ candidates.find(&:directory?)
+ end
+
+ def find_source_directory
+ return nil unless @filename && @view_root
+
+ dir = Pathname.new(@filename).dirname
+ @view_root.join(dir)
+ end
+ end
+
+ class OptimizableRenderCollector < ::Herb::Visitor
+ attr_reader :results
+
+ def initialize(inliner)
+ super()
+ @inliner = inliner
+ @results = [] #: Array[Hash[Symbol, untyped]]
+ end
+
+ def visit_erb_render_node(node)
+ if @inliner.can_inline?(node)
+ entry = {
+ node: node,
+ partial_path: node.partial_path,
+ resolved_path: @inliner.resolve_path(node),
+ locals: @inliner.local_assignments(node),
+ collection: @inliner.collection?(node),
+ }
+
+ if entry[:collection]
+ entry[:collection_expression] = @inliner.collection_expression(node)
+ entry[:item_name] = @inliner.collection_item_name(node)
+ end
+
+ @results << entry
+ end
+
+ visit_child_nodes(node)
+ end
+ end
+ end
+end
diff --git a/sig/herb/engine/compiler.rbs b/sig/herb/engine/compiler.rbs
index 638c28149..672b2ee30 100644
--- a/sig/herb/engine/compiler.rbs
+++ b/sig/herb/engine/compiler.rbs
@@ -61,6 +61,8 @@ module Herb
def visit_erb_content_node: (untyped node) -> untyped
+ def visit_erb_render_node: (untyped node) -> untyped
+
def visit_erb_control_node: (untyped node) ?{ (?) -> untyped } -> untyped
def visit_erb_if_node: (untyped node) -> untyped
@@ -101,6 +103,10 @@ module Herb
private
+ def inline_partial: (untyped node) -> untyped
+
+ def inline_collection: (untyped node) -> untyped
+
def check_for_escaped_erb_tag!: (untyped opening) -> untyped
def current_context: () -> untyped
diff --git a/sig/herb/engine/render_inliner.rbs b/sig/herb/engine/render_inliner.rbs
new file mode 100644
index 000000000..693fcffc6
--- /dev/null
+++ b/sig/herb/engine/render_inliner.rbs
@@ -0,0 +1,47 @@
+# Generated from lib/herb/engine/render_inliner.rb with RBS::Inline
+
+module Herb
+ class Engine
+ class RenderInliner
+ def initialize: (untyped engine, ?untyped options) -> untyped
+
+ def can_inline?: (untyped node) -> untyped
+
+ def can_inline_collection?: (untyped node) -> untyped
+
+ def collection?: (untyped node) -> untyped
+
+ def collection_expression: (untyped node) -> untyped
+
+ def collection_item_name: (untyped node) -> untyped
+
+ def resolve_path: (untyped node) -> untyped
+
+ def optimizable_renders: (untyped ast) -> untyped
+
+ def local_assignments: (untyped node) -> untyped
+
+ def parse: (untyped source) -> untyped
+
+ def push: (untyped path) -> untyped
+
+ def pop: (untyped path) -> untyped
+
+ private
+
+ def safe_to_inline?: (untyped file_path) -> untyped
+
+ def find_view_root: () -> untyped
+
+ def find_source_directory: () -> untyped
+ end
+
+ class OptimizableRenderCollector < ::Herb::Visitor
+ attr_reader results: untyped
+
+ def initialize: (untyped inliner) -> untyped
+
+ def visit_erb_render_node: (untyped node) -> untyped
+ end
+ end
+end
diff --git a/test/engine/render_inliner_test.rb b/test/engine/render_inliner_test.rb
new file mode 100644
index 000000000..9b104a8d2
--- /dev/null
+++ b/test/engine/render_inliner_test.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require_relative "../test_helper"
+require_relative "../snapshot_utils"
+require_relative "../../lib/herb/engine"
+
+module Engine
+ class RenderInlinerTest < Minitest::Spec
+ include SnapshotUtils
+
+ PROJECT_PATH = "test/fixtures/render_inliner"
+
+ test "inlines a simple partial" do
+ assert_compiled_snapshot('<%= render "shared/header" %>', filename: "app/views/posts/index.html.erb", project_path: PROJECT_PATH, optimize: true, inline_render: true, escape: false)
+ end
+
+ test "inlines partial with locals" do
+ assert_compiled_snapshot('<%= render partial: "posts/card", locals: { title: @post.title } %>', filename: "app/views/posts/index.html.erb", project_path: PROJECT_PATH, optimize: true, inline_render: true, escape: false)
+ end
+
+ test "does not inline dynamic render" do
+ assert_compiled_snapshot("<%= render @product %>", filename: "app/views/posts/index.html.erb", project_path: PROJECT_PATH, optimize: true, inline_render: true, escape: false)
+ end
+
+ test "does not inline when optimize is false" do
+ assert_compiled_snapshot('<%= render "shared/header" %>', filename: "app/views/posts/index.html.erb", project_path: PROJECT_PATH, optimize: false, escape: false)
+ end
+
+ test "does not inline partial with yield" do
+ assert_compiled_snapshot('<%= render "shared/wrapper" %>', filename: "app/views/posts/index.html.erb", project_path: PROJECT_PATH, optimize: true, inline_render: true, escape: false)
+ end
+
+ test "does not inline partial with content_for" do
+ assert_compiled_snapshot('<%= render "shared/sidebar" %>', filename: "app/views/posts/index.html.erb", project_path: PROJECT_PATH, optimize: true, inline_render: true, escape: false)
+ end
+
+ test "does not inline partial with local_assigns" do
+ assert_compiled_snapshot('<%= render "shared/item" %>', filename: "app/views/posts/index.html.erb", project_path: PROJECT_PATH, optimize: true, inline_render: true, escape: false)
+ end
+
+ test "inlines collection renders" do
+ assert_compiled_snapshot('<%= render partial: "posts/post", collection: @posts %>', filename: "app/views/posts/index.html.erb", project_path: PROJECT_PATH, optimize: true, inline_render: true, escape: false)
+ end
+
+ test "inlines collection with as: option" do
+ assert_compiled_snapshot('<%= render partial: "posts/post", collection: @posts, as: :item %>', filename: "app/views/posts/index.html.erb", project_path: PROJECT_PATH, optimize: true, inline_render: true, escape: false)
+ end
+
+ test "does not inline collection with spacer_template" do
+ assert_compiled_snapshot('<%= render partial: "posts/post", collection: @posts, spacer_template: "posts/spacer" %>', filename: "app/views/posts/index.html.erb", project_path: PROJECT_PATH, optimize: true, inline_render: true, escape: false)
+ end
+
+ test "inlines nested partials" do
+ assert_compiled_snapshot('<%= render "shared/outer" %>', filename: "app/views/posts/index.html.erb", project_path: PROJECT_PATH, optimize: true, inline_render: true, escape: false)
+ end
+
+ test "detects circular references and falls back" do
+ assert_compiled_snapshot('<%= render "shared/a" %>', filename: "app/views/posts/index.html.erb", project_path: PROJECT_PATH, optimize: true, inline_render: true, escape: false)
+ end
+
+ test "scopes locals with begin/end block" do
+ assert_compiled_snapshot('<%= render partial: "posts/card", locals: { name: "test" } %>', filename: "app/views/posts/index.html.erb", project_path: PROJECT_PATH, optimize: true, inline_render: true, escape: false)
+ end
+
+ test "falls back for unresolvable partial" do
+ assert_compiled_snapshot('<%= render "nonexistent/missing" %>', filename: "app/views/posts/index.html.erb", project_path: PROJECT_PATH, optimize: true, inline_render: true, escape: false)
+ end
+
+ test "handles Ruby 3.1 shorthand hash locals" do
+ assert_compiled_snapshot('<%= render partial: "posts/card", locals: { title: } %>', filename: "app/views/posts/index.html.erb", project_path: PROJECT_PATH, optimize: true, inline_render: true, escape: false)
+ end
+
+ test "inlines shorthand render with inline locals" do
+ assert_compiled_snapshot('<%= render "posts/card", title: "Title" %>', filename: "app/views/posts/index.html.erb", project_path: PROJECT_PATH, optimize: true, inline_render: true, escape: false)
+ end
+
+ test "inlines shorthand render with multiple inline locals" do
+ assert_compiled_snapshot('<%= render "posts/card", title: "Title", name: @user.name %>', filename: "app/views/posts/index.html.erb", project_path: PROJECT_PATH, optimize: true, inline_render: true, escape: false)
+ end
+ end
+end
diff --git a/test/fixtures/render_inliner/app/views/posts/_card.html.erb b/test/fixtures/render_inliner/app/views/posts/_card.html.erb
new file mode 100644
index 000000000..90f33f56e
--- /dev/null
+++ b/test/fixtures/render_inliner/app/views/posts/_card.html.erb
@@ -0,0 +1 @@
+
<%= title %>
diff --git a/test/fixtures/render_inliner/app/views/posts/_post.html.erb b/test/fixtures/render_inliner/app/views/posts/_post.html.erb
new file mode 100644
index 000000000..fc9a37462
--- /dev/null
+++ b/test/fixtures/render_inliner/app/views/posts/_post.html.erb
@@ -0,0 +1 @@
+Post
diff --git a/test/fixtures/render_inliner/app/views/posts/_spacer.html.erb b/test/fixtures/render_inliner/app/views/posts/_spacer.html.erb
new file mode 100644
index 000000000..e123ba7e3
--- /dev/null
+++ b/test/fixtures/render_inliner/app/views/posts/_spacer.html.erb
@@ -0,0 +1 @@
+
diff --git a/test/fixtures/render_inliner/app/views/shared/_a.html.erb b/test/fixtures/render_inliner/app/views/shared/_a.html.erb
new file mode 100644
index 000000000..df26f4170
--- /dev/null
+++ b/test/fixtures/render_inliner/app/views/shared/_a.html.erb
@@ -0,0 +1 @@
+<%= render "shared/b" %>
diff --git a/test/fixtures/render_inliner/app/views/shared/_b.html.erb b/test/fixtures/render_inliner/app/views/shared/_b.html.erb
new file mode 100644
index 000000000..d5ed9a10b
--- /dev/null
+++ b/test/fixtures/render_inliner/app/views/shared/_b.html.erb
@@ -0,0 +1 @@
+<%= render "shared/a" %>
diff --git a/test/fixtures/render_inliner/app/views/shared/_header.html.erb b/test/fixtures/render_inliner/app/views/shared/_header.html.erb
new file mode 100644
index 000000000..ae6b431c0
--- /dev/null
+++ b/test/fixtures/render_inliner/app/views/shared/_header.html.erb
@@ -0,0 +1 @@
+
diff --git a/test/fixtures/render_inliner/app/views/shared/_inner.html.erb b/test/fixtures/render_inliner/app/views/shared/_inner.html.erb
new file mode 100644
index 000000000..291277aa0
--- /dev/null
+++ b/test/fixtures/render_inliner/app/views/shared/_inner.html.erb
@@ -0,0 +1 @@
+Inner
diff --git a/test/fixtures/render_inliner/app/views/shared/_item.html.erb b/test/fixtures/render_inliner/app/views/shared/_item.html.erb
new file mode 100644
index 000000000..66869596c
--- /dev/null
+++ b/test/fixtures/render_inliner/app/views/shared/_item.html.erb
@@ -0,0 +1 @@
+<%= local_assigns[:name] %>
diff --git a/test/fixtures/render_inliner/app/views/shared/_outer.html.erb b/test/fixtures/render_inliner/app/views/shared/_outer.html.erb
new file mode 100644
index 000000000..5d9eedc47
--- /dev/null
+++ b/test/fixtures/render_inliner/app/views/shared/_outer.html.erb
@@ -0,0 +1 @@
+Outer<%= render "shared/inner" %>
diff --git a/test/fixtures/render_inliner/app/views/shared/_sidebar.html.erb b/test/fixtures/render_inliner/app/views/shared/_sidebar.html.erb
new file mode 100644
index 000000000..86bce3b61
--- /dev/null
+++ b/test/fixtures/render_inliner/app/views/shared/_sidebar.html.erb
@@ -0,0 +1 @@
+<%= content_for :sidebar %>
diff --git a/test/fixtures/render_inliner/app/views/shared/_wrapper.html.erb b/test/fixtures/render_inliner/app/views/shared/_wrapper.html.erb
new file mode 100644
index 000000000..1c26cb986
--- /dev/null
+++ b/test/fixtures/render_inliner/app/views/shared/_wrapper.html.erb
@@ -0,0 +1 @@
+<%= yield %>
diff --git a/test/snapshots/engine/evaluation_test/test_0022_complex_real_world_example_d8e54f09b6cf5d8199c941a30266ede6.txt b/test/snapshots/engine/evaluation_test/test_0022_complex_real_world_example_d8e54f09b6cf5d8199c941a30266ede6.txt
new file mode 100644
index 000000000..722cb3b32
--- /dev/null
+++ b/test/snapshots/engine/evaluation_test/test_0022_complex_real_world_example_d8e54f09b6cf5d8199c941a30266ede6.txt
@@ -0,0 +1,66 @@
+---
+source: "Engine::EvaluationTest#test_0022_complex real world example"
+input: "{source: \"\\n\\n \\n <%= page_title %>\\n \\\">\\n \\n \\\">\\n \\n \\n \\n <% if flash_message %>\\n \\\">\\n <%= flash_message %>\\n
\\n <% end %>\\n \\n \\n <% unless posts.empty? %>\\n <% posts.each_with_index do |post, index| %>\\n \\\">\\n <%= post.title %>
\\n \\n By <%= post.author %> on <%= post.date.strftime(\\\"%B %d, %Y\\\") %>\\n
\\n \\n <%= post.excerpt %>\\n
\\n <% if post.tags.any? %>\\n \\n <% post.tags.each do |tag| %>\\n <%= tag %>\\n <% end %>\\n
\\n <% end %>\\n \\n <% end %>\\n <% else %>\\n No posts available.
\\n <% end %>\\n \\n \\n \\n \\n \\n\\n\", locals: {page_title: \"My Blog\", meta_description: \"A blog about programming\", body_classes: [\"blog\", \"home\"], site_name: \"My Awesome Blog\", navigation_items: [{title: \"Home\", url: \"/\", active: true}, {title: \"About\", url: \"/about\", active: false}, {title: \"Contact\", url: \"/contact\", active: false}], flash_message: \"Welcome!\", flash_type: \"success\", posts: [#, excerpt=\"This is the first post excerpt.\", tags=[\"ruby\", \"programming\"]>, #, excerpt=\"This is the second post excerpt.\", tags=[\"html\", \"css\"]>]}, options: {escape: false}}"
+---
+
+
+
+ My Blog
+
+
+
+
+
+
+
+ Welcome!
+
+
+
+
+ First Post
+
+ By John Doe on January 15, 2024
+
+
+ This is the first post excerpt.
+
+
+ ruby
+ programming
+
+
+
+ Second Post
+
+ By Jane Smith on February 01, 2024
+
+
+ This is the second post excerpt.
+
+
+ html
+ css
+
+
+
+
+
+
+
+
diff --git a/test/snapshots/engine/render_inliner_test/test_0001_inlines_a_simple_partial_603df3fe395b10fa9c997c61ff9a15f9.txt b/test/snapshots/engine/render_inliner_test/test_0001_inlines_a_simple_partial_603df3fe395b10fa9c997c61ff9a15f9.txt
new file mode 100644
index 000000000..c5059c766
--- /dev/null
+++ b/test/snapshots/engine/render_inliner_test/test_0001_inlines_a_simple_partial_603df3fe395b10fa9c997c61ff9a15f9.txt
@@ -0,0 +1,7 @@
+---
+source: "Engine::RenderInlinerTest#test_0001_inlines a simple partial"
+input: "{source: \"<%= render \\\"shared/header\\\" %>\", options: {filename: \"app/views/posts/index.html.erb\", project_path: \"test/fixtures/render_inliner\", optimize: true, inline_render: true, escape: false}}"
+---
+_buf = ::String.new; begin; ;; _buf << '
+'.freeze; ; end;;
+_buf.to_s
diff --git a/test/snapshots/engine/render_inliner_test/test_0002_inlines_partial_with_locals_95135940db611a0a5e761afc44f22616.txt b/test/snapshots/engine/render_inliner_test/test_0002_inlines_partial_with_locals_95135940db611a0a5e761afc44f22616.txt
new file mode 100644
index 000000000..a70a2abb4
--- /dev/null
+++ b/test/snapshots/engine/render_inliner_test/test_0002_inlines_partial_with_locals_95135940db611a0a5e761afc44f22616.txt
@@ -0,0 +1,7 @@
+---
+source: "Engine::RenderInlinerTest#test_0002_inlines partial with locals"
+input: "{source: \"<%= render partial: \\\"posts/card\\\", locals: { title: @post.title } %>\", options: {filename: \"app/views/posts/index.html.erb\", project_path: \"test/fixtures/render_inliner\", optimize: true, inline_render: true, escape: false}}"
+---
+_buf = ::String.new; begin; ; title = (@post.title); ;; _buf << ''.freeze; _buf << (title).to_s; _buf << '
+'.freeze; ; end;;
+_buf.to_s
diff --git a/test/snapshots/engine/render_inliner_test/test_0003_does_not_inline_dynamic_render_d4b9ae2333f9edd9d350a39182291bce.txt b/test/snapshots/engine/render_inliner_test/test_0003_does_not_inline_dynamic_render_d4b9ae2333f9edd9d350a39182291bce.txt
new file mode 100644
index 000000000..1b7771c4b
--- /dev/null
+++ b/test/snapshots/engine/render_inliner_test/test_0003_does_not_inline_dynamic_render_d4b9ae2333f9edd9d350a39182291bce.txt
@@ -0,0 +1,6 @@
+---
+source: "Engine::RenderInlinerTest#test_0003_does not inline dynamic render"
+input: "{source: \"<%= render @product %>\", options: {filename: \"app/views/posts/index.html.erb\", project_path: \"test/fixtures/render_inliner\", optimize: true, inline_render: true, escape: false}}"
+---
+_buf = ::String.new; _buf << (render @product).to_s;
+_buf.to_s
diff --git a/test/snapshots/engine/render_inliner_test/test_0004_does_not_inline_when_optimize_is_false_edfdb37e0ee42d3137ab9f6954974e6e.txt b/test/snapshots/engine/render_inliner_test/test_0004_does_not_inline_when_optimize_is_false_edfdb37e0ee42d3137ab9f6954974e6e.txt
new file mode 100644
index 000000000..e3ce4e2fc
--- /dev/null
+++ b/test/snapshots/engine/render_inliner_test/test_0004_does_not_inline_when_optimize_is_false_edfdb37e0ee42d3137ab9f6954974e6e.txt
@@ -0,0 +1,6 @@
+---
+source: "Engine::RenderInlinerTest#test_0004_does not inline when optimize is false"
+input: "{source: \"<%= render \\\"shared/header\\\" %>\", options: {filename: \"app/views/posts/index.html.erb\", project_path: \"test/fixtures/render_inliner\", optimize: false, escape: false}}"
+---
+_buf = ::String.new; _buf << (render "shared/header").to_s;
+_buf.to_s
diff --git a/test/snapshots/engine/render_inliner_test/test_0005_does_not_inline_partial_with_yield_614327996b5f80569bb8967a712a666d.txt b/test/snapshots/engine/render_inliner_test/test_0005_does_not_inline_partial_with_yield_614327996b5f80569bb8967a712a666d.txt
new file mode 100644
index 000000000..e865db3c4
--- /dev/null
+++ b/test/snapshots/engine/render_inliner_test/test_0005_does_not_inline_partial_with_yield_614327996b5f80569bb8967a712a666d.txt
@@ -0,0 +1,6 @@
+---
+source: "Engine::RenderInlinerTest#test_0005_does not inline partial with yield"
+input: "{source: \"<%= render \\\"shared/wrapper\\\" %>\", options: {filename: \"app/views/posts/index.html.erb\", project_path: \"test/fixtures/render_inliner\", optimize: true, inline_render: true, escape: false}}"
+---
+_buf = ::String.new; _buf << (render "shared/wrapper").to_s;
+_buf.to_s
diff --git a/test/snapshots/engine/render_inliner_test/test_0006_does_not_inline_partial_with_content_for_a745f9a05124c3f3ad68036a4bdd192f.txt b/test/snapshots/engine/render_inliner_test/test_0006_does_not_inline_partial_with_content_for_a745f9a05124c3f3ad68036a4bdd192f.txt
new file mode 100644
index 000000000..871e41eac
--- /dev/null
+++ b/test/snapshots/engine/render_inliner_test/test_0006_does_not_inline_partial_with_content_for_a745f9a05124c3f3ad68036a4bdd192f.txt
@@ -0,0 +1,6 @@
+---
+source: "Engine::RenderInlinerTest#test_0006_does not inline partial with content_for"
+input: "{source: \"<%= render \\\"shared/sidebar\\\" %>\", options: {filename: \"app/views/posts/index.html.erb\", project_path: \"test/fixtures/render_inliner\", optimize: true, inline_render: true, escape: false}}"
+---
+_buf = ::String.new; _buf << (render "shared/sidebar").to_s;
+_buf.to_s
diff --git a/test/snapshots/engine/render_inliner_test/test_0007_does_not_inline_partial_with_local_assigns_270738f467068b11565be247f854bf68.txt b/test/snapshots/engine/render_inliner_test/test_0007_does_not_inline_partial_with_local_assigns_270738f467068b11565be247f854bf68.txt
new file mode 100644
index 000000000..9e8974d30
--- /dev/null
+++ b/test/snapshots/engine/render_inliner_test/test_0007_does_not_inline_partial_with_local_assigns_270738f467068b11565be247f854bf68.txt
@@ -0,0 +1,6 @@
+---
+source: "Engine::RenderInlinerTest#test_0007_does not inline partial with local_assigns"
+input: "{source: \"<%= render \\\"shared/item\\\" %>\", options: {filename: \"app/views/posts/index.html.erb\", project_path: \"test/fixtures/render_inliner\", optimize: true, inline_render: true, escape: false}}"
+---
+_buf = ::String.new; _buf << (render "shared/item").to_s;
+_buf.to_s
diff --git a/test/snapshots/engine/render_inliner_test/test_0008_inlines_collection_renders_2b36b6c15d03c691c65df99d2dcbb6bf.txt b/test/snapshots/engine/render_inliner_test/test_0008_inlines_collection_renders_2b36b6c15d03c691c65df99d2dcbb6bf.txt
new file mode 100644
index 000000000..a59324efd
--- /dev/null
+++ b/test/snapshots/engine/render_inliner_test/test_0008_inlines_collection_renders_2b36b6c15d03c691c65df99d2dcbb6bf.txt
@@ -0,0 +1,7 @@
+---
+source: "Engine::RenderInlinerTest#test_0008_inlines collection renders"
+input: "{source: \"<%= render partial: \\\"posts/post\\\", collection: @posts %>\", options: {filename: \"app/views/posts/index.html.erb\", project_path: \"test/fixtures/render_inliner\", optimize: true, inline_render: true, escape: false}}"
+---
+_buf = ::String.new; ; __herb_collection = (@posts); __herb_collection.each_with_index do |post, post_counter|; ;; _buf << 'Post
+'.freeze; ; end;;
+_buf.to_s
diff --git a/test/snapshots/engine/render_inliner_test/test_0009_inlines_collection_with_as_option_c959e2228d7be537f50a0415f1c9b577.txt b/test/snapshots/engine/render_inliner_test/test_0009_inlines_collection_with_as_option_c959e2228d7be537f50a0415f1c9b577.txt
new file mode 100644
index 000000000..16f637679
--- /dev/null
+++ b/test/snapshots/engine/render_inliner_test/test_0009_inlines_collection_with_as_option_c959e2228d7be537f50a0415f1c9b577.txt
@@ -0,0 +1,7 @@
+---
+source: "Engine::RenderInlinerTest#test_0009_inlines collection with as: option"
+input: "{source: \"<%= render partial: \\\"posts/post\\\", collection: @posts, as: :item %>\", options: {filename: \"app/views/posts/index.html.erb\", project_path: \"test/fixtures/render_inliner\", optimize: true, inline_render: true, escape: false}}"
+---
+_buf = ::String.new; ; __herb_collection = (@posts); __herb_collection.each_with_index do |item, item_counter|; ;; _buf << 'Post
+'.freeze; ; end;;
+_buf.to_s
diff --git a/test/snapshots/engine/render_inliner_test/test_0010_does_not_inline_collection_with_spacer_template_1a8e2afa971bed617e1ca00a7b672f7e.txt b/test/snapshots/engine/render_inliner_test/test_0010_does_not_inline_collection_with_spacer_template_1a8e2afa971bed617e1ca00a7b672f7e.txt
new file mode 100644
index 000000000..446fa9d59
--- /dev/null
+++ b/test/snapshots/engine/render_inliner_test/test_0010_does_not_inline_collection_with_spacer_template_1a8e2afa971bed617e1ca00a7b672f7e.txt
@@ -0,0 +1,6 @@
+---
+source: "Engine::RenderInlinerTest#test_0010_does not inline collection with spacer_template"
+input: "{source: \"<%= render partial: \\\"posts/post\\\", collection: @posts, spacer_template: \\\"posts/spacer\\\" %>\", options: {filename: \"app/views/posts/index.html.erb\", project_path: \"test/fixtures/render_inliner\", optimize: true, inline_render: true, escape: false}}"
+---
+_buf = ::String.new; _buf << (render partial: "posts/post", collection: @posts, spacer_template: "posts/spacer").to_s;
+_buf.to_s
diff --git a/test/snapshots/engine/render_inliner_test/test_0011_inlines_nested_partials_d1b017469e1b907e2043c278abeef1a5.txt b/test/snapshots/engine/render_inliner_test/test_0011_inlines_nested_partials_d1b017469e1b907e2043c278abeef1a5.txt
new file mode 100644
index 000000000..7a223bb97
--- /dev/null
+++ b/test/snapshots/engine/render_inliner_test/test_0011_inlines_nested_partials_d1b017469e1b907e2043c278abeef1a5.txt
@@ -0,0 +1,8 @@
+---
+source: "Engine::RenderInlinerTest#test_0011_inlines nested partials"
+input: "{source: \"<%= render \\\"shared/outer\\\" %>\", options: {filename: \"app/views/posts/index.html.erb\", project_path: \"test/fixtures/render_inliner\", optimize: true, inline_render: true, escape: false}}"
+---
+_buf = ::String.new; begin; ;; _buf << 'Outer'.freeze; begin; ;; _buf << 'Inner
+'.freeze; ; end;; _buf << '
+'.freeze; ; end;;
+_buf.to_s
diff --git a/test/snapshots/engine/render_inliner_test/test_0012_detects_circular_references_and_falls_back_ffa0077aac262faa659ea5be61da7f83.txt b/test/snapshots/engine/render_inliner_test/test_0012_detects_circular_references_and_falls_back_ffa0077aac262faa659ea5be61da7f83.txt
new file mode 100644
index 000000000..937351609
--- /dev/null
+++ b/test/snapshots/engine/render_inliner_test/test_0012_detects_circular_references_and_falls_back_ffa0077aac262faa659ea5be61da7f83.txt
@@ -0,0 +1,8 @@
+---
+source: "Engine::RenderInlinerTest#test_0012_detects circular references and falls back"
+input: "{source: \"<%= render \\\"shared/a\\\" %>\", options: {filename: \"app/views/posts/index.html.erb\", project_path: \"test/fixtures/render_inliner\", optimize: true, inline_render: true, escape: false}}"
+---
+_buf = ::String.new; begin; ;; begin; ;; _buf << (render "shared/a").to_s; _buf << '
+'.freeze; ; end;; _buf << '
+'.freeze; ; end;;
+_buf.to_s
diff --git a/test/snapshots/engine/render_inliner_test/test_0013_scopes_locals_with_begin_end_block_eed6faed6d9f587a049f5e651c305a4c.txt b/test/snapshots/engine/render_inliner_test/test_0013_scopes_locals_with_begin_end_block_eed6faed6d9f587a049f5e651c305a4c.txt
new file mode 100644
index 000000000..185818a72
--- /dev/null
+++ b/test/snapshots/engine/render_inliner_test/test_0013_scopes_locals_with_begin_end_block_eed6faed6d9f587a049f5e651c305a4c.txt
@@ -0,0 +1,7 @@
+---
+source: "Engine::RenderInlinerTest#test_0013_scopes locals with begin/end block"
+input: "{source: \"<%= render partial: \\\"posts/card\\\", locals: { name: \\\"test\\\" } %>\", options: {filename: \"app/views/posts/index.html.erb\", project_path: \"test/fixtures/render_inliner\", optimize: true, inline_render: true, escape: false}}"
+---
+_buf = ::String.new; begin; ; name = ("test"); ;; _buf << ''.freeze; _buf << (title).to_s; _buf << '
+'.freeze; ; end;;
+_buf.to_s
diff --git a/test/snapshots/engine/render_inliner_test/test_0014_falls_back_for_unresolvable_partial_6b2741fc46cb17dbbbaab71cefbf48dd.txt b/test/snapshots/engine/render_inliner_test/test_0014_falls_back_for_unresolvable_partial_6b2741fc46cb17dbbbaab71cefbf48dd.txt
new file mode 100644
index 000000000..7f0b14cb5
--- /dev/null
+++ b/test/snapshots/engine/render_inliner_test/test_0014_falls_back_for_unresolvable_partial_6b2741fc46cb17dbbbaab71cefbf48dd.txt
@@ -0,0 +1,6 @@
+---
+source: "Engine::RenderInlinerTest#test_0014_falls back for unresolvable partial"
+input: "{source: \"<%= render \\\"nonexistent/missing\\\" %>\", options: {filename: \"app/views/posts/index.html.erb\", project_path: \"test/fixtures/render_inliner\", optimize: true, inline_render: true, escape: false}}"
+---
+_buf = ::String.new; _buf << (render "nonexistent/missing").to_s;
+_buf.to_s
diff --git a/test/snapshots/engine/render_inliner_test/test_0015_handles_Ruby_3.1_shorthand_hash_locals_37b2a50c3b7db3cfeff62565497a8174.txt b/test/snapshots/engine/render_inliner_test/test_0015_handles_Ruby_3.1_shorthand_hash_locals_37b2a50c3b7db3cfeff62565497a8174.txt
new file mode 100644
index 000000000..4df5919fd
--- /dev/null
+++ b/test/snapshots/engine/render_inliner_test/test_0015_handles_Ruby_3.1_shorthand_hash_locals_37b2a50c3b7db3cfeff62565497a8174.txt
@@ -0,0 +1,7 @@
+---
+source: "Engine::RenderInlinerTest#test_0015_handles Ruby 3.1 shorthand hash locals"
+input: "{source: \"<%= render partial: \\\"posts/card\\\", locals: { title: } %>\", options: {filename: \"app/views/posts/index.html.erb\", project_path: \"test/fixtures/render_inliner\", optimize: true, inline_render: true, escape: false}}"
+---
+_buf = ::String.new; begin; ; title = (title); ;; _buf << ''.freeze; _buf << (title).to_s; _buf << '
+'.freeze; ; end;;
+_buf.to_s
diff --git a/test/snapshots/engine/render_inliner_test/test_0016_inlines_shorthand_render_with_inline_locals_b86766e862c57e3e004ab4073580e1ce.txt b/test/snapshots/engine/render_inliner_test/test_0016_inlines_shorthand_render_with_inline_locals_b86766e862c57e3e004ab4073580e1ce.txt
new file mode 100644
index 000000000..ecb7df640
--- /dev/null
+++ b/test/snapshots/engine/render_inliner_test/test_0016_inlines_shorthand_render_with_inline_locals_b86766e862c57e3e004ab4073580e1ce.txt
@@ -0,0 +1,7 @@
+---
+source: "Engine::RenderInlinerTest#test_0016_inlines shorthand render with inline locals"
+input: "{source: \"<%= render \\\"posts/card\\\", title: \\\"Title\\\" %>\", options: {filename: \"app/views/posts/index.html.erb\", project_path: \"test/fixtures/render_inliner\", optimize: true, inline_render: true, escape: false}}"
+---
+_buf = ::String.new; begin; ; title = ("Title"); ;; _buf << ''.freeze; _buf << (title).to_s; _buf << '
+'.freeze; ; end;;
+_buf.to_s
diff --git a/test/snapshots/engine/render_inliner_test/test_0017_inlines_shorthand_render_with_multiple_inline_locals_ff0e362cc9121c9f7c204ba9fcad8dbe.txt b/test/snapshots/engine/render_inliner_test/test_0017_inlines_shorthand_render_with_multiple_inline_locals_ff0e362cc9121c9f7c204ba9fcad8dbe.txt
new file mode 100644
index 000000000..34aa3b2ca
--- /dev/null
+++ b/test/snapshots/engine/render_inliner_test/test_0017_inlines_shorthand_render_with_multiple_inline_locals_ff0e362cc9121c9f7c204ba9fcad8dbe.txt
@@ -0,0 +1,7 @@
+---
+source: "Engine::RenderInlinerTest#test_0017_inlines shorthand render with multiple inline locals"
+input: "{source: \"<%= render \\\"posts/card\\\", title: \\\"Title\\\", name: @user.name %>\", options: {filename: \"app/views/posts/index.html.erb\", project_path: \"test/fixtures/render_inliner\", optimize: true, inline_render: true, escape: false}}"
+---
+_buf = ::String.new; begin; ; title = ("Title"); ; name = (@user.name); ;; _buf << ''.freeze; _buf << (title).to_s; _buf << '
+'.freeze; ; end;;
+_buf.to_s