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 = +"")
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('"', """) << '"' ymbol r << " " << base_name << name << '="' << v.name.tr("_", "-").gsub('"', """) << '"' 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('"', """) << '"' 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('"', """)
def __styles__(styles)
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)
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
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
def cache_store Cache store not implemented."
def call(...)
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)
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(&)
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
def flush @_state.flush end
def format_object(object)
def format_object(object) ject oat, Integer .to_s
def fragment(name)
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)
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)
- 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)
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)
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?
Returns `false` before rendering and `true` once the component has started rendering.
def rendering? !!@_state end
def safe(value)
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(&)
def whitespace(&) state = @_state return unless state.should_render? buffer = state.buffer buffer << " " if block_given? __yield_content__(&) buffer << " " end nil end