class Phlex::SGML
**Standard Generalized Markup Language** for behaviour common to {HTML} and {SVG}.
Constants
- ERBCompiler
- REF_ATTRIBUTES
- UNSAFE_ATTRIBUTES
Public Class Methods
Source
# File lib/phlex/sgml.rb, line 23 def call(...) new(...).call end
Render the view to a String. Arguments are delegated to {.new}.
Source
# File lib/phlex/sgml.rb, line 39 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
Source
# File lib/phlex/sgml.rb, line 722 def self.method_added(method_name) if method_name == :view_template location = instance_method(method_name).source_location[0] if location[0] in "/" | "." Phlex.__expand_attribute_cache__(location) end else super end end
Source
# File lib/phlex/sgml.rb, line 29 def new(*a, **k, &block) if block object = super(*a, **k, &nil) object.instance_exec { @_content_block = block } object else super end end
Create a new instance of the component. @note The block will not be delegated to {#initialize}. Instead, it will be sent to {#view_template} when rendering.
Public Instance Methods
Source
# File lib/phlex/sgml.rb, line 283 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
Cache a block of content.
“‘ruby @products.each do |product|
cache product do h1 { product.name } end
end “‘
Source
# File lib/phlex/sgml.rb, line 86 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
Source
# File lib/phlex/sgml.rb, line 211 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
Capture the output of the block and returns it as a string.
Source
# File lib/phlex/sgml.rb, line 181 def comment(&) state = @_state return unless state.should_render? buffer = state.buffer buffer << "<!-- " __yield_content__(&) buffer << " -->" nil end
Wrap the output in an HTML comment.
[MDN Docs](developer.mozilla.org/en-US/docs/Web/HTML/Comments)
Source
# File lib/phlex/sgml.rb, line 136 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
Source
# File lib/phlex/sgml.rb, line 243 def flush @_state.flush end
Flush the current state to the output buffer.
Source
# File lib/phlex/sgml.rb, line 222 def fragment(name) state = @_state state.begin_fragment(name) yield state.end_fragment(name) nil end
Define a named fragment that can be selectively rendered.
Source
# File lib/phlex/sgml.rb, line 98 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
Source
# File lib/phlex/sgml.rb, line 335 def json_escape(string) ERB::Util.json_escape(string) end
Source
# File lib/phlex/sgml.rb, line 312 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
Cache a block of content where you control the entire cache key. If you really know what you’re doing and want to take full control and responsibility for the cache key, use this method.
“‘ruby low_level_cache([Commonmarker::VERSION, Digest::MD5.hexdigest(@content)]) do
markdown(@content)
end “‘
Note: To allow you more control, this method does not take a splat of cache keys. If you need to pass multiple cache keys, you should pass an array.
Source
# File lib/phlex/sgml.rb, line 153 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
Output plain text.
Source
# File lib/phlex/sgml.rb, line 195 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
Output the given safe object as-is. You may need to use ‘safe` to mark a string as a safe object.
Source
# File lib/phlex/sgml.rb, line 247 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
Source
# File lib/phlex/sgml.rb, line 148 def rendering? !!@_state end
Returns ‘false` before rendering and `true` once the component has started rendering. It will not reset back to false after rendering.
Source
# File lib/phlex/sgml.rb, line 231 def safe(value) case value when String Phlex::SGML::SafeValue.new(value) else raise Phlex::ArgumentError.new("Expected a String.") end end
Mark the given string as safe for HTML output.
Source
# File lib/phlex/sgml.rb, line 74 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
Source
# File lib/phlex/sgml.rb, line 162 def whitespace(&) state = @_state return unless state.should_render? buffer = state.buffer buffer << " " if block_given? __yield_content__(&) buffer << " " end nil end
Output a single space character. If a block is given, a space will be output before and after the block.
Private Instance Methods
Source
# File lib/phlex/sgml.rb, line 465 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('"', """) when Symbol v.name.tr("_", "-").gsub('"', """) 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('"', """) else __nested_attributes__(v, "#{name}-", buffer) end when Array case k when :style __styles__(v).gsub('"', """) else __nested_tokens__(v) end when Set case k when :style __styles__(v).gsub('"', """) else __nested_tokens__(v.to_a) end when Phlex::SGML::SafeObject v.to_s.gsub('"', """) 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
Source
# File lib/phlex/sgml.rb, line 418 def __implicit_output__(content) state = @_state return true unless state.should_render? case content when Phlex::SGML::SafeObject state.buffer << content.to_s when String state.buffer << Phlex::Escape.html_escape(content) when Symbol state.buffer << Phlex::Escape.html_escape(content.name) when nil nil else if (formatted_object = format_object(content)) state.buffer << Phlex::Escape.html_escape(formatted_object) else return false end end true end
Source
# File lib/phlex/sgml.rb, line 562 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('"', """) << '"' when Symbol buffer << " " << base_name << name << '="' << v.name.tr("_", "-").gsub('"', """) << '"' 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('"', """) << '"' else raise Phlex::ArgumentError.new("Invalid attribute value #{v.inspect}.") end if root_key base_name = original_base_name end buffer end end
Provides the nested-attributes case for serializing out attributes. This allows us to skip many of the checks the ‘__attributes__` method must perform.
Source
# File lib/phlex/sgml.rb, line 611 def __nested_tokens__(tokens, sep = " ") buffer = +"" i, length = 0, tokens.length while i < length token = tokens[i] case token when String 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) else buffer << __nested_tokens__(token, sep) end end when nil # Do nothing else raise Phlex::ArgumentError.new("Invalid token type: #{token.class}.") end i += 1 end buffer.gsub('"', """) end
Source
# File lib/phlex/sgml.rb, line 659 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 end
Result is unsafe, so it should be escaped!
Source
# File lib/phlex/sgml.rb, line 443 def __text__(content) state = @_state return true unless state.should_render? case content when String state.buffer << Phlex::Escape.html_escape(content) when Symbol state.buffer << Phlex::Escape.html_escape(content.name) when nil nil else if (formatted_object = format_object(content)) state.buffer << Phlex::Escape.html_escape(formatted_object) else return false end end true end
same as implicit_output but escapes even ‘safe` objects
Source
# File lib/phlex/sgml.rb, line 382 def __yield_content__ return unless block_given? buffer = @_state.buffer original_length = buffer.bytesize content = yield(self) __implicit_output__(content) if original_length == buffer.bytesize nil end
Source
# File lib/phlex/sgml.rb, line 406 def __yield_content_with_args__(*a) return unless block_given? buffer = @_state.buffer original_length = buffer.bytesize content = yield(*a) __implicit_output__(content) if original_length == buffer.bytesize nil end
Source
# File lib/phlex/sgml.rb, line 394 def __yield_content_with_no_yield_args__ return unless block_given? buffer = @_state.buffer original_length = buffer.bytesize content = yield # <-- doesn’t yield self 😉 __implicit_output__(content) if original_length == buffer.bytesize nil end
Source
# File lib/phlex/sgml.rb, line 340 def app_version_key Phlex::DEPLOYED_AT end
Override this method to use a different deployment key.
Source
# File lib/phlex/sgml.rb, line 369 def around_template yield nil end
Source
# File lib/phlex/sgml.rb, line 345 def cache_store raise "Cache store not implemented." end
Override this method to use a different cache store.
Source
# File lib/phlex/sgml.rb, line 349 def enable_cache_reloading? false end
Source
# File lib/phlex/sgml.rb, line 362 def format_object(object) case object when Float, Integer object.to_s end end