class Phlex::SGML

**Standard Generalized Markup Language** for behaviour common to {HTML} and {SVG}.

def self.method_added(method_name)

def self.method_added(method_name)
iew_template
_method(method_name).source_location[0]
/" | "."
ribute_cache__(location)

def __attributes__(attributes, buffer = +"")

def __attributes__(attributes, buffer = +"")
tes.each do |k, v|
nless v
 case k
String then k
Symbol then k.name.tr("_", "-")
raise Phlex::ArgumentError.new("Attribute keys should be Strings or Symbols.")
= case v
rue
tring
b('"', """)
ymbol
e.tr("_", "-").gsub('"', """)
nteger, Float
s
ate
8601
ime
pond_to?(:iso8601) ? v.iso8601 : v.strftime("%Y-%m-%dT%H:%M:%S%:z")
ash
k
:style
yles__(v).gsub('"', """)
sted_attributes__(v, "#{name}-", buffer)
rray
k
:style
yles__(v).gsub('"', """)
sted_tokens__(v)
et
k
:style
yles__(v).gsub('"', """)
sted_tokens__(v.to_a)
hlex::SGML::SafeObject
s.gsub('"', """)
 Phlex::ArgumentError.new("Invalid attribute value for #{k}: #{v.inspect}.")
name = name.downcase
 Phlex::SGML::SafeObject === v
lized_name = lower_name.delete("^a-z-")
lue != true && REF_ATTRIBUTES.include?(normalized_name)
 value
 String
value.downcase.delete("^a-z:").start_with?("javascript:")
We just ignore these because they were likely not specified by the developer.
xt


se Phlex::ArgumentError.new("Invalid attribute value for #{k}: #{v.inspect}.")
rmalized_name.bytesize > 2 && normalized_name.start_with?("on") && !normalized_name.include?("-")
e Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
SAFE_ATTRIBUTES.include?(normalized_name)
e Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
e.match?(/[<>&"']/)
 Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
er_name.to_sym == :id && k != :id
 Phlex::ArgumentError.new(":id attribute should only be passed as a lowercase symbol.")
alue
rue
r << " " << name
tring
r << " " << name << '="' << value << '"'

def __implicit_output__(content)

def __implicit_output__(content)
 @_state
true unless state.should_render?
ntent
lex::SGML::SafeObject
buffer << content.to_s
ring
buffer << Phlex::Escape.html_escape(content)
mbol
buffer << Phlex::Escape.html_escape(content.name)
l
rmatted_object = format_object(content))
.buffer << Phlex::Escape.html_escape(formatted_object)
n false

def __nested_attributes__(attributes, base_name, buffer = +"")

This allows us to skip many of the checks the `__attributes__` method must perform.
Provides the nested-attributes case for serializing out attributes.
def __nested_attributes__(attributes, base_name, buffer = +"")
tes.each do |k, v|
nless v
ot_key = (:_ == k))
= ""
nal_base_name = base_name
name = base_name.delete_suffix("-")
= case k
 String then k
 Symbol then k.name.tr("_", "-")
 raise Phlex::ArgumentError.new("Attribute keys should be Strings or Symbols")
me.match?(/[<>&"']/)
e Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")

rue
r << " " << base_name << name
tring
r << " " << base_name << name << '="' << v.gsub('"', "&quot;") << '"'
ymbol
r << " " << base_name << name << '="' << v.name.tr("_", "-").gsub('"', "&quot;") << '"'
nteger, Float
r << " " << base_name << name << '="' << v.to_s << '"'
ash
ted_attributes__(v, "#{base_name}#{name}-", buffer)
rray
r << " " << base_name << name << '="' << __nested_tokens__(v) << '"'
et
r << " " << base_name << name << '="' << __nested_tokens__(v.to_a) << '"'
hlex::SGML::SafeObject
r << " " << base_name << name << '="' << v.to_s.gsub('"', "&quot;") << '"'
 Phlex::ArgumentError.new("Invalid attribute value #{v.inspect}.")
t_key
name = original_base_name

def __nested_tokens__(tokens, sep = " ")

def __nested_tokens__(tokens, sep = " ")
= +""
th = 0, tokens.length
 < length
= tokens[i]
oken
tring
> 0
er << sep << token
er << token
ymbol
> 0
er << sep << token.name.tr("_", "-")
er << token.name.tr("_", "-")
nteger, Float, Phlex::SGML::SafeObject
> 0
er << sep << token.to_s
er << token.to_s
rray
ken.length > 0
 > 0
fer << sep << __nested_tokens__(token, sep)

fer << __nested_tokens__(token, sep)
il
nothing
 Phlex::ArgumentError.new("Invalid token type: #{token.class}.")

gsub('"', "&quot;")

def __styles__(styles)

Result is **unsafe**, so it should be escaped!
def __styles__(styles)
yles
ray, Set
.filter_map do |s|
s
String
 == "" || s.end_with?(";")

s};"
Phlex::SGML::SafeObject
e = s.to_s
e.end_with?(";") ? value : "#{value};"
Hash
 __styles__(s)
nil
 nil
e Phlex::ArgumentError.new("Invalid style: #{s.inspect}.")
in(" ")
sh
 = +""
.each do |k, v|
= case k
String
Symbol
me.tr("_", "-")
e Phlex::ArgumentError.new("Style keys should be Strings or Symbols.")
 = case v
String
Symbol
me.tr("_", "-")
Integer, Float, Phlex::SGML::SafeObject
_s
nil
e Phlex::ArgumentError.new("Invalid style value: #{v.inspect}")
lue
 == 0
fer << prop << ": " << value << ";"

fer << " " << prop << ": " << value << ";"
1

def __text__(content)

same as __implicit_output__ but escapes even `safe` objects
def __text__(content)
 @_state
true unless state.should_render?
ntent
ring
buffer << Phlex::Escape.html_escape(content)
mbol
buffer << Phlex::Escape.html_escape(content.name)
l
rmatted_object = format_object(content))
.buffer << Phlex::Escape.html_escape(formatted_object)
n false

def __yield_content__

def __yield_content__
unless block_given?
= @_state.buffer
l_length = buffer.bytesize
 = yield(self)
cit_output__(content) if original_length == buffer.bytesize

def __yield_content_with_args__(*a)

def __yield_content_with_args__(*a)
unless block_given?
= @_state.buffer
l_length = buffer.bytesize
 = yield(*a)
cit_output__(content) if original_length == buffer.bytesize

def __yield_content_with_no_yield_args__

def __yield_content_with_no_yield_args__
unless block_given?
= @_state.buffer
l_length = buffer.bytesize
 = yield # <-- doesn’t yield self 😉
cit_output__(content) if original_length == buffer.bytesize

def after_template

def after_template

def app_version_key

Override this method to use a different deployment key.
def app_version_key
DEPLOYED_AT

def around_template

def around_template

def before_template

def before_template

def cache(*cache_key, **, &content)

```
end
end
h1 { product.name }
cache product do
@products.each do |product|
```ruby

Cache a block of content.
def cache(*cache_key, **, &content)
	location = caller_locations(1, 1)[0]
	full_key = [
		app_version_key,                                   # invalidates the key when deploying new code in case of changes
		self.class.name,                                   # prevents collisions between classes
		(self.class.object_id if enable_cache_reloading?), # enables reloading
		location.base_label,                               # prevents collisions between different methods
		location.lineno,                                   # prevents collisions between different lines
		cache_key,                                         # allows for custom cache keys
	].freeze
	low_level_cache(full_key, **, &content)
	nil
end

def cache_store

Override this method to use a different cache store.
def cache_store
Cache store not implemented."

def call(...)

Render the view to a String. Arguments are delegated to {.new}.
def call(...)
	new(...).call
end

def call(buffer = +"", context: {}, fragments: nil, &)

def call(buffer = +"", context: {}, fragments: nil, &)
	state = Phlex::SGML::State.new(
		user_context: context,
		output_buffer: buffer,
		fragments: fragments&.to_set,
	)
	internal_call(parent: nil, state:, &)
	state.output_buffer << state.buffer
end

def capture(*args, &block)

Capture the output of the block and returns it as a string.
def capture(*args, &block)
	return "" unless block
	if args.length > 0
		@_state.capture { __yield_content_with_args__(*args, &block) }
	else
		@_state.capture { __yield_content__(&block) }
	end
end

def comment(&)

[MDN Docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Comments)

Wrap the output in an HTML comment.
def comment(&)
	state = @_state
	return unless state.should_render?
	buffer = state.buffer
	buffer << "<!-- "
	__yield_content__(&)
	buffer << " -->"
	nil
end

def context

def context
	if rendering?
		@_state.user_context
	else
		raise Phlex::ArgumentError.new(<<~MESSAGE)
			You can’t access the context before the component has started rendering.
		MESSAGE
	end
end

def enable_cache_reloading?

def enable_cache_reloading?

def erb(method_name, erb = nil, locals: nil, &block)

def erb(method_name, erb = nil, locals: nil, &block)
	loc = caller_locations(1, 1)[0]
	path = loc.path.delete_suffix(".rb")
	file = loc.path
	line = loc.lineno - 1
	unless erb
		method_path = "#{path}/#{method_name}.html.erb"
		sidecar_path = "#{path}.html.erb"
		if File.exist?(method_path)
			erb = File.read(method_path)
			file = method_path
			line = 1
		elsif method_name == :view_template && File.exist?(sidecar_path)
			erb = File.read(sidecar_path)
			file = sidecar_path
			line = 1
		else
			raise Phlex::RuntimeError.new(<<~MESSAGE)
				No ERB template found for #{method_name}
			MESSAGE
		end
	end
	code, _enc = ERBCompiler.compile(erb)
	class_eval(<<~RUBY, file, line)
		def #{method_name} #{locals}
			#{code}
		end
	RUBY
end

def flush

Flush the current state to the output buffer.
def flush
	@_state.flush
end

def format_object(object)

def format_object(object)
ject
oat, Integer
.to_s

def fragment(name)

Define a named fragment that can be selectively rendered.
def fragment(name)
	state = @_state
	state.begin_fragment(name)
	yield
	state.end_fragment(name)
	nil
end

def internal_call(parent: nil, state: nil, &block)

def internal_call(parent: nil, state: nil, &block)
	if @_state
		raise Phlex::DoubleRenderError.new(
			"You can't render a #{self.class.name} more than once."
		)
	end
	@_state = state
	return "" unless render?
	block ||= @_content_block
	Thread.current[:__phlex_component__] = [self, Fiber.current.object_id].freeze
	state.around_render(self) do
		before_template(&block)
		around_template do
			if block
				view_template do |*args|
					if args.length > 0
						__yield_content_with_args__(*args, &block)
					else
						__yield_content__(&block)
					end
				end
			else
				view_template
			end
		end
		after_template(&block)
	end
ensure
	Thread.current[:__phlex_component__] = [parent, Fiber.current.object_id].freeze
end

def json_escape(string)

def json_escape(string)
	ERB::Util.json_escape(string)
end

def low_level_cache(cache_key, **options, &content)

If you need to pass multiple cache keys, you should pass an array.
Note: To allow you more control, this method does not take a splat of cache keys.

```
end
markdown(@content)
low_level_cache([Commonmarker::VERSION, Digest::MD5.hexdigest(@content)]) do
```ruby

and responsibility for the cache key, use this method.
If you really know what you’re doing and want to take full control
Cache a block of content where you control the entire cache key.
def low_level_cache(cache_key, **options, &content)
	state = @_state
	cached_buffer, fragment_map = cache_store.fetch(cache_key, **options) { state.caching(&content) }
	if state.should_render?
		fragment_map.each do |fragment_name, (offset, length, nested_fragments)|
			state.record_fragment(fragment_name, offset, length, nested_fragments)
		end
		state.buffer << cached_buffer
	else
		fragment_map.each do |fragment_name, (offset, length, nested_fragments)|
			if state.fragments.include?(fragment_name)
				state.fragments.delete(fragment_name)
				state.fragments.subtract(nested_fragments)
				state.buffer << cached_buffer.byteslice(offset, length)
			end
		end
	end
	nil
end

def new(*a, **k, &block)

Other tags:
    Note: - The block will not be delegated to {#initialize}. Instead, it will be sent to {#view_template} when rendering.
def new(*a, **k, &block)
	if block
		object = super(*a, **k, &nil)
		object.instance_exec { @_content_block = block }
		object
	else
		super
	end
end

def plain(content)

Output plain text.
def plain(content)
	unless __text__(content)
		raise Phlex::ArgumentError.new("You've passed an object to plain that is not handled by format_object. See https://rubydoc.info/gems/phlex/Phlex/SGML#format_object-instance_method for more information")
	end
	nil
end

def raw(content)

Output the given safe object as-is. You may need to use `safe` to mark a string as a safe object.
def raw(content)
	case content
	when Phlex::SGML::SafeObject
		state = @_state
		return unless state.should_render?
		state.buffer << content.to_s
	when nil, "" # do nothing
	else
		raise Phlex::ArgumentError.new("You passed an unsafe object to `raw`.")
	end
	nil
end

def render(renderable = nil, &)

def render(renderable = nil, &)
	case renderable
	when Phlex::SGML
		renderable.internal_call(state: @_state, parent: self, &)
	when Class
		if renderable < Phlex::SGML
			render(renderable.new, &)
		end
	when Enumerable
		renderable.each { |r| render(r, &) }
	when Proc, Method
		if renderable.arity == 0
			__yield_content_with_no_yield_args__(&renderable)
		else
			__yield_content__(&renderable)
		end
	when String
		plain(renderable)
	when nil
		__yield_content__(&) if block_given?
	else
		raise Phlex::ArgumentError.new("You can't render a #{renderable.inspect}.")
	end
	nil
end

def render?

def render?

def rendering?

It will not reset back to false after rendering.
Returns `false` before rendering and `true` once the component has started rendering.
def rendering?
	!!@_state
end

def safe(value)

Mark the given string as safe for HTML output.
def safe(value)
	case value
	when String
		Phlex::SGML::SafeValue.new(value)
	else
		raise Phlex::ArgumentError.new("Expected a String.")
	end
end

def to_proc

def to_proc
	proc { |c| c.render(self) }
end

def vanish(...)

def vanish(...)
(...)

def view_template

def view_template
	if block_given?
		yield
	else
		plain "Phlex Warning: Your `#{self.class.name}` class doesn't define a `view_template` method. If you are upgrading to Phlex 2.x make sure to rename your `template` method to `view_template`. See: https://beta.phlex.fun/guides/v2-upgrade.html"
	end
end

def whitespace(&)

Output a single space character. If a block is given, a space will be output before and after the block.
def whitespace(&)
	state = @_state
	return unless state.should_render?
	buffer = state.buffer
	buffer << " "
	if block_given?
		__yield_content__(&)
		buffer << " "
	end
	nil
end