Repository: yippee-fun/phlex (ruby)

Issue:
Generate source maps
We should be able to create source maps from the compiled source back to the original source code.


Discussion:

Patch:
diff --git a/.rubocop-https---www-goodcop-style-base-yml b/.rubocop-https---www-goodcop-style-base-yml
--- a/.rubocop-https---www-goodcop-style-base-yml
+++ b/.rubocop-https---www-goodcop-style-base-yml
@@ -1,3 +1,4 @@
+# yaml-language-server: $schema=https://www.rubyschema.org/rubocop.json
 ---
 AllCops:
   DisabledByDefault: true
@@ -821,10 +822,6 @@ Style/QuotedSymbols:
   Enabled: true
   EnforcedStyle: same_as_string_literals
 
-Style/RaiseArgs:
-  Enabled: true
-  EnforcedStyle: compact
-
 Style/RandomWithOffset:
   Enabled: true
 
diff --git a/lib/phlex/compiler.rb b/lib/phlex/compiler.rb
new file mode 100644
--- /dev/null
+++ b/lib/phlex/compiler.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require "prism"
+
+module Phlex::Compiler
+	def self.compile(component)
+		path, line = Object.const_source_location(component.name)
+		return unless File.exist?(path)
+		source = File.read(path)
+		tree = Prism.parse(source).value
+		Compilation.new(component, path, line, source, tree).compile
+	end
+end
diff --git a/lib/phlex/compiler/class_compiler.rb b/lib/phlex/compiler/class_compiler.rb
new file mode 100644
--- /dev/null
+++ b/lib/phlex/compiler/class_compiler.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+class Phlex::Compiler::ClassCompiler < Prism::Visitor
+	def initialize(compiler)
+		@compiler = compiler
+	end
+
+	def compile(node)
+		visit_all(node.child_nodes)
+	end
+
+	def visit_def_node(node)
+		return if node.name == :initialize
+
+		compiled_source = Phlex::Compiler::MethodCompiler.new(@compiler.component).compile(node)
+
+		if compiled_source
+			puts compiled_source
+			@compiler.redefine_method(compiled_source, node.location.start_line)
+		end
+	end
+
+	def visit_class_node(node)
+		nil
+	end
+
+	def visit_module_node(node)
+		nil
+	end
+
+	def visit_block_node(node)
+		nil
+	end
+end
diff --git a/lib/phlex/compiler/compilation.rb b/lib/phlex/compiler/compilation.rb
new file mode 100644
--- /dev/null
+++ b/lib/phlex/compiler/compilation.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Phlex::Compiler
+	class Compilation
+		def initialize(component, path, line, source, tree)
+			@component = component
+			@path = path
+			@line = line
+			@source = source
+			@tree = tree
+			freeze
+		end
+
+		attr_reader :component, :line, :source, :path
+
+		def compile
+			FileCompiler.new(self).compile(@tree)
+		end
+
+		def redefine_method(source, line)
+			@component.class_eval("# frozen_string_literal: true\n#{source}", @path, line - 1)
+		end
+	end
+end
diff --git a/lib/phlex/compiler/file_compiler.rb b/lib/phlex/compiler/file_compiler.rb
new file mode 100644
--- /dev/null
+++ b/lib/phlex/compiler/file_compiler.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+class Phlex::Compiler::FileCompiler < Prism::Visitor
+	def initialize(compiler)
+		@compiler = compiler
+	end
+
+	def compile(node)
+		visit(node)
+	end
+
+	def visit_class_node(node)
+		if @compiler.line == node.location.start_line
+			Phlex::Compiler::ClassCompiler.new(@compiler).compile(node)
+		end
+	end
+
+	# def visit_module_node(node)
+	# 	super
+	# end
+
+	def visit_def_node(node)
+		nil
+	end
+
+	def visit_block_node(node)
+		nil
+	end
+end
diff --git a/lib/phlex/compiler/formatter.rb b/lib/phlex/compiler/formatter.rb
new file mode 100644
--- /dev/null
+++ b/lib/phlex/compiler/formatter.rb
@@ -0,0 +1,136 @@
+# frozen_string_literal: true
+
+module Phlex::Compiler
+	class Formatter < VerbatimFormatter
+		def initialize
+			super
+			@new_line_character = "\n"
+			@indentation_character = "\t"
+			@level = 0
+		end
+
+		def visit(node)
+			case node
+			when nil
+				nil
+			when Array
+				visit_array(node)
+			when Proc
+				node.call(self)
+			else
+				super
+			end
+		end
+
+		def visit_array(nodes)
+			nodes.each do |node|
+				case node
+				when Array
+					indent { visit_array(node) }
+				else
+					visit(node)
+				end
+			end
+		end
+
+		def format(node)
+			@buffer.clear
+			visit(node)
+			[@buffer.join, @source_map]
+		end
+
+		def visit_each(nodes)
+			i, len = 0, nodes.length
+			if block_given?
+				while i < len
+					node = nodes[i]
+					i += 1
+					if node
+						visit node
+						yield unless i == len
+					end
+				end
+			else
+				while i < len
+					visit nodes[i]
+					i += 1
+				end
+			end
+		end
+
+		def space
+			push " "
+		end
+
+		def statement
+			ensure_new_line
+			yield
+		end
+
+		def ensure_new_line
+			new_line unless on_new_line?
+		end
+
+		def on_new_line?
+			@new_line_at == @buffer.length
+		end
+
+		def new_line
+			push "#{@new_line_character}#{@indentation_character * @level}"
+			@new_line_at = @buffer.length
+		end
+
+		def indent
+			original_level = @level
+			@level += 1
+			ensure_new_line
+			yield
+			@level = original_level
+		end
+
+		def visit_block_node(node)
+			emit node.opening_loc
+			indent do
+				visit node.body
+			end
+			new_line
+			emit node.closing_loc
+		end
+
+		def visit_call_node(node)
+			visit node.receiver
+			emit node.call_operator_loc
+			emit node.message_loc
+			if node.opening_loc
+				emit node.opening_loc
+			else
+				space
+			end
+			visit node.arguments
+			emit node.closing_loc
+			space
+			visit node.block
+		end
+
+		def visit_def_node(node)
+			emit node.def_keyword_loc
+			space
+			push node.name
+
+			if node.parameters
+				push "("
+				visit node.parameters
+				push ")"
+			end
+
+			indent { visit node.body }
+
+			new_line
+			emit node.end_keyword_loc
+		end
+
+		def visit_statements_node(node)
+			visit_each(node.compact_child_nodes) { ensure_new_line }
+		end
+	end
+end
diff --git a/lib/phlex/compiler/method_compiler.rb b/lib/phlex/compiler/method_compiler.rb
new file mode 100644
--- /dev/null
+++ b/lib/phlex/compiler/method_compiler.rb
@@ -0,0 +1,332 @@
+# frozen_string_literal: true
+
+require "prism"
+
+module Phlex::Compiler
+	class MethodCompiler < Prism::MutationCompiler
+		def initialize(component)
+			@component = component
+			@current_buffer = nil
+		end
+
+		def compile(node)
+			result = visit(node)
+			result.body&.body&.unshift(
+				proc do |f|
+					f.statement do
+						f.push "__phlex_state__ = @_state"
+					end
+					f.statement do
+						f.push "__phlex_buffer__ = __phlex_state__.buffer"
+					end
+					f.statement do
+						f.push "__phlex_me__ = self"
+					end
+					f.statement do
+						f.push "__phlex_should_render__ = __phlex_state__.should_render?; nil"
+					end
+				end
+			)
+
+			source, map = Phlex::Compiler::Formatter.new.format(result)
+			source
+		end
+
+		def visit_call_node(node)
+			if nil == node.receiver
+				if (tag = standard_element?(node))
+					return compile_standard_element(node, tag)
+				elsif (tag = void_element?(node))
+					return compile_void_element(node, tag)
+				elsif whitespace_helper?(node)
+					return compile_whitespace_helper(node)
+				elsif doctype_helper?(node)
+					return compile_doctype_helper(node)
+				elsif plain_helper?(node)
+					return compile_plain_helper(node)
+				elsif fragment_helper?(node)
+					return compile_fragment_helper(node)
+				elsif comment_helper?(node)
+					return compile_comment_helper(node)
+				elsif raw_helper?(node)
+					return compile_raw_helper(node)
+				end
+			end
+
+			super
+		end
+
+		def visit_class_node(node)
+			node
+		end
+
+		def visit_module_node(node)
+			node
+		end
+
+		def compile_standard_element(node, tag)
+			[
+				[
+					buffer("<#{tag}"),
+					*(
+						if node.arguments
+							visit_phlex_attributes(node.arguments)
+						end
+					),
+					buffer(">"),
+					*(
+						if node.block
+							[visit_phlex_block(node.block)]
+						end
+					),
+					buffer("</#{tag}>"),
+				],
+			]
+		end
+
+		def visit_phlex_attributes(node)
+			if node.arguments in [Prism::KeywordHashNode[elements: attributes]]
+				result = attributes.all? { |attribute| attribute in Prism::AssocNode[key: Prism::SymbolNode, value: Prism::StringNode] }
+				if result
+					return buffer(Phlex::SGML::Attributes.generate_attributes(eval("{#{node.slice}}")))
+				end
+			end
+
+			[
+				ensure_new_line,
+				push("__render_attributes__("),
+				node,
+				push(")"),
+			]
+		end
+
+		def visit_phlex_block(node)
+			if Prism::BlockArgumentNode === node
+				[statement("__yield_content__("), node, push(")")]
+			elsif output_block?(node)
+				visit(node.body)
+			elsif content_block?(node)
+				content = node.body.body.first
+				case content
+				when Prism::StringNode
+					buffer(Phlex::Escape.html_escape(content.unescaped))
+				else
+					raise
+				end
+			else
+				[
+					statement("__yield_content__ do"),
+					[node.body],
+					statement("end"),
+				]
+			end
+		end
+
+		def visit_block_node(node)
+			node.copy(
+				body: compile_block_body_node(node.body)
+			)
+		end
+
+		def compile_block_body_node(node)
+			[
+				statement("if __phlex_me__ == self;"),
+				visit(node),
+				statement("else;"),
+				[[node]],
+				statement("end;"),
+			]
+		end
+
+		def compile_void_element(node, tag)
+			[
+				[
+					buffer("<#{tag}"),
+					*(
+						if node.arguments
+							visit_phlex_attributes(node.arguments)
+						end
+					),
+					buffer(">"),
+				],
+			]
+		end
+
+		def compile_whitespace_helper(node)
+			if node.block
+				[
+					buffer(" "),
+					visit_phlex_block(node.block),
+					buffer(" "),
+				]
+			else
+				[
+					buffer(" "),
+				]
+			end
+		end
+
+		def compile_doctype_helper(node)
+			[
+				buffer("<!doctype html>"),
+			]
+		end
+
+		def compile_plain_helper(node)
+			if node.arguments in [Prism::StringNode]
+				[
+					buffer(node.arguments.child_nodes.first.unescaped),
+				]
+			else
+				@current_buffer = nil
+				node
+			end
+		end
+
+		def compile_fragment_helper(node)
+			node.copy(
+				block: compile_fragment_helper_block(node.block)
+			)
+		end
+
+		def compile_fragment_helper_block(node)
+			node.copy(
+				body: [
+					statement("__phlex_original_should_render__ = __phlex_should_render__"),
+					statement("__phlex_should_render__ = __phlex_state__.should_render?;"),
+					visit(node.body),
+					statement("__phlex_should_render__ = __phlex_original_should_render__"),
+				]
+			)
+		end
+
+		def compile_comment_helper(node)
+			[
+				buffer("<!-- "),
+				visit_phlex_block(node.block),
+				buffer(" -->"),
+			]
+		end
+
+		def compile_raw_helper(node)
+			@current_buffer = nil
+			node
+		end
+
+		private def ensure_new_line
+			proc(&:ensure_new_line)
+		end
+
+		private def new_line
+			@current_buffer = nil
+
+			proc(&:new_line)
+		end
+
+		private def statement(string)
+			@current_buffer = nil
+
+			proc do |f|
+				f.statement do
+					f.push string
+				end
+			end
+		end
+
+		private def push(value)
+			@current_buffer = nil
+
+			proc do |f|
+				f.push value
+			end
+		end
+
+		private def buffer(value)
+			if @current_buffer
+				@current_buffer << value
+				nil
+			else
+				new_buffer = +""
+				@current_buffer = new_buffer
+				new_buffer << value
+
+				proc do |f|
+					f.statement do
+						f.push "__phlex_buffer__ << \"#{new_buffer.gsub('"', '\\"')}\" if __phlex_should_render__; nil;"
+					end
+				end
+			end
+		end
+
+		private def new_scope
+			original_in_scope = @in_scope
+			@in_scope = false
+			yield
+			@in_scope = original_in_scope
+		end
+
+		private def output_block?(node)
+			node.body.body.any? do |child|
+				Prism::CallNode === child && (standard_element?(child) || void_element?(child) || plain_helper?(child) || whitespace_helper?(child))
+			end
+		end
+
+		private def content_block?(node)
+			return false unless node.body.body.length == 1
+			node.body.body.first in Prism::StringNode
+		end
+
+		private def standard_element?(node)
+			if (tag = Phlex::HTML::StandardElements.__registered_elements__[node.name]) &&
+					(Phlex::HTML::StandardElements == @component.instance_method(node.name).owner)
+
+				tag
+			else
+				false
+			end
+		end
+
+		private def void_element?(node)
+			if (tag = Phlex::HTML::VoidElements.__registered_elements__[node.name]) &&
+					(Phlex::HTML::VoidElements == @component.instance_method(node.name).owner)
+
+				tag
+			else
+				false
+			end
+		end
+
+		private def whitespace_helper?(node)
+			node.name == :whitespace && own_method_without_scope?(node)
+		end
+
+		private def doctype_helper?(node)
+			node.name == :doctype && own_method_without_scope?(node)
+		end
+
+		private def plain_helper?(node)
+			node.name == :plain && own_method_without_scope?(node)
+		end
+
+		private def fragment_helper?(node)
+			node.name == :fragment && own_method_without_scope?(node)
+		end
+
+		private def comment_helper?(node)
+			node.name == :comment && own_method_without_scope?(node)
+		end
+
+		private def raw_helper?(node)
+			node.name == :raw && own_method_without_scope?(node)
+		end
+
+		ALLOWED_OWNERS = [Phlex::SGML, Phlex::HTML, Phlex::SVG]
+		private def own_method_without_scope?(node)
+			ALLOWED_OWNERS.include?(@component.instance_method(node.name).owner)
+		end
+
+		private def extract_kwargs_from_string(string)
+			eval("{#{string}}")
+		end
+	end
+end
diff --git a/lib/phlex/compiler/verbatim_formatter.rb b/lib/phlex/compiler/verbatim_formatter.rb
new file mode 100644
--- /dev/null
+++ b/lib/phlex/compiler/verbatim_formatter.rb
@@ -0,0 +1,644 @@
+# frozen_string_literal: true
+
+module Phlex::Compiler
+	class VerbatimFormatter < Prism::BasicVisitor
+		def initialize
+			@buffer = []
+			@source_map = []
+			@current_line = 1
+		end
+
+		def push(value)
+			string = case value
+			when String
+				value
+			when Symbol
+				value.name
+			end
+
+			@buffer << string
+			new_lines = string.count("\n")
+			@current_line += new_lines
+		end
+
+		def emit(node)
+			return unless node
+			source_map = @source_map
+			current_line = @current_line
+			start_line = node.start_line
+			end_line = node.end_line
+			number_of_lines = end_line - start_line
+			i = 0
+			while i <= number_of_lines
+				source_map[current_line + i] = start_line + i
+				i += 1
+			end
+
+			push node.slice
+		end
+
+		def visit_alias_global_variable_node(node)
+			emit node
+		end
+
+		def visit_alias_method_node(node)
+			emit node
+		end
+
+		def visit_alternation_pattern_node(node)
+			emit node
+		end
+
+		def visit_and_node(node)
+			emit node
+		end
+
+		def visit_arguments_node(node)
+			emit node
+		end
+
+		def visit_array_node(node)
+			emit node
+		end
+
+		def visit_array_pattern_node(node)
+			emit node
+		end
+
+		def visit_assoc_node(node)
+			emit node
+		end
+
+		def visit_assoc_splat_node(node)
+			emit node
+		end
+
+		def visit_back_reference_read_node(node)
+			emit node
+		end
+
+		def visit_begin_node(node)
+			emit node
+		end
+
+		def visit_block_argument_node(node)
+			emit node
+		end
+
+		def visit_block_local_variable_node(node)
+			emit node
+		end
+
+		def visit_block_node(node)
+			emit node
+		end
+
+		def visit_block_parameter_node(node)
+			emit node
+		end
+
+		def visit_block_parameters_node(node)
+			emit node
+		end
+
+		def visit_break_node(node)
+			emit node
+		end
+
+		def visit_call_and_write_node(node)
+			emit node
+		end
+
+		def visit_call_node(node)
+			emit node
+		end
+
+		def visit_call_operator_write_node(node)
+			emit node
+		end
+
+		def visit_call_or_write_node(node)
+			emit node
+		end
+
+		def visit_call_target_node(node)
+			emit node
+		end
+
+		def visit_capture_pattern_node(node)
+			emit node
+		end
+
+		def visit_case_match_node(node)
+			emit node
+		end
+
+		def visit_case_node(node)
+			emit node
+		end
+
+		def visit_class_node(node)
+			emit node
+		end
+
+		def visit_class_variable_and_write_node(node)
+			emit node
+		end
+
+		def visit_class_variable_operator_write_node(node)
+			emit node
+		end
+
+		def visit_class_variable_or_write_node(node)
+			emit node
+		end
+
+		def visit_class_variable_read_node(node)
+			emit node
+		end
+
+		def visit_class_variable_target_node(node)
+			emit node
+		end
+
+		def visit_class_variable_write_node(node)
+			emit node
+		end
+
+		def visit_constant_and_write_node(node)
+			emit node
+		end
+
+		def visit_constant_operator_write_node(node)
+			emit node
+		end
+
+		def visit_constant_or_write_node(node)
+			emit node
+		end
+
+		def visit_constant_path_and_write_node(node)
+			emit node
+		end
+
+		def visit_constant_path_node(node)
+			emit node
+		end
+
+		def visit_constant_path_operator_write_node(node)
+			emit node
+		end
+
+		def visit_constant_path_or_write_node(node)
+			emit node
+		end
+
+		def visit_constant_path_target_node(node)
+			emit node
+		end
+
+		def visit_constant_path_write_node(node)
+			emit node
+		end
+
+		def visit_constant_read_node(node)
+			emit node
+		end
+
+		def visit_constant_target_node(node)
+			emit node
+		end
+
+		def visit_constant_write_node(node)
+			emit node
+		end
+
+		def visit_def_node(node)
+			emit node
+		end
+
+		def visit_defined_node(node)
+			emit node
+		end
+
+		def visit_else_node(node)
+			emit node
+		end
+
+		def visit_embedded_statements_node(node)
+			emit node
+		end
+
+		def visit_embedded_variable_node(node)
+			emit node
+		end
+
+		def visit_ensure_node(node)
+			emit node
+		end
+
+		def visit_false_node(node)
+			emit node
+		end
+
+		def visit_find_pattern_node(node)
+			emit node
+		end
+
+		def visit_flip_flop_node(node)
+			emit node
+		end
+
+		def visit_float_node(node)
+			emit node
+		end
+
+		def visit_for_node(node)
+			emit node
+		end
+
+		def visit_forwarding_arguments_node(node)
+			emit node
+		end
+
+		def visit_forwarding_parameter_node(node)
+			emit node
+		end
+
+		def visit_forwarding_super_node(node)
+			emit node
+		end
+
+		def visit_global_variable_and_write_node(node)
+			emit node
+		end
+
+		def visit_global_variable_operator_write_node(node)
+			emit node
+		end
+
+		def visit_global_variable_or_write_node(node)
+			emit node
+		end
+
+		def visit_global_variable_read_node(node)
+			emit node
+		end
+
+		def visit_global_variable_target_node(node)
+			emit node
+		end
+
+		def visit_global_variable_write_node(node)
+			emit node
+		end
+
+		def visit_hash_node(node)
+			emit node
+		end
+
+		def visit_hash_pattern_node(node)
+			emit node
+		end
+
+		def visit_if_node(node)
+			emit node
+		end
+
+		def visit_imaginary_node(node)
+			emit node
+		end
+
+		def visit_implicit_node(node)
+			emit node
+		end
+
+		def visit_implicit_rest_node(node)
+			emit node
+		end
+
+		def visit_in_node(node)
+			emit node
+		end
+
+		def visit_index_and_write_node(node)
+			emit node
+		end
+
+		def visit_index_operator_write_node(node)
+			emit node
+		end
+
+		def visit_index_or_write_node(node)
+			emit node
+		end
+
+		def visit_index_target_node(node)
+			emit node
+		end
+
+		def visit_instance_variable_and_write_node(node)
+			emit node
+		end
+
+		def visit_instance_variable_operator_write_node(node)
+			emit node
+		end
+
+		def visit_instance_variable_or_write_node(node)
+			emit node
+		end
+
+		def visit_instance_variable_read_node(node)
+			emit node
+		end
+
+		def visit_instance_variable_target_node(node)
+			emit node
+		end
+
+		def visit_instance_variable_write_node(node)
+			emit node
+		end
+
+		def visit_integer_node(node)
+			emit node
+		end
+
+		def visit_interpolated_match_last_line_node(node)
+			emit node
+		end
+
+		def visit_interpolated_regular_expression_node(node)
+			emit node
+		end
+
+		def visit_interpolated_string_node(node)
+			emit node
+		end
+
+		def visit_interpolated_symbol_node(node)
+			emit node
+		end
+
+		def visit_interpolated_x_string_node(node)
+			emit node
+		end
+
+		def visit_it_local_variable_read_node(node)
+			emit node
+		end
+
+		def visit_it_parameters_node(node)
+			emit node
+		end
+
+		def visit_keyword_hash_node(node)
+			emit node
+		end
+
+		def visit_keyword_rest_parameter_node(node)
+			emit node
+		end
+
+		def visit_lambda_node(node)
+			emit node
+		end
+
+		def visit_local_variable_and_write_node(node)
+			emit node
+		end
+
+		def visit_local_variable_operator_write_node(node)
+			emit node
+		end
+
+		def visit_local_variable_or_write_node(node)
+			emit node
+		end
+
+		def visit_local_variable_read_node(node)
+			emit node
+		end
+
+		def visit_local_variable_target_node(node)
+			emit node
+		end
+
+		def visit_local_variable_write_node(node)
+			emit node
+		end
+
+		def visit_match_last_line_node(node)
+			emit node
+		end
+
+		def visit_match_predicate_node(node)
+			emit node
+		end
+
+		def visit_match_required_node(node)
+			emit node
+		end
+
+		def visit_match_write_node(node)
+			emit node
+		end
+
+		def visit_missing_node(node)
+			emit node
+		end
+
+		def visit_module_node(node)
+			emit node
+		end
+
+		def visit_multi_target_node(node)
+			emit node
+		end
+
+		def visit_multi_write_node(node)
+			emit node
+		end
+
+		def visit_next_node(node)
+			emit node
+		end
+
+		def visit_nil_node(node)
+			emit node
+		end
+
+		def visit_no_keywords_parameter_node(node)
+			emit node
+		end
+
+		def visit_numbered_parameters_node(node)
+			emit node
+		end
+
+		def visit_numbered_reference_read_node(node)
+			emit node
+		end
+
+		def visit_optional_keyword_parameter_node(node)
+			emit node
+		end
+
+		def visit_optional_parameter_node(node)
+			emit node
+		end
+
+		def visit_or_node(node)
+			emit node
+		end
+
+		def visit_parameters_node(node)
+			emit node
+		end
+
+		def visit_parentheses_node(node)
+			emit node
+		end
+
+		def visit_pinned_expression_node(node)
+			emit node
+		end
+
+		def visit_pinned_variable_node(node)
+			emit node
+		end
+
+		def visit_post_execution_node(node)
+			emit node
+		end
+
+		def visit_pre_execution_node(node)
+			emit node
+		end
+
+		def visit_program_node(node)
+			emit node
+		end
+
+		def visit_range_node(node)
+			emit node
+		end
+
+		def visit_rational_node(node)
+			emit node
+		end
+
+		def visit_redo_node(node)
+			emit node
+		end
+
+		def visit_regular_expression_node(node)
+			emit node
+		end
+
+		def visit_required_keyword_parameter_node(node)
+			emit node
+		end
+
+		def visit_required_parameter_node(node)
+			emit node
+		end
+
+		def visit_rescue_modifier_node(node)
+			emit node
+		end
+
+		def visit_rescue_node(node)
+			emit node
+		end
+
+		def visit_rest_parameter_node(node)
+			emit node
+		end
+
+		def visit_retry_node(node)
+			emit node
+		end
+
+		def visit_return_node(node)
+			emit node
+		end
+
+		def visit_self_node(node)
+			emit node
+		end
+
+		def visit_shareable_constant_node(node)
+			emit node
+		end
+
+		def visit_singleton_class_node(node)
+			emit node
+		end
+
+		def visit_source_encoding_node(node)
+			emit node
+		end
+
+		def visit_source_file_node(node)
+			emit node
+		end
+
+		def visit_source_line_node(node)
+			emit node
+		end
+
+		def visit_splat_node(node)
+			emit node
+		end
+
+		def visit_statements_node(node)
+			emit node
+		end
+
+		def visit_string_node(node)
+			emit node
+		end
+
+		def visit_super_node(node)
+			emit node
+		end
+
+		def visit_symbol_node(node)
+			emit node
+		end
+
+		def visit_true_node(node)
+			emit node
+		end
+
+		def visit_undef_node(node)
+			emit node
+		end
+
+		def visit_unless_node(node)
+			emit node
+		end
+
+		def visit_until_node(node)
+			emit node
+		end
+
+		def visit_when_node(node)
+			emit node
+		end
+
+		def visit_while_node(node)
+			emit node
+		end
+
+		def visit_x_string_node(node)
+			emit node
+		end
+
+		def visit_yield_node(node)
+			emit node
+		end
+	end
+end
diff --git a/lib/phlex/html.rb b/lib/phlex/html.rb
--- a/lib/phlex/html.rb
+++ b/lib/phlex/html.rb
@@ -58,7 +58,7 @@ def tag(name, **attributes, &)
 		if (tag = StandardElements.__registered_elements__[name]) || (tag = name.name.tr("_", "-")).include?("-")
 			if attributes.length > 0 # with attributes
 				if block_given # with content block
-					buffer << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[attributes] ||= __attributes__(attributes)) << ">"
+					buffer << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[attributes] ||= Phlex::SGML::Attributes.generate_attributes(attributes)) << ">"
 					if tag == "svg"
 						render Phlex::SVG.new(&)
 					else
@@ -66,7 +66,7 @@ def tag(name, **attributes, &)
 					end
 					buffer << "</#{tag}>"
 				else # without content
-					buffer << "<#{tag}" << (::Phlex::ATTRIBUTE_CACHE[attributes] ||= __attributes__(attributes)) << "></#{tag}>"
+					buffer << "<#{tag}" << (::Phlex::ATTRIBUTE_CACHE[attributes] ||= Phlex::SGML::Attributes.generate_attributes(attributes)) << "></#{tag}>"
 				end
 			else # without attributes
 				if block_given # with content block
@@ -87,7 +87,7 @@ def tag(name, **attributes, &)
 			end
 
 			if attributes.length > 0 # with attributes
-				buffer << "<#{tag}" << (::Phlex::ATTRIBUTE_CACHE[attributes] ||= __attributes__(attributes)) << ">"
+				buffer << "<#{tag}" << (::Phlex::ATTRIBUTE_CACHE[attributes] ||= Phlex::SGML::Attributes.generate_attributes(attributes)) << ">"
 			else # without attributes
 				buffer << "<#{tag}>"
 			end
diff --git a/lib/phlex/sgml.rb b/lib/phlex/sgml.rb
--- a/lib/phlex/sgml.rb
+++ b/lib/phlex/sgml.rb
@@ -2,9 +2,6 @@
 
 # **Standard Generalized Markup Language** for behaviour common to {HTML} and {SVG}.
 class Phlex::SGML
-	UNSAFE_ATTRIBUTES = Set.new(%w[srcdoc sandbox http-equiv]).freeze
-	REF_ATTRIBUTES = Set.new(%w[href src action formaction lowsrc dynsrc background ping]).freeze
-
 	ERBCompiler = ERB::Compiler.new("<>").tap do |compiler|
 		compiler.pre_cmd    = [""]
 		compiler.put_cmd    = "@_state.buffer.<<"
@@ -462,262 +459,10 @@ def json_escape(string)
 		true
 	end
 
-	private def __attributes__(attributes, buffer = +"")
-		attributes.each do |k, v|
-			next unless v
-
-			name = case k
-				when String then k
-				when Symbol then k.name.tr("_", "-")
-				else raise Phlex::ArgumentError.new("Attribute keys should be Strings or Symbols.")
-			end
-
-			value = case v
-			when true
-				true
-			when String
-				v.gsub('"', "&quot;")
-			when Symbol
-				v.name.tr("_", "-").gsub('"', "&quot;")
-			when Integer, Float
-				v.to_s
-			when Date
-				v.iso8601
-			when Time
-				v.respond_to?(:iso8601) ? v.iso8601 : v.strftime("%Y-%m-%dT%H:%M:%S%:z")
-			when Hash
-				case k
-				when :style
-					__styles__(v).gsub('"', "&quot;")
-				else
-					__nested_attributes__(v, "#{name}-", buffer)
-				end
-			when Array
-				case k
-				when :style
-					__styles__(v).gsub('"', "&quot;")
-				else
-					__nested_tokens__(v)
-				end
-			when Set
-				case k
-				when :style
-					__styles__(v).gsub('"', "&quot;")
-				else
-					__nested_tokens__(v.to_a)
-				end
-			when Phlex::SGML::SafeObject
-				v.to_s.gsub('"', "&quot;")
-			else
-				raise Phlex::ArgumentError.new("Invalid attribute value for #{k}: #{v.inspect}.")
-			end
-
-			lower_name = name.downcase
-
-			unless Phlex::SGML::SafeObject === v
-				normalized_name = lower_name.delete("^a-z-")
-
-				if value != true && REF_ATTRIBUTES.include?(normalized_name)
-					case value
-					when String
-						if value.downcase.delete("^a-z:").start_with?("javascript:")
-							# We just ignore these because they were likely not specified by the developer.
-							next
-						end
-					else
-						raise Phlex::ArgumentError.new("Invalid attribute value for #{k}: #{v.inspect}.")
-					end
-				end
-
-				if normalized_name.bytesize > 2 && normalized_name.start_with?("on") && !normalized_name.include?("-")
-					raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
-				end
-
-				if UNSAFE_ATTRIBUTES.include?(normalized_name)
-					raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
-				end
-			end
-
-			if name.match?(/[<>&"']/)
-				raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
-			end
-
-			if lower_name.to_sym == :id && k != :id
-				raise Phlex::ArgumentError.new(":id attribute should only be passed as a lowercase symbol.")
-			end
-
-			case value
-			when true
-				buffer << " " << name
-			when String
-				buffer << " " << name << '="' << value << '"'
-			end
-		end
-
-		buffer
-	end
-
-	# Provides the nested-attributes case for serializing out attributes.
-	# This allows us to skip many of the checks the `__attributes__` method must perform.
-	private def __nested_attributes__(attributes, base_name, buffer = +"")
-		attributes.each do |k, v|
-			next unless v
-
-			if (root_key = (:_ == k))
-				name = ""
-				original_base_name = base_name
-				base_name = base_name.delete_suffix("-")
-			else
-				name = case k
-					when String then k
-					when Symbol then k.name.tr("_", "-")
-					else raise Phlex::ArgumentError.new("Attribute keys should be Strings or Symbols")
-				end
-
-				if name.match?(/[<>&"']/)
-					raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
-				end
-			end
-
-			case v
-			when true
-				buffer << " " << base_name << name
-			when String
-				buffer << " " << base_name << name << '="' << v.gsub('"', "&quot;") << '"'
-			when Symbol
-				buffer << " " << base_name << name << '="' << v.name.tr("_", "-").gsub('"', "&quot;") << '"'
-			when Integer, Float
-				buffer << " " << base_name << name << '="' << v.to_s << '"'
-			when Hash
-				__nested_attributes__(v, "#{base_name}#{name}-", buffer)
-			when Array
-				buffer << " " << base_name << name << '="' << __nested_tokens__(v) << '"'
-			when Set
-				buffer << " " << base_name << name << '="' << __nested_tokens__(v.to_a) << '"'
-			when Phlex::SGML::SafeObject
-				buffer << " " << base_name << name << '="' << v.to_s.gsub('"', "&quot;") << '"'
-			else
-				raise Phlex::ArgumentError.new("Invalid attribute value #{v.inspect}.")
-			end
-
-			if root_key
-				base_name = original_base_name
-			end
-
-			buffer
-		end
-	end
-
-	private def __nested_tokens__(tokens, sep = " ", gsub_from = nil, gsub_to	= "")
-		buffer = +""
-
-		i, length = 0, tokens.length
-
-		while i < length
-			token = tokens[i]
-
-			case token
-			when String
-				token = token.gsub(gsub_from, gsub_to) if gsub_from
-				if i > 0
-					buffer << sep << token
-				else
-					buffer << token
-				end
-			when Symbol
-				if i > 0
-					buffer << sep << token.name.tr("_", "-")
-				else
-					buffer << token.name.tr("_", "-")
-				end
-			when Integer, Float, Phlex::SGML::SafeObject
-				if i > 0
-					buffer << sep << token.to_s
-				else
-					buffer << token.to_s
-				end
-			when Array
-				if token.length > 0
-					if i > 0
-						buffer << sep << __nested_tokens__(token, sep, gsub_from, gsub_to)
-					else
-						buffer << __nested_tokens__(token, sep, gsub_from, gsub_to)
-					end
-				end
-			when nil
-				# Do nothing
-			else
-				raise Phlex::ArgumentError.new("Invalid token type: #{token.class}.")
-			end
-
-			i += 1
-		end
-
-		buffer.gsub('"', "&quot;")
-	end
-
-	# Result is **unsafe**, so it should be escaped!
-	private def __styles__(styles)
-		case styles
-		when Array, Set
-			styles.filter_map do |s|
-				case s
-				when String
-					if s == "" || s.end_with?(";")
-						s
-					else
-						"#{s};"
-					end
-				when Phlex::SGML::SafeObject
-					value = s.to_s
-					value.end_with?(";") ? value : "#{value};"
-				when Hash
-					next __styles__(s)
-				when nil
-					next nil
-				else
-					raise Phlex::ArgumentError.new("Invalid style: #{s.inspect}.")
-				end
-			end.join(" ")
-		when Hash
-			buffer = +""
-			i = 0
-			styles.each do |k, v|
-				prop = case k
-				when String
-					k
-				when Symbol
-					k.name.tr("_", "-")
-				else
-					raise Phlex::ArgumentError.new("Style keys should be Strings or Symbols.")
-				end
-
-				value = case v
-				when String
-					v
-				when Symbol
-					v.name.tr("_", "-")
-				when Integer, Float, Phlex::SGML::SafeObject
-					v.to_s
-				when nil
-					nil
-				else
-					raise Phlex::ArgumentError.new("Invalid style value: #{v.inspect}")
-				end
-
-				if value
-					if i == 0
-						buffer << prop << ": " << value << ";"
-					else
-						buffer << " " << prop << ": " << value << ";"
-					end
-				end
-
-				i += 1
-			end
-
-			buffer
-		end
+	private def __render_attributes__(attributes)
+		state = @_state
+		return unless state.should_render?
+		state.buffer << (Phlex::ATTRIBUTE_CACHE[attributes] ||= Phlex::SGML::Attributes.generate_attributes(attributes))
 	end
 
 	private_class_method def self.method_added(method_name)
@@ -727,8 +472,13 @@ def json_escape(string)
 			if location[0] in "/" | "."
 				Phlex.__expand_attribute_cache__(location)
 			end
-		else
-			super
 		end
+
+		super
+	end
+
+	def self.__compile__(method_name)
+		path, line = instance_method(method_name).source_location
+		Phlex::Compiler::Method.new(self, path, line, method_name).compile
 	end
 end
diff --git a/lib/phlex/sgml/attributes.rb b/lib/phlex/sgml/attributes.rb
new file mode 100644
--- /dev/null
+++ b/lib/phlex/sgml/attributes.rb
@@ -0,0 +1,266 @@
+# frozen_string_literal: true
+
+module Phlex::SGML::Attributes
+	extend self
+
+	UNSAFE_ATTRIBUTES = Set.new(%w[srcdoc sandbox http-equiv]).freeze
+	REF_ATTRIBUTES = Set.new(%w[href src action formaction lowsrc dynsrc background ping]).freeze
+
+	def generate_attributes(attributes, buffer = +"")
+		attributes.each do |k, v|
+			next unless v
+
+			name = case k
+				when String then k
+				when Symbol then k.name.tr("_", "-")
+				else raise Phlex::ArgumentError.new("Attribute keys should be Strings or Symbols.")
+			end
+
+			value = case v
+			when true
+				true
+			when String
+				v.gsub('"', "&quot;")
+			when Symbol
+				v.name.tr("_", "-").gsub('"', "&quot;")
+			when Integer, Float
+				v.to_s
+			when Date
+				v.iso8601
+			when Time
+				v.respond_to?(:iso8601) ? v.iso8601 : v.strftime("%Y-%m-%dT%H:%M:%S%:z")
+			when Hash
+				case k
+				when :style
+					generate_styles(v).gsub('"', "&quot;")
+				else
+					generate_nested_attributes(v, "#{name}-", buffer)
+				end
+			when Array
+				case k
+				when :style
+					generate_styles(v).gsub('"', "&quot;")
+				else
+					generate_nested_tokens(v)
+				end
+			when Set
+				case k
+				when :style
+					generate_styles(v).gsub('"', "&quot;")
+				else
+					generate_nested_tokens(v.to_a)
+				end
+			when Phlex::SGML::SafeObject
+				v.to_s.gsub('"', "&quot;")
+			else
+				raise Phlex::ArgumentError.new("Invalid attribute value for #{k}: #{v.inspect}.")
+			end
+
+			lower_name = name.downcase
+
+			unless Phlex::SGML::SafeObject === v
+				normalized_name = lower_name.delete("^a-z-")
+
+				if value != true && REF_ATTRIBUTES.include?(normalized_name)
+					case value
+					when String
+						if value.downcase.delete("^a-z:").start_with?("javascript:")
+							# We just ignore these because they were likely not specified by the developer.
+							next
+						end
+					else
+						raise Phlex::ArgumentError.new("Invalid attribute value for #{k}: #{v.inspect}.")
+					end
+				end
+
+				if normalized_name.bytesize > 2 && normalized_name.start_with?("on") && !normalized_name.include?("-")
+					raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
+				end
+
+				if UNSAFE_ATTRIBUTES.include?(normalized_name)
+					raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
+				end
+			end
+
+			if name.match?(/[<>&"']/)
+				raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
+			end
+
+			if lower_name.to_sym == :id && k != :id
+				raise Phlex::ArgumentError.new(":id attribute should only be passed as a lowercase symbol.")
+			end
+
+			case value
+			when true
+				buffer << " " << name
+			when String
+				buffer << " " << name << '="' << value << '"'
+			end
+		end
+
+		buffer
+	end
+
+	# Provides the nested-attributes case for serializing out attributes.
+	# This allows us to skip many of the checks the `__attributes__` method must perform.
+	def generate_nested_attributes(attributes, base_name, buffer = +"")
+		attributes.each do |k, v|
+			next unless v
+
+			if (root_key = (:_ == k))
+				name = ""
+				original_base_name = base_name
+				base_name = base_name.delete_suffix("-")
+			else
+				name = case k
+					when String then k
+					when Symbol then k.name.tr("_", "-")
+					else raise Phlex::ArgumentError.new("Attribute keys should be Strings or Symbols")
+				end
+
+				if name.match?(/[<>&"']/)
+					raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
+				end
+			end
+
+			case v
+			when true
+				buffer << " " << base_name << name
+			when String
+				buffer << " " << base_name << name << '="' << v.gsub('"', "&quot;") << '"'
+			when Symbol
+				buffer << " " << base_name << name << '="' << v.name.tr("_", "-").gsub('"', "&quot;") << '"'
+			when Integer, Float
+				buffer << " " << base_name << name << '="' << v.to_s << '"'
+			when Hash
+				generate_nested_attributes(v, "#{base_name}#{name}-", buffer)
+			when Array
+				buffer << " " << base_name << name << '="' << generate_nested_tokens(v) << '"'
+			when Set
+				buffer << " " << base_name << name << '="' << generate_nested_tokens(v.to_a) << '"'
+			when Phlex::SGML::SafeObject
+				buffer << " " << base_name << name << '="' << v.to_s.gsub('"', "&quot;") << '"'
+			else
+				raise Phlex::ArgumentError.new("Invalid attribute value #{v.inspect}.")
+			end
+
+			if root_key
+				base_name = original_base_name
+			end
+
+			buffer
+		end
+	end
+
+	def generate_nested_tokens(tokens, sep = " ", gsub_from = nil, gsub_to	= "")
+		buffer = +""
+
+		i, length = 0, tokens.length
+
+		while i < length
+			token = tokens[i]
+
+			case token
+			when String
+				token = token.gsub(gsub_from, gsub_to) if gsub_from
+				if i > 0
+					buffer << sep << token
+				else
+					buffer << token
+				end
+			when Symbol
+				if i > 0
+					buffer << sep << token.name.tr("_", "-")
+				else
+					buffer << token.name.tr("_", "-")
+				end
+			when Integer, Float, Phlex::SGML::SafeObject
+				if i > 0
+					buffer << sep << token.to_s
+				else
+					buffer << token.to_s
+				end
+			when Array
+				if token.length > 0
+					if i > 0
+						buffer << sep << generate_nested_tokens(token, sep, gsub_from, gsub_to)
+					else
+						buffer << generate_nested_tokens(token, sep, gsub_from, gsub_to)
+					end
+				end
+			when nil
+				# Do nothing
+			else
+				raise Phlex::ArgumentError.new("Invalid token type: #{token.class}.")
+			end
+
+			i += 1
+		end
+
+		buffer.gsub('"', "&quot;")
+	end
+
+	# The result is unsafe so should be escaped.
+	def generate_styles(styles)
+		case styles
+		when Array, Set
+			styles.filter_map do |s|
+				case s
+				when String
+					if s == "" || s.end_with?(";")
+						s
+					else
+						"#{s};"
+					end
+				when Phlex::SGML::SafeObject
+					value = s.to_s
+					value.end_with?(";") ? value : "#{value};"
+				when Hash
+					next generate_styles(s)
+				when nil
+					next nil
+				else
+					raise Phlex::ArgumentError.new("Invalid style: #{s.inspect}.")
+				end
+			end.join(" ")
+		when Hash
+			buffer = +""
+			i = 0
+			styles.each do |k, v|
+				prop = case k
+				when String
+					k
+				when Symbol
+					k.name.tr("_", "-")
+				else
+					raise Phlex::ArgumentError.new("Style keys should be Strings or Symbols.")
+				end
+
+				value = case v
+				when String
+					v
+				when Symbol
+					v.name.tr("_", "-")
+				when Integer, Float, Phlex::SGML::SafeObject
+					v.to_s
+				when nil
+					nil
+				else
+					raise Phlex::ArgumentError.new("Invalid style value: #{v.inspect}")
+				end
+
+				if value
+					if i == 0
+						buffer << prop << ": " << value << ";"
+					else
+						buffer << " " << prop << ": " << value << ";"
+					end
+				end
+
+				i += 1
+			end
+
+			buffer
+		end
+	end
+end
diff --git a/lib/phlex/sgml/elements.rb b/lib/phlex/sgml/elements.rb
--- a/lib/phlex/sgml/elements.rb
+++ b/lib/phlex/sgml/elements.rb
@@ -4,16 +4,16 @@ module Phlex::SGML::Elements
 	COMMA_SEPARATED_TOKENS = {
 		img: <<~RUBY,
 			if Array === (srcset_attribute = attributes[:srcset])
-				attributes[:srcset] = __nested_tokens__(srcset_attribute, ", ", ",", "%2C")
+				attributes[:srcset] = Phlex::SGML::Attributes.generate_nested_tokens(srcset_attribute, ", ", ",", "%2C")
 			end
 		RUBY
 		link: <<~RUBY,
 			if Array === (media_attribute = attributes[:media])
-				attributes[:media] = __nested_tokens__(media_attribute, ", ", ",", "%2C")
+				attributes[:media] = Phlex::SGML::Attributes.generate_nested_tokens(media_attribute, ", ", ",", "%2C")
 			end
 
 			if Array === (sizes_attribute = attributes[:sizes])
-				attributes[:sizes] = __nested_tokens__(sizes_attribute, ", ", ",", "%2C")
+				attributes[:sizes] = Phlex::SGML::Attributes.generate_nested_tokens(sizes_attribute, ", ", ",", "%2C")
 			end
 
 			if Array === (imagesrcset_attribute = attributes[:imagesrcset])
@@ -21,7 +21,7 @@ module Phlex::SGML::Elements
 				as_attribute = attributes[:as] || attributes["as"]
 
 				if ("preload" == rel_attribute || :preload == rel_attribute) && ("image" == as_attribute || :image == as_attribute)
-					attributes[:imagesrcset] = __nested_tokens__(imagesrcset_attribute, ", ", ",", "%2C")
+					attributes[:imagesrcset] = Phlex::SGML::Attributes.generate_nested_tokens(imagesrcset_attribute, ", ", ",", "%2C")
 				end
 			end
 		RUBY
@@ -30,7 +30,7 @@ module Phlex::SGML::Elements
 				type_attribute = attributes[:type] || attributes["type"]
 
 				if "file" == type_attribute || :file == type_attribute
-					attributes[:accept] = __nested_tokens__(accept_attribute, ", ", ",", "%2C")
+					attributes[:accept] = Phlex::SGML::Attributes.generate_nested_tokens(accept_attribute, ", ", ",", "%2C")
 				end
 			end
 		RUBY
@@ -59,7 +59,7 @@ def #{method_name}(**attributes)
 						buffer << "<#{tag}"
 						begin
 							#{COMMA_SEPARATED_TOKENS[method_name]}
-							buffer << (Phlex::ATTRIBUTE_CACHE[attributes] ||= __attributes__(attributes))
+							buffer << (Phlex::ATTRIBUTE_CACHE[attributes] ||= Phlex::SGML::Attributes.generate_attributes(attributes))
 						ensure
 							buffer << ">"
 						end
@@ -90,7 +90,7 @@ def #{method_name}(**attributes)
 						buffer << "<#{tag}"
 						begin
 							#{COMMA_SEPARATED_TOKENS[method_name]}
-							buffer << (::Phlex::ATTRIBUTE_CACHE[attributes] ||= __attributes__(attributes))
+							buffer << (::Phlex::ATTRIBUTE_CACHE[attributes] ||= Phlex::SGML::Attributes.generate_attributes(attributes))
 						ensure
 							buffer << "></#{tag}>"
 						end
@@ -152,7 +152,7 @@ def #{method_name}(**attributes)
 					buffer << "<#{tag}"
 					begin
 						#{COMMA_SEPARATED_TOKENS[method_name]}
-						buffer << (::Phlex::ATTRIBUTE_CACHE[attributes] ||= __attributes__(attributes))
+						buffer << (::Phlex::ATTRIBUTE_CACHE[attributes] ||= Phlex::SGML::Attributes.generate_attributes(attributes))
 					ensure
 						buffer << ">"
 					end
diff --git a/lib/phlex/svg.rb b/lib/phlex/svg.rb
--- a/lib/phlex/svg.rb
+++ b/lib/phlex/svg.rb
@@ -44,11 +44,11 @@ def tag(name, **attributes, &)
 		if (tag = StandardElements.__registered_elements__[name]) || (tag = name.name.tr("_", "-")).include?("-")
 			if attributes.length > 0 # with attributes
 				if block_given # with content block
-					buffer << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[attributes] ||= __attributes__(attributes)) << ">"
+					buffer << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[attributes] ||= Phlex::SGML::Attributes.generate_attributes(attributes)) << ">"
 					__yield_content__(&)
 					buffer << "</#{tag}>"
 				else # without content
-					buffer << "<#{tag}" << (::Phlex::ATTRIBUTE_CACHE[attributes] ||= __attributes__(attributes)) << "></#{tag}>"
+					buffer << "<#{tag}" << (::Phlex::ATTRIBUTE_CACHE[attributes] ||= Phlex::SGML::Attributes.generate_attributes(attributes)) << "></#{tag}>"
 				end
 			else # without attributes
 				if block_given # with content block
diff --git a/quickdraw/compilation_equivalence_cases/basic.rb b/quickdraw/compilation_equivalence_cases/basic.rb
new file mode 100644
--- /dev/null
+++ b/quickdraw/compilation_equivalence_cases/basic.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Basic < Phlex::HTML
+	def view_template
+		h1 { "Hello" }
+		br
+		br(class: "my-class")
+	end
+end
diff --git a/quickdraw/compilation_equivalence_cases/comment.rb b/quickdraw/compilation_equivalence_cases/comment.rb
new file mode 100644
--- /dev/null
+++ b/quickdraw/compilation_equivalence_cases/comment.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+class Comment < Phlex::HTML
+	def view_template
+		comment { "hello world" }
+		comment { "Begin rendering #{self.class.name}" }
+	end
+end
diff --git a/quickdraw/compilation_equivalence_cases/plain.rb b/quickdraw/compilation_equivalence_cases/plain.rb
new file mode 100644
--- /dev/null
+++ b/quickdraw/compilation_equivalence_cases/plain.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class Plain < Phlex::HTML
+	def view_template
+		local_variable = "good"
+		plain "Greetings "
+		plain local_variable
+		plain "sir!"
+	end
+end
diff --git a/quickdraw/compilation_equivalence_cases/raw.rb b/quickdraw/compilation_equivalence_cases/raw.rb
new file mode 100644
--- /dev/null
+++ b/quickdraw/compilation_equivalence_cases/raw.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Raw < Phlex::HTML
+	def view_template
+		p { "output before" }
+		raw(safe("<h1>raw output in the middle</h1>"))
+		p { "output after" }
+	end
+end
diff --git a/quickdraw/compilation_equivalence_cases/whitespace.rb b/quickdraw/compilation_equivalence_cases/whitespace.rb
new file mode 100644
--- /dev/null
+++ b/quickdraw/compilation_equivalence_cases/whitespace.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class Whitespace < Phlex::HTML
+	def view_template
+		h1 { "Hello" }
+		whitespace
+		h2 { "world" }
+		br
+		br
+		p do
+			plain "This sentence has"
+			whitespace { em { "emphasis" } }
+			plain "in it."
+		end
+	end
+end
diff --git a/quickdraw/fixtures/standard_element_example.rb b/quickdraw/fixtures/standard_element_example.rb
new file mode 100644
--- /dev/null
+++ b/quickdraw/fixtures/standard_element_example.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+class ExampleComponent < Phlex::HTML
+	def view_template(&)
+		div(&)
+	end
+end
+
+class StandardElementExample < Phlex::HTML
+	def initialize(execution_checker = -> {})
+		@execution_checker = execution_checker
+	end
+
+	def view_template
+		doctype
+		div {
+			comment { h1(id: "target") }
+			h1 { "Before" }
+			img(src: "before.jpg")
+			render ExampleComponent.new { "Should not render" }
+			whitespace
+			comment { "This is a comment" }
+			fragment("target") do
+				h1(id: "target") {
+					plain "Hello"
+					strong { "World" }
+					img(src: "image.jpg")
+				}
+			end
+			@execution_checker.call
+			strong { "Here" }
+			fragment("image") do
+				img(id: "image", src: "after.jpg")
+			end
+			h1(id: "target") { "After" }
+		}
+	end
+end


Test patch:
diff --git a/quickdraw/compilation_equivalence.test.rb b/quickdraw/compilation_equivalence.test.rb
new file mode 100644
--- /dev/null
+++ b/quickdraw/compilation_equivalence.test.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+Dir["./compilation_equivalence_cases/*.rb", base: File.dirname(__FILE__)].each do |file|
+	test File.basename(file) do
+		load File.expand_path(file, File.dirname(__FILE__))
+
+		class_name = File.basename(file, ".rb").split("_").map(&:capitalize).join
+		component = Object.const_get(class_name)
+
+		before = component.new.call
+		Phlex::Compiler.compile(component)
+		after = component.new.call
+
+		assert_equal_html after, before
+	end
+end
+
+require_relative "../fixtures/page"
+require_relative "../fixtures/layout"
+
+test "benchmark fixtures" do
+	before = Example::Page.new.call
+	Phlex::Compiler.compile(Example::LayoutComponent)
+	Phlex::Compiler.compile(Example::Page)
+	after = Example::Page.new.call
+
+	assert_equal_html after, before
+end
diff --git a/quickdraw/compiler.test.rb b/quickdraw/compiler.test.rb
new file mode 100644
--- /dev/null
+++ b/quickdraw/compiler.test.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+# test "standard element, no args, no block" do
+# 	snippet = Prism.parse(<<~RUBY).value.statements.body.first
+# 		def foo
+# 			h1
+# 		end
+# 	RUBY
+
+# 	compiled = Phlex::Compiler::MethodCompiler.new(Phlex::HTML).compile(snippet)
+
+# 	assert_equal_ruby compiled.strip, out = <<~RUBY.strip
+# 		def foo
+# 			__phlex_buffer__ = @_state.buffer
+# 			__phlex_buffer__ << "<h1></h1>"
+# 		end
+# 	RUBY
+# end
+
+test "sequential elements", skip: true do
+	snippet = Prism.parse(<<~RUBY).value.statements.body.first
+		def foo
+			div do
+				"Hello"
+			end
+		end
+	RUBY
+
+	compiled = Phlex::Compiler::MethodCompiler.new(Phlex::HTML).compile(snippet)
+
+	puts compiled
+
+	assert_equal compiled.strip, out = <<~RUBY.strip
+		def foo
+			__phlex_buffer__ = @_state.buffer
+			__phlex_buffer__ << "<h1></h1><h2></h2>"
+		end
+	RUBY
+end
diff --git a/quickdraw/sgml/attributes.test.rb b/quickdraw/sgml/attributes.test.rb
--- a/quickdraw/sgml/attributes.test.rb
+++ b/quickdraw/sgml/attributes.test.rb
@@ -595,8 +595,3 @@
 	output = phlex { input(:accept => ["image/jpeg", "image/png"], "type" => "file") }
 	assert_equal_html output, %(<input accept="image/jpeg, image/png" type="file">)
 end
-
-# This is just for coverage.
-Phlex::HTML.call do |c|
-	c.__send__(:__styles__, nil)
-end
diff --git a/test/phlex/view/naughty_business.rb b/test/phlex/view/naughty_business.rb
--- a/test/phlex/view/naughty_business.rb
+++ b/test/phlex/view/naughty_business.rb
@@ -157,7 +157,7 @@ def view_template
 		end
 	end
 
-	Phlex::SGML::UNSAFE_ATTRIBUTES.each do |event_attribute|
+	Phlex::SGML::Attributes::UNSAFE_ATTRIBUTES.each do |event_attribute|
 		with "with naughty #{event_attribute} attribute" do
 			naughty_attributes = { event_attribute => "alert(1);" }