Repository: yippee-fun/phlex (ruby)

Issue:
Compiled components



Discussion:

Patch:
diff --git a/.rubocop.yml b/.rubocop.yml
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -4,3 +4,20 @@ inherit_from:
 
 AllCops:
   TargetRubyVersion: 2.7
+
+Style/PercentLiteralDelimiters:
+  Enabled: false
+
+Layout/CaseIndentation:
+  Enabled: false
+
+Style/StringConcatenation:
+  Enabled: false
+
+
+
+Security/Eval:
+  Enabled: false
+
+Style/MethodCallWithoutArgsParentheses:
+  Enabled: false
diff --git a/bench.rb b/bench.rb
--- a/bench.rb
+++ b/bench.rb
@@ -9,6 +9,13 @@
 
 puts RUBY_DESCRIPTION
 
+a = Example::Page.new.call
+# Example::Page.compile
+# Example::LayoutComponent.compile
+b = Example::Page.new.call
+
+raise unless a == b
+
 Benchmark.ips do |x|
 	x.report("Page") { Example::Page.new.call }
 end
diff --git a/fixtures/compilation/vcall.rb b/fixtures/compilation/vcall.rb
deleted file mode 100644
--- a/fixtures/compilation/vcall.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-module Fixtures
-	module Compilation
-		module VCall
-			class WithStandardElement < Phlex::View
-				def template
-					div
-				end
-			end
-
-			class WithVoidElement < Phlex::View
-				def template
-					img
-				end
-			end
-
-			class WithAnotherMethodCall < Phlex::View
-				def template
-					article
-					some_other_method
-					article
-				end
-			end
-
-			class WithRedefinedTagMethod < Phlex::View
-				def template
-					title
-					article
-				end
-
-				def title
-					h1
-				end
-			end
-		end
-	end
-end
diff --git a/fixtures/content.rb b/fixtures/content.rb
new file mode 100644
--- /dev/null
+++ b/fixtures/content.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module Fixtures
+	module Content
+		class BareString < Phlex::View
+			def template
+				h1 { "Hello" }
+			end
+		end
+
+		class Symbol < Phlex::View
+			def template
+				h1 { :hello }
+			end
+		end
+
+		class Float < Phlex::View
+			def template
+				h1 { 1.2 }
+			end
+		end
+
+		class Integer < Phlex::View
+			def template
+				h1 { 1 }
+			end
+		end
+
+		class Variable < Phlex::View
+			def template
+				greeting = "Hello"
+				h1 { greeting }
+			end
+		end
+
+		class InstanceVariable < Phlex::View
+			def template
+				h1 { @hello }
+			end
+		end
+
+		class NestedTags < Phlex::View
+			def template
+				article {
+					h1 { "Inside" }
+				}
+			end
+		end
+
+		class NonMutatingNestedContent < Phlex::View
+			def template
+				div { say_hello }
+			end
+
+			def say_hello
+				"Hello"
+			end
+		end
+	end
+end
diff --git a/fixtures/page.rb b/fixtures/page.rb
--- a/fixtures/page.rb
+++ b/fixtures/page.rb
@@ -6,32 +6,26 @@ def template
 			render LayoutComponent.new do
 				h1 { "Hi" }
 
-				5.times do
-					div do
-						10.times do
-							a(href: "something", unique: SecureRandom.uuid, data: { value: 1 }) { "Test" }
+				table id: "test", class: "a b c d e f g" do
+					tr do
+						td id: "test", class: "a b c d e f g" do
+							span { "Hi" }
 						end
-					end
-				end
 
-				table do
-					thead do
-						10.times do
-							tr do
-								th { "Hi" }
-							end
+						td id: "test", class: "a b c d e f g" do
+							span { "Hi" }
+						end
+
+						td id: "test", class: "a b c d e f g" do
+							span { "Hi" }
+						end
+
+						td id: "test", class: "a b c d e f g" do
+							span { "Hi" }
 						end
-					end
 
-					tbody do
-						100.times do
-							tr class: "a b c d e f g", id: "something" do
-								10.times do
-									td class: "f g h i j k l" do
-										span { "Test" }
-									end
-								end
-							end
+						td id: "test", class: "a b c d e f g" do
+							span { "Hi" }
 						end
 					end
 				end
diff --git a/fixtures/standard_element.rb b/fixtures/standard_element.rb
new file mode 100644
--- /dev/null
+++ b/fixtures/standard_element.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+module Fixtures
+	module StandardElement
+		class WithParens < Phlex::View
+			def template
+				h1()
+			end
+		end
+
+		class WithoutParens < Phlex::View
+			def template
+				h1
+			end
+		end
+
+		module WithAttributes
+			class WithParens < Phlex::View
+				def template
+					h1(class: "font-bold")
+				end
+			end
+
+			class WithoutParens < Phlex::View
+				def template
+					h1 class: "font-bold"
+				end
+			end
+		end
+
+		module WithBraceBlock
+			class WithParens < Phlex::View
+				def template
+					h1() { "Hi" }
+				end
+			end
+
+			class WithoutParens < Phlex::View
+				def template
+					h1 { "Hi" }
+				end
+			end
+
+			class WithAttributes < Phlex::View
+				def template
+					h1(class: "font-bold") { "Hi" }
+				end
+			end
+		end
+
+		module WithDoBlock
+			class WithParens < Phlex::View
+				def template
+					h1() do
+						"Hi"
+					end
+				end
+			end
+
+			class WithoutParens < Phlex::View
+				def template
+					h1 do
+						"Hi"
+					end
+				end
+			end
+
+			module WithAttributes
+				class WithParens < Phlex::View
+					def template
+						h1(class: "font-bold") do
+							"Hi"
+						end
+					end
+				end
+
+				class WithoutParens < Phlex::View
+					def template
+						h1 class: "font-bold" do
+							"Hi"
+						end
+					end
+				end
+			end
+		end
+	end
+end
diff --git a/fixtures/void_element.rb b/fixtures/void_element.rb
new file mode 100644
--- /dev/null
+++ b/fixtures/void_element.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Fixtures
+	module VoidElement
+		class WithParens < Phlex::View
+			def template
+				img()
+			end
+		end
+
+		class WithoutParens < Phlex::View
+			def template
+				img
+			end
+		end
+
+		module WithAttributes
+			class WithParens < Phlex::View
+				def template
+					img(class: "a b c")
+				end
+			end
+
+			class WithoutParens < Phlex::View
+				def template
+					img class: "a b c"
+				end
+			end
+		end
+	end
+end
diff --git a/lib/phlex/compiler.rb b/lib/phlex/compiler.rb
--- a/lib/phlex/compiler.rb
+++ b/lib/phlex/compiler.rb
@@ -6,6 +6,8 @@ def initialize(view)
 			@view = view
 		end
 
+		attr_writer :scope
+
 		def inspect
 			"#{self.class.name} for #{@view.name} view class"
 		end
@@ -14,6 +16,10 @@ def call
 			Visitors::File.new(self).visit(tree)
 		end
 
+		def tag_method?(method_name)
+			(HTML::STANDARD_ELEMENTS.key?(method_name) || HTML::VOID_ELEMENTS.key?(method_name)) && !redefined?(method_name)
+		end
+
 		def redefined?(method_name)
 			prototype = @view.allocate
 
@@ -21,8 +27,22 @@ def redefined?(method_name)
 				Phlex::View.instance_method(method_name).bind(prototype)
 		end
 
-		def redefine(method)
-			@view.class_eval(method)
+		def redefine(method, line:)
+			patch = scope + method + unscope
+			eval(patch, Kernel.binding, file, (line - 1))
+		end
+
+		def scope
+			@scope.map do |scope|
+				case scope
+					in SyntaxTree::ModuleDeclaration then "module #{scope.constant.constant.value};"
+					in SyntaxTree::ClassDeclaration then "class #{scope.constant.constant.value};"
+				end
+			end.join + "\n"
+		end
+
+		def unscope
+			"; end" * @scope.size
 		end
 
 		def line
diff --git a/lib/phlex/compiler/elements.rb b/lib/phlex/compiler/elements.rb
new file mode 100644
--- /dev/null
+++ b/lib/phlex/compiler/elements.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module Phlex::Compiler::Elements
+	module VCall
+		def format(formatter)
+			Phlex::Compiler::Generators::Element.new(
+				Phlex::Compiler::Nodes::VCall.new(self),
+				formatter: formatter
+			).call
+		end
+	end
+
+	module FCall
+		def format(formatter)
+			Phlex::Compiler::Generators::Element.new(
+				Phlex::Compiler::Nodes::FCall.new(self),
+				formatter: formatter
+			).call
+		end
+	end
+
+	module Command
+		def format(formatter)
+			Phlex::Compiler::Generators::Element.new(
+				Phlex::Compiler::Nodes::Command.new(self),
+				formatter: formatter
+			).call
+		end
+	end
+
+	module MutatingMethodAddBlock
+		def format(formatter)
+			Phlex::Compiler::Generators::Element.new(
+				Phlex::Compiler::Nodes::MethodAddBlock.new(self),
+				formatter: formatter,
+				mutating: true
+			).call
+		end
+	end
+
+	module MethodAddBlock
+		def format(formatter)
+			Phlex::Compiler::Generators::Element.new(
+				Phlex::Compiler::Nodes::MethodAddBlock.new(self),
+				formatter: formatter
+			).call
+		end
+	end
+end
diff --git a/lib/phlex/compiler/generators/content.rb b/lib/phlex/compiler/generators/content.rb
new file mode 100644
--- /dev/null
+++ b/lib/phlex/compiler/generators/content.rb
@@ -0,0 +1,103 @@
+# frozen_string_literal: true
+
+module Phlex
+	class Compiler
+		module Generators
+			class Content
+				def initialize(formatter, content:, mutating: false)
+					@formatter = formatter
+					@content = content
+					@mutating = mutating
+				end
+
+				def call
+					return if nil_value?
+					return bare_string_value if bare_string_value?
+					return symbol_value if symbol_value?
+					return numeric_value if numeric_value?
+					return variable_value if variable_value?
+
+					unknown_value
+				end
+
+				private
+
+				def nil_value?
+					case @content
+					in SyntaxTree::VarRef[value: SyntaxTree::Kw[value: "nil"]]
+						true
+					else
+						false
+					end
+				end
+
+				def bare_string_value?
+					case @content
+					in SyntaxTree::StringLiteral[parts: [SyntaxTree::TStringContent]]
+						true
+					else
+						false
+					end
+				end
+
+				def symbol_value?
+					@content.is_a?(SyntaxTree::SymbolLiteral)
+				end
+
+				def numeric_value?
+					@content.is_a?(SyntaxTree::Int) || @content.is_a?(SyntaxTree::FloatLiteral)
+				end
+
+				def variable_value?
+					@content.is_a?(SyntaxTree::VarRef)
+				end
+
+				def bare_string_value
+					@formatter.append do |f|
+						f.text CGI.escape_html(
+							@content.parts.first.value
+						)
+					end
+				end
+
+				def symbol_value
+					@formatter.append do |f|
+						f.text CGI.escape_html(
+							@content.value.value
+						)
+					end
+				end
+
+				def numeric_value
+					@formatter.append do |f|
+						f.text CGI.escape_html(
+							@content.value
+						)
+					end
+				end
+
+				def variable_value
+					@formatter.chain_append do |f|
+						f.text "CGI.escape_html("
+						@content.format(f)
+						f.text ")"
+					end
+				end
+
+				def unknown_value
+					@formatter.breakable(force: true)
+					if @mutating
+						@content.format(@formatter)
+					else
+						@formatter.text "yield_content {"
+						@formatter.breakable(force: true)
+						@content.format(@formatter)
+						@formatter.breakable(force: true)
+						@formatter.text "}"
+					end
+					@formatter.breakable(force: true)
+				end
+			end
+		end
+	end
+end
diff --git a/lib/phlex/compiler/generators/element.rb b/lib/phlex/compiler/generators/element.rb
new file mode 100644
--- /dev/null
+++ b/lib/phlex/compiler/generators/element.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module Phlex
+	class Compiler
+		module Generators
+			class Element
+				def initialize(node, formatter:, mutating: false)
+					@node = node
+					@formatter = formatter
+					@mutating = mutating
+				end
+
+				def call
+					@formatter.append do |f|
+						f.text "<"
+						f.text tag
+					end
+
+					if @node.arguments&.parts&.any?
+						@formatter.chain_append do |f|
+							f.text "fetch_attrs("
+							@node.arguments.format(@formatter)
+							f.text ")"
+						end
+					end
+
+					@formatter.append do |f|
+						f.text ">"
+					end
+
+					return if void?
+
+					case @node.content
+					in SyntaxTree::Statements[body: [c]]
+						Content.new(@formatter, content: c, mutating: @mutating).call
+					in nil
+						nil
+					else
+						@node.content.format(@formatter)
+					end
+
+					@formatter.append do |f|
+						f.text "</"
+						f.text tag
+						f.text ">"
+					end
+				end
+
+				private
+
+				def tag
+					HTML::STANDARD_ELEMENTS[@node.name] || HTML::VOID_ELEMENTS[@node.name]
+				end
+
+				def void?
+					HTML::VOID_ELEMENTS.key?(@node.name)
+				end
+			end
+		end
+	end
+end
diff --git a/lib/phlex/compiler/generators/standard_element.rb b/lib/phlex/compiler/generators/standard_element.rb
deleted file mode 100644
--- a/lib/phlex/compiler/generators/standard_element.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-module Phlex
-	class Compiler
-		module Generators
-			class StandardElement
-				def initialize(formatter, method_name:, arguments: nil)
-					@formatter = formatter
-					@method_name = method_name
-				end
-
-				def call
-					@formatter.append do |f|
-						f.text "<"
-						f.text tag
-						f.text ">"
-
-						f.text "</"
-						f.text tag
-						f.text ">"
-					end
-				end
-
-				def tag
-					HTML::STANDARD_ELEMENTS[@method_name]
-				end
-			end
-		end
-	end
-end
diff --git a/lib/phlex/compiler/generators/void_element.rb b/lib/phlex/compiler/generators/void_element.rb
deleted file mode 100644
--- a/lib/phlex/compiler/generators/void_element.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-module Phlex
-	class Compiler
-		module Generators
-			class VoidElement
-				def initialize(formatter, method_name:, arguments: nil)
-					@formatter = formatter
-					@method_name = method_name
-					@arguments = arguments
-				end
-
-				def call
-					@formatter.append do |f|
-						f.text "<"
-						f.text tag
-						f.text " />"
-					end
-				end
-
-				private
-
-				def tag
-					HTML::VOID_ELEMENTS[@method_name]
-				end
-			end
-		end
-	end
-end
diff --git a/lib/phlex/compiler/nodes/base.rb b/lib/phlex/compiler/nodes/base.rb
new file mode 100644
--- /dev/null
+++ b/lib/phlex/compiler/nodes/base.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Phlex::Compiler::Nodes
+	class Base
+		def initialize(node)
+			@node = node
+		end
+
+		attr_accessor :node
+
+		def arguments
+			nil
+		end
+
+		def content
+			nil
+		end
+	end
+end
diff --git a/lib/phlex/compiler/nodes/call.rb b/lib/phlex/compiler/nodes/call.rb
new file mode 100644
--- /dev/null
+++ b/lib/phlex/compiler/nodes/call.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Phlex::Compiler::Nodes
+	class Call < Base
+		def name
+			nil
+		end
+	end
+end
diff --git a/lib/phlex/compiler/nodes/command.rb b/lib/phlex/compiler/nodes/command.rb
new file mode 100644
--- /dev/null
+++ b/lib/phlex/compiler/nodes/command.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Phlex::Compiler::Nodes
+	class Command < Base
+		def name
+			@node.message.value.to_sym
+		end
+
+		def arguments
+			@node.arguments
+		end
+	end
+end
diff --git a/lib/phlex/compiler/nodes/fcall.rb b/lib/phlex/compiler/nodes/fcall.rb
new file mode 100644
--- /dev/null
+++ b/lib/phlex/compiler/nodes/fcall.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Phlex::Compiler::Nodes
+	class FCall < Base
+		def name
+			@node.value.value.to_sym
+		end
+
+		def arguments
+			case @node.arguments
+			in SyntaxTree::Args
+				@node.arguments
+			in SyntaxTree::ArgParen
+				@node.arguments.arguments
+			end
+		end
+	end
+end
diff --git a/lib/phlex/compiler/nodes/method_add_block.rb b/lib/phlex/compiler/nodes/method_add_block.rb
new file mode 100644
--- /dev/null
+++ b/lib/phlex/compiler/nodes/method_add_block.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Phlex::Compiler::Nodes
+	class MethodAddBlock < Base
+		def name
+			method_call.name
+		end
+
+		def arguments
+			method_call.arguments
+		end
+
+		def method_call
+			@method_call ||= case @node.call
+			in SyntaxTree::FCall
+				Phlex::Compiler::Nodes::FCall.new(@node.call)
+			in SyntaxTree::Command
+				Phlex::Compiler::Nodes::Command.new(@node.call)
+			in SyntaxTree::Call
+				Phlex::Compiler::Nodes::Call.new(@node.call)
+			end
+		end
+
+		def content
+			case @node.block
+			in SyntaxTree::BraceBlock
+				@node.block.statements
+			in SyntaxTree::DoBlock
+				@node.block.bodystmt.statements
+			end
+		end
+	end
+end
diff --git a/lib/phlex/compiler/nodes/vcall.rb b/lib/phlex/compiler/nodes/vcall.rb
new file mode 100644
--- /dev/null
+++ b/lib/phlex/compiler/nodes/vcall.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Phlex::Compiler::Nodes
+	class VCall < Base
+		def name
+			@node.value.value.to_sym
+		end
+	end
+end
diff --git a/lib/phlex/compiler/optimizer.rb b/lib/phlex/compiler/optimizer.rb
new file mode 100644
--- /dev/null
+++ b/lib/phlex/compiler/optimizer.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+class Phlex::Compiler
+	class Optimizer
+		def initialize(node, compiler:)
+			@node = node
+			@compiler = compiler
+		end
+
+		def call
+			return optimize_element if optimize_element?
+
+			false
+		end
+
+		private
+
+		def optimize_element
+			case @node
+			in Nodes::VCall
+				@node.node.extend(Phlex::Compiler::Elements::VCall)
+			in Nodes::FCall
+				@node.node.extend(Phlex::Compiler::Elements::FCall)
+			in Nodes::Command
+				@node.node.extend(Phlex::Compiler::Elements::Command)
+			in Nodes::MethodAddBlock
+				optimize_add_method_block_element
+			end
+
+			true
+		end
+
+		def optimize_add_method_block_element
+			visitor = Phlex::Compiler::Visitors::Statements.new(@compiler)
+			visitor.visit(@node.content)
+
+			if visitor.mutating?
+				@node.node.extend(Phlex::Compiler::Elements::MutatingMethodAddBlock)
+			else
+				@node.node.extend(Phlex::Compiler::Elements::MethodAddBlock)
+			end
+
+			Phlex::Compiler::Visitors::ViewMethod.new(@compiler).visit(@node.content)
+		end
+
+		def optimize_element?
+			element? && !redefined?
+		end
+
+		def element?
+			standard_element? || void_element?
+		end
+
+		def redefined?
+			@compiler.redefined?(@node.name)
+		end
+
+		def standard_element?
+			Phlex::HTML::STANDARD_ELEMENTS.key?(@node.name)
+		end
+
+		def void_element?
+			Phlex::HTML::VOID_ELEMENTS.key?(@node.name)
+		end
+	end
+end
diff --git a/lib/phlex/compiler/optimizers/base_optimizer.rb b/lib/phlex/compiler/optimizers/base_optimizer.rb
deleted file mode 100644
--- a/lib/phlex/compiler/optimizers/base_optimizer.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-# frozen_string_literal: true
-
-module Phlex
-	class Compiler
-		module Optimizers
-			class BaseOptimizer
-				def initialize(node, compiler:)
-					@node = node
-					@compiler = compiler
-				end
-
-				def call
-					if standard_element?
-						@node.extend(self.class::StandardElement)
-					elsif void_element?
-						@node.extend(self.class::VoidElement)
-					else
-						false
-					end
-				end
-
-				private
-
-				def standard_element?
-					HTML::STANDARD_ELEMENTS[name] && !@compiler.redefined?(name)
-				end
-
-				def void_element?
-					HTML::VOID_ELEMENTS[name] && !@compiler.redefined?(name)
-				end
-			end
-		end
-	end
-end
diff --git a/lib/phlex/compiler/optimizers/vcall.rb b/lib/phlex/compiler/optimizers/vcall.rb
deleted file mode 100644
--- a/lib/phlex/compiler/optimizers/vcall.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-module Phlex
-	class Compiler
-		module Optimizers
-			class VCall < BaseOptimizer
-				module StandardElement
-					def format(formatter)
-						Generators::StandardElement.new(formatter,
-							method_name: value.value.to_sym).call
-					end
-				end
-
-				module VoidElement
-					def format(formatter)
-						Generators::VoidElement.new(formatter,
-							method_name: value.value.to_sym).call
-					end
-				end
-
-				private
-
-				def name
-					@node.value.value.to_sym
-				end
-			end
-		end
-	end
-end
diff --git a/lib/phlex/compiler/visitors/base.rb b/lib/phlex/compiler/visitors/base.rb
new file mode 100644
--- /dev/null
+++ b/lib/phlex/compiler/visitors/base.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Phlex::Compiler::Visitors
+	class Base < SyntaxTree::Visitor
+		def initialize(compiler = nil)
+			@compiler = compiler
+		end
+
+		private
+
+		def format(node)
+			Phlex::Compiler::Formatter.format("", node)
+		end
+	end
+end
diff --git a/lib/phlex/compiler/visitors/base_visitor.rb b/lib/phlex/compiler/visitors/base_visitor.rb
deleted file mode 100644
--- a/lib/phlex/compiler/visitors/base_visitor.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-module Phlex
-	class Compiler
-		module Visitors
-			class BaseVisitor < SyntaxTree::Visitor
-				def initialize(compiler = nil)
-					@compiler = compiler
-				end
-
-				private
-
-				def format(node)
-					Formatter.format("", node)
-				end
-			end
-		end
-	end
-end
diff --git a/lib/phlex/compiler/visitors/component.rb b/lib/phlex/compiler/visitors/component.rb
deleted file mode 100644
--- a/lib/phlex/compiler/visitors/component.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-module Phlex
-	class Compiler
-		module Visitors
-			class Component < BaseVisitor
-				visit_method def visit_def(node)
-					visitor = Visitors::ComponentMethod.new(@compiler)
-					visitor.visit_all(node.child_nodes)
-
-					if visitor.optimized_something?
-						@compiler.redefine(
-							format(node)
-						)
-					end
-				end
-
-				visit_method def visit_class(node)
-					nil
-				end
-
-				visit_method def visit_module(node)
-					nil
-				end
-			end
-		end
-	end
-end
diff --git a/lib/phlex/compiler/visitors/component_method.rb b/lib/phlex/compiler/visitors/component_method.rb
deleted file mode 100644
--- a/lib/phlex/compiler/visitors/component_method.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-module Phlex
-	class Compiler
-		module Visitors
-			class ComponentMethod < BaseVisitor
-				def optimized_something?
-					!!@optimized_something
-				end
-
-				visit_method def visit_vcall(node)
-					@optimized_something = Optimizers::VCall.new(node,
-						compiler: @compiler).call
-
-					super
-				end
-
-				visit_method def visit_class(node)
-					nil
-				end
-
-				visit_method def visit_module(node)
-					nil
-				end
-			end
-		end
-	end
-end
diff --git a/lib/phlex/compiler/visitors/file.rb b/lib/phlex/compiler/visitors/file.rb
--- a/lib/phlex/compiler/visitors/file.rb
+++ b/lib/phlex/compiler/visitors/file.rb
@@ -1,17 +1,29 @@
 # frozen_string_literal: true
 
-module Phlex
-	class Compiler
-		module Visitors
-			class File < BaseVisitor
-				visit_method def visit_class(node)
-					if node.location.start_line == @compiler.line
-						Visitors::Component.new(@compiler).visit_all(node.child_nodes)
-					else
-						super
-					end
-				end
+module Phlex::Compiler::Visitors
+	class File < Base
+		def initialize(compiler)
+			@scope = []
+			super
+		end
+
+		visit_method def visit_class(node)
+			@scope.push(node)
+
+			if node.location.start_line == @compiler.line
+				@compiler.scope = @scope
+				View.new(@compiler).visit_all(node.child_nodes)
+			else
+				super
 			end
+
+			@scope.pop
+		end
+
+		visit_method def visit_module(node)
+			@scope.push(node)
+			super
+			@scope.pop
 		end
 	end
 end
diff --git a/lib/phlex/compiler/visitors/stable_scope.rb b/lib/phlex/compiler/visitors/stable_scope.rb
new file mode 100644
--- /dev/null
+++ b/lib/phlex/compiler/visitors/stable_scope.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+# A mixin for visitors that stops them from visiting other scopes.
+
+module Phlex::Compiler::Visitors::StableScope
+	def visit_class(node)
+		nil
+	end
+
+	def visit_module(node)
+		nil
+	end
+
+	def visit_brace_block(node)
+		nil
+	end
+
+	def visit_do_block(node)
+		nil
+	end
+
+	def visit_method_add_block(node)
+		node = Phlex::Compiler::Nodes::MethodAddBlock.new(node)
+		if node.method_call.name == :render
+			visit(node.content)
+		end
+	end
+end
diff --git a/lib/phlex/compiler/visitors/statements.rb b/lib/phlex/compiler/visitors/statements.rb
new file mode 100644
--- /dev/null
+++ b/lib/phlex/compiler/visitors/statements.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Phlex::Compiler::Visitors
+	class Statements < Base
+		MUTATING_METHODS = [:raw, :whitespace, :comment, :text, :doctype]
+
+		include StableScope
+
+		def mutating?
+			!!@mutating
+		end
+
+		visit_method def visit_vcall(node)
+			check Phlex::Compiler::Nodes::VCall.new(node)
+		end
+
+		visit_method def visit_fcall(node)
+			check Phlex::Compiler::Nodes::FCall.new(node)
+		end
+
+		visit_method def visit_command(node)
+			check Phlex::Compiler::Nodes::Command.new(node)
+		end
+
+		visit_method def visit_method_add_block(node)
+			check Phlex::Compiler::Nodes::MethodAddBlock.new(node)
+		end
+
+		private
+
+		def check(node)
+			@mutating = true if @compiler.tag_method?(node.name)
+			@mutating = true if MUTATING_METHODS.include?(node.name) && !@compiler.redefined?(node.name)
+		end
+	end
+end
diff --git a/lib/phlex/compiler/visitors/view.rb b/lib/phlex/compiler/visitors/view.rb
new file mode 100644
--- /dev/null
+++ b/lib/phlex/compiler/visitors/view.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Phlex::Compiler::Visitors
+	class View < Base
+		include StableScope
+
+		visit_method def visit_def(node)
+			visitor = ViewMethod.new(@compiler)
+			visitor.visit_all(node.child_nodes)
+
+			if visitor.optimized_something?
+				@compiler.redefine(
+					format(node),
+					line: node.location.start_line
+				)
+			end
+		end
+	end
+end
diff --git a/lib/phlex/compiler/visitors/view_method.rb b/lib/phlex/compiler/visitors/view_method.rb
new file mode 100644
--- /dev/null
+++ b/lib/phlex/compiler/visitors/view_method.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module Phlex::Compiler::Visitors
+	class ViewMethod < Base
+		include StableScope
+
+		def optimized_something?
+			!!@optimized_something
+		end
+
+		visit_method def visit_method_add_block(node)
+			return super if node.call.is_a?(SyntaxTree::Call)
+
+			optimizer = Phlex::Compiler::Optimizer.new(
+				Phlex::Compiler::Nodes::MethodAddBlock.new(node),
+				compiler: @compiler
+			)
+
+			if optimizer.call
+				@optimized_something = true
+			end
+
+			super
+		end
+
+		visit_method def visit_vcall(node)
+			optimizer = Phlex::Compiler::Optimizer.new(
+				Phlex::Compiler::Nodes::VCall.new(node),
+				compiler: @compiler
+			)
+
+			if optimizer.call
+				@optimized_something = true
+			end
+		end
+
+		visit_method def visit_fcall(node)
+			optimizer = Phlex::Compiler::Optimizer.new(
+				Phlex::Compiler::Nodes::FCall.new(node),
+				compiler: @compiler
+			)
+
+			if optimizer.call
+				@optimized_something = true
+			end
+		end
+
+		visit_method def visit_command(node)
+			optimizer = Phlex::Compiler::Optimizer.new(
+				Phlex::Compiler::Nodes::Command.new(node),
+				compiler: @compiler
+			)
+
+			if optimizer.call
+				@optimized_something = true
+			end
+		end
+	end
+end
diff --git a/lib/phlex/view.rb b/lib/phlex/view.rb
--- a/lib/phlex/view.rb
+++ b/lib/phlex/view.rb
@@ -73,13 +73,17 @@ def yield_content(&block)
 		end
 
 		def text(content)
-			@_target << case content
+			@_target << _output(content)
+
+			nil
+		end
+
+		def _output(content)
+			case content
 			when String then CGI.escape_html(content)
 			when Symbol then CGI.escape_html(content.name)
 			else CGI.escape_html(content.to_s)
 			end
-
-			nil
 		end
 
 		def whitespace
@@ -178,6 +182,10 @@ def helpers
 			@_view_context
 		end
 
+		def fetch_attrs(**kwargs)
+			Phlex::ATTRIBUTE_CACHE[kwargs.hash] || _attributes(kwargs)
+		end
+
 		def _attributes(attributes, buffer: +"")
 			if attributes[:href]&.start_with?(/\s*javascript/)
 				attributes[:href] = attributes[:href].sub(/^\s*(javascript:)+/, "")


Test patch:
diff --git a/fixtures/compiler_test_helpers.rb b/fixtures/compiler_test_helpers.rb
new file mode 100644
--- /dev/null
+++ b/fixtures/compiler_test_helpers.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module CompilerTestHelpers
+	# @return Array
+	def compile(view)
+		@compiler = Phlex::Compiler.new(view)
+		output = []
+
+		mock(@compiler) do |m|
+			m.before(:redefine) { output << _1 }
+		end
+
+		@compiler.call
+
+		output.map! do |method|
+			Phlex::Compiler::Formatter.format("", SyntaxTree.parse(method))
+		end
+	end
+end
diff --git a/fixtures/test_helper.rb b/fixtures/test_helper.rb
--- a/fixtures/test_helper.rb
+++ b/fixtures/test_helper.rb
@@ -1,7 +1,17 @@
 # frozen_string_literal: true
 
 require "simplecov"
-SimpleCov.start
+
+SimpleCov.start do
+	enable_coverage :branch
+	primary_coverage :branch
+
+	add_filter "/fixtures/"
+
+	add_group "Compiler", %{/lib/phlex/compiler}
+	add_group "Experimental Stdlib", %r{/lib/phlex/(table|collection)\.rb}
+	add_group "Rails", %r{/lib/phlex/rails}
+end
 
 require "phlex"
 require "bundler"
@@ -15,5 +25,6 @@
 end
 
 require "view_helper"
+require "compiler_test_helpers"
 
 Zeitwerk::Loader.eager_load_all
diff --git a/test/phlex/compilation/content.rb b/test/phlex/compilation/content.rb
new file mode 100644
--- /dev/null
+++ b/test/phlex/compilation/content.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "content"
+
+describe Phlex::Compiler do
+	include CompilerTestHelpers
+
+	it "supports string content" do
+		output = compile(Fixtures::Content::BareString)
+
+		expect(output.first).to be == <<~RUBY
+			def template
+				@_target << "<h1>Hello</h1>"
+			end
+		RUBY
+	end
+
+	it "supports symbol content" do
+		output = compile(Fixtures::Content::Symbol)
+
+		expect(output.first).to be == <<~RUBY
+			def template
+				@_target << "<h1>hello</h1>"
+			end
+		RUBY
+	end
+
+	it "supports float content" do
+		output = compile(Fixtures::Content::Float)
+
+		expect(output.first).to be == <<~RUBY
+			def template
+				@_target << "<h1>1.2</h1>"
+			end
+		RUBY
+	end
+
+	it "supports integer content" do
+		output = compile(Fixtures::Content::Integer)
+
+		expect(output.first).to be == <<~RUBY
+			def template
+				@_target << "<h1>1</h1>"
+			end
+		RUBY
+	end
+
+	it "supports nested tags" do
+		output = compile(Fixtures::Content::NestedTags)
+
+		expect(output.first).to be == <<~RUBY
+			def template
+				@_target << "<article><h1>Inside</h1></article>"
+			end
+		RUBY
+	end
+
+	it "supports variable content" do
+		output = compile(Fixtures::Content::Variable)
+
+		expect(output.first).to be == <<~RUBY
+			def template
+				greeting = "Hello"
+				@_target << "<h1>" << CGI.escape_html(greeting) << "</h1>"
+			end
+		RUBY
+	end
+
+	it "supports instance variable content" do
+		output = compile(Fixtures::Content::InstanceVariable)
+
+		expect(output.first).to be == <<~RUBY
+			def template
+				@_target << "<h1>" << CGI.escape_html(@hello) << "</h1>"
+			end
+		RUBY
+	end
+
+	it "supports non-mutating nested content" do
+		output = compile(Fixtures::Content::NonMutatingNestedContent)
+
+		expect(output.first).to be == <<~RUBY
+			def template
+				@_target << "<div>"
+				yield_content { say_hello }
+				@_target << "</div>"
+			end
+		RUBY
+	end
+end
diff --git a/test/phlex/compilation/standard_element.rb b/test/phlex/compilation/standard_element.rb
new file mode 100644
--- /dev/null
+++ b/test/phlex/compilation/standard_element.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "standard_element"
+
+describe Phlex::Compiler do
+	include CompilerTestHelpers
+
+	it "supports standard elements" do
+		with_parens = compile(Fixtures::StandardElement::WithParens)
+		without_parens = compile(Fixtures::StandardElement::WithoutParens)
+
+		expected = <<~RUBY
+			def template
+				@_target << "<h1></h1>"
+			end
+		RUBY
+
+		expect(with_parens.first).to be == expected
+		expect(without_parens.first).to be == expected
+	end
+
+	it "supports standard elements with attributes" do
+		with_parens = compile(Fixtures::StandardElement::WithAttributes::WithParens)
+		without_parens = compile(Fixtures::StandardElement::WithAttributes::WithoutParens)
+
+		expected = <<~RUBY
+			def template
+				@_target << "<h1" << fetch_attrs(class: "font-bold") << "></h1>"
+			end
+		RUBY
+
+		expect(with_parens.first).to be == expected
+		expect(without_parens.first).to be == expected
+	end
+
+	it "supports standard elements with brace block" do
+		with_parens = compile(Fixtures::StandardElement::WithBraceBlock::WithParens)
+		without_parens = compile(Fixtures::StandardElement::WithBraceBlock::WithoutParens)
+
+		expected = <<~RUBY
+			def template
+				@_target << "<h1>Hi</h1>"
+			end
+		RUBY
+
+		expect(with_parens.first).to be == expected
+		expect(without_parens.first).to be == expected
+	end
+
+	it "supports standard elements with brace block and attributes" do
+		output = compile(Fixtures::StandardElement::WithBraceBlock::WithAttributes)
+
+		expect(output.first).to be == <<~RUBY
+			def template
+				@_target << "<h1" << fetch_attrs(class: "font-bold") << ">Hi</h1>"
+			end
+		RUBY
+	end
+
+	it "supports standard elements with do block" do
+		with_parens = compile(Fixtures::StandardElement::WithDoBlock::WithParens)
+		without_parens = compile(Fixtures::StandardElement::WithDoBlock::WithoutParens)
+
+		expected = <<~RUBY
+			def template
+				@_target << "<h1>Hi</h1>"
+			end
+		RUBY
+
+		expect(with_parens.first).to be == expected
+		expect(without_parens.first).to be == expected
+	end
+
+	it "supports standard elements with do block with attributes" do
+		with_parens = compile(Fixtures::StandardElement::WithDoBlock::WithAttributes::WithParens)
+		without_parens = compile(Fixtures::StandardElement::WithDoBlock::WithAttributes::WithoutParens)
+
+		expected = <<~RUBY
+			def template
+				@_target << "<h1" << fetch_attrs(class: "font-bold") << ">Hi</h1>"
+			end
+		RUBY
+
+		expect(with_parens.first).to be == expected
+		expect(without_parens.first).to be == expected
+	end
+end
diff --git a/test/phlex/compilation/vcall.rb b/test/phlex/compilation/vcall.rb
deleted file mode 100644
--- a/test/phlex/compilation/vcall.rb
+++ /dev/null
@@ -1,90 +0,0 @@
-# frozen_string_literal: true
-
-require "test_helper"
-require "compilation/vcall"
-
-describe Phlex::Compiler do
-	with "a standard element" do
-		let(:compiler) {
-			Phlex::Compiler.new(
-				Fixtures::Compilation::VCall::WithStandardElement
-			)
-		}
-
-		it "compiles the method" do
-			expect(compiler).to receive(:redefine).with <<~RUBY.chomp
-				def template
-					@_target << "<div></div>"
-				end
-			RUBY
-
-			compiler.call
-		end
-	end
-
-	with "a void element" do
-		let(:compiler) {
-			Phlex::Compiler.new(
-				Fixtures::Compilation::VCall::WithVoidElement
-			)
-		}
-
-		it "compiles the method" do
-			expect(compiler).to receive(:redefine).with <<~RUBY.chomp
-				def template
-					@_target << "<img />"
-				end
-			RUBY
-
-			compiler.call
-		end
-	end
-
-	with "another method call" do
-		let(:compiler) {
-			Phlex::Compiler.new(
-				Fixtures::Compilation::VCall::WithAnotherMethodCall
-			)
-		}
-
-		it "retains the original method call" do
-			expect(compiler).to receive(:redefine).with <<~RUBY.chomp
-				def template
-					@_target << "<article></article>"
-					some_other_method
-					@_target << "<article></article>"
-				end
-			RUBY
-
-			compiler.call
-		end
-	end
-
-	with "a redefined tag method" do
-		let(:compiler) { Phlex::Compiler.new(Fixtures::Compilation::VCall::WithRedefinedTagMethod) }
-		let(:optimized_methods) { [] }
-
-		it "doesn't optimize overridden HTML tag methods" do
-			mock(compiler) do |mock|
-				mock.before(:redefine) { optimized_methods << _1 }
-			end
-
-			compiler.call
-
-			expect(optimized_methods).to be == [
-				<<~RUBY.chomp,
-					def template
-						title
-						@_target << "<article></article>"
-					end
-				RUBY
-
-				<<~RUBY.chomp
-					def title
-						@_target << "<h1></h1>"
-					end
-				RUBY
-			]
-		end
-	end
-end
diff --git a/test/phlex/compilation/void_element.rb b/test/phlex/compilation/void_element.rb
new file mode 100644
--- /dev/null
+++ b/test/phlex/compilation/void_element.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "void_element"
+
+describe Phlex::Compiler do
+	include CompilerTestHelpers
+
+	it "supports void elements" do
+		with_parens = compile(Fixtures::VoidElement::WithParens)
+		without_parens = compile(Fixtures::VoidElement::WithoutParens)
+
+		expected = <<~RUBY
+			def template
+				@_target << "<img>"
+			end
+		RUBY
+
+		expect(with_parens.first).to be == expected
+		expect(without_parens.first).to be == expected
+	end
+
+	it "supports void elements with attributes" do
+		with_parens = compile(Fixtures::VoidElement::WithAttributes::WithParens)
+		without_parens = compile(Fixtures::VoidElement::WithAttributes::WithoutParens)
+
+		expected = <<~RUBY
+			def template
+				@_target << "<img" << fetch_attrs(class: "a b c") << ">"
+			end
+		RUBY
+
+		expect(with_parens.first).to be == expected
+		expect(without_parens.first).to be == expected
+	end
+end