lib/phlex/component.rb
# frozen_string_literal: true if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.0") using Overrides::Symbol::Name end module Phlex class Component extend HTML include Renderable class << self attr_accessor :rendered_at_least_once end def call(buffer = +"", view_context: nil, parent: nil, &block) raise "The same component instance shouldn't be rendered twice" if rendered? @_rendered = true @_target = buffer @_view_context = view_context @_parent = parent @output_buffer = self template(&block) self.class.rendered_at_least_once ||= true buffer end def rendered? @_rendered ||= false end HTML::STANDARD_ELEMENTS.each do |element| register_element(element) end HTML::VOID_ELEMENTS.each do |element| register_void_element(element) end register_element :template_tag, tag: "template" def content(&block) return unless block_given? original_length = @_target.length output = yield(self) if block_given? unchanged = (original_length == @_target.length) text(output) if unchanged && output.is_a?(String) nil end def text(content) @_target << CGI.escape_html(content) nil end def whitespace @_target << " " nil end def doctype @_target << HTML::DOCTYPE nil end def raw(content) @_target << content nil end def html_safe? true end def safe_append=(value) return unless value @_target << case value when String then value when Symbol then value.name else value.to_s end end def append=(value) return unless value if value.html_safe? self.safe_append = value else @_target << case value when String then CGI.escape_html(value) when Symbol then CGI.escape_html(value.name) else CGI.escape_html(value.to_s) end end end def capture(&block) return unless block_given? original_buffer = @_target new_buffer = +"" @_target = new_buffer yield @_target = original_buffer new_buffer.html_safe end def classes(*tokens, **conditional_tokens) { class: self.tokens(*tokens, **conditional_tokens) } end def tokens(*tokens, **conditional_tokens) conditional_tokens.each do |condition, token| case condition when Symbol then next unless send(condition) when Proc then next unless condition.call else raise ArgumentError, "The class condition must be a Symbol or a Proc." end case token when Symbol then tokens << token.name when String then tokens << token when Array then tokens.concat(t) else raise ArgumentError, "Conditional classes must be Symbols, Strings, or Arrays of Symbols or Strings." end end tokens.compact.join(" ") end def _attributes(attributes, buffer: +"") if attributes[:href]&.start_with?(/\s*javascript/) attributes[:href] = attributes[:href].sub(/^\s*(javascript:)+/, "") end _build_attributes(attributes, buffer: buffer) unless self.class.rendered_at_least_once Phlex::ATTRIBUTE_CACHE[attributes.hash] = buffer.freeze end buffer end def _build_attributes(attributes, buffer:) attributes.each do |k, v| next unless v name = case k when String k when Symbol k.name.tr("_", "-") else k.to_s end if HTML::EVENT_ATTRIBUTES[name] || name.match?(/[<>&"']/) raise ArgumentError, "Unsafe attribute name detected: #{k}." end case v when true buffer << " " << name when String buffer << " " << name << '="' << CGI.escape_html(v) << '"' when Symbol buffer << " " << name << '="' << CGI.escape_html(v.name) << '"' when Hash _build_attributes(v.transform_keys { "#{k}-#{_1}" }, buffer: buffer) else buffer << " " << name << '="' << CGI.escape_html(v.to_s) << '"' end end buffer end end end