# frozen_string_literal: true require "json" require "set" module Hanami class View module Helpers module TagHelper # Tag builder returned from {TagHelper#tag}. # # @see TagHelper#tag # # @api public # @since 2.0.0 class TagBuilder # @api private # @since 2.0.0 HTML_VOID_ELEMENTS = %i( area base br col embed hr img input keygen link meta param source track wbr ).to_set # @api private # @since 2.0.0 SVG_SELF_CLOSING_ELEMENTS = %i( animate animateMotion animateTransform circle ellipse line path polygon polyline rect set stop use view ).to_set # @api private # @since 2.0.0 ATTRIBUTE_SEPARATOR = " " # @api private # @since 2.0.0 BOOLEAN_ATTRIBUTES = %w( allowfullscreen allowpaymentrequest async autofocus autoplay checked compact controls declare default defaultchecked defaultmuted defaultselected defer disabled enabled formnovalidate hidden indeterminate inert ismap itemscope loop multiple muted nohref nomodule noresize noshade novalidate nowrap open pauseonexit playsinline readonly required reversed scoped seamless selected sortable truespeed typemustmatch visible ).to_set BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map(&:to_sym)) BOOLEAN_ATTRIBUTES.freeze # @api private # @since 2.0.0 ARIA_PREFIXES = ["aria", :aria].to_set.freeze # @api private # @since 2.0.0 DATA_PREFIXES = ["data", :data].to_set.freeze # @api private # @since 2.0.0 TAG_TYPES = {}.tap do |hsh| BOOLEAN_ATTRIBUTES.each { |attr| hsh[attr] = :boolean } DATA_PREFIXES.each { |attr| hsh[attr] = :data } ARIA_PREFIXES.each { |attr| hsh[attr] = :aria } hsh.freeze end # @api private # @since 2.0.0 PRE_CONTENT_STRINGS = Hash.new { "" } PRE_CONTENT_STRINGS[:textarea] = "\n" PRE_CONTENT_STRINGS["textarea"] = "\n" PRE_CONTENT_STRINGS.freeze # @api private # @since 2.0.0 attr_reader :inflector # @api private # @since 2.0.0 def initialize(inflector:) @inflector = inflector end # Transforms a Hash into HTML Attributes, ready to be interpolated into # ERB. # # @example # > # # => # # @api public # @since 2.0.0 def attributes(**attributes) tag_options(**attributes).to_s.strip.html_safe end # Returns a `
` HTML tag. # # @api public # @since 2.0.0 def p(*args, **options, &block) tag_string(:p, *args, **options, &block) end # @api private # @since 2.0.0 def tag_string(name, content = nil, **options) content = yield if block_given? self_closing = SVG_SELF_CLOSING_ELEMENTS.include?(name) if (HTML_VOID_ELEMENTS.include?(name) || self_closing) && content.nil? "<#{inflector.dasherize(name.to_s)}#{tag_options(**options)}#{self_closing ? " />" : ">"}".html_safe else content_tag_string(inflector.dasherize(name.to_s), content || "", **options) end end # @api private # @since 2.0.0 def content_tag_string(name, content, **options) tag_options = tag_options(**options) unless options.empty? name = EscapeHelper.escape_xml_name(name) content = EscapeHelper.escape_html(content) "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name]}#{content}#{name}>".html_safe end # @api private # @since 2.0.0 def tag_options(**options) return if options.none? output = +"" options.each_pair do |key, value| type = TAG_TYPES[key] if type == :data && value.is_a?(Hash) value.each_pair do |k, v| next if v.nil? output << ATTRIBUTE_SEPARATOR output << prefix_tag_option(key, k, v) end elsif type == :aria && value.is_a?(Hash) value.each_pair do |k, v| next if v.nil? case v when Array, Hash tokens = TagHelper.build_tag_values(v) next if tokens.none? v = EscapeHelper.escape_join(tokens, " ") else v = v.to_s end output << ATTRIBUTE_SEPARATOR output << prefix_tag_option(key, k, v) end elsif type == :boolean if value output << ATTRIBUTE_SEPARATOR output << boolean_tag_option(key) end elsif !value.nil? output << ATTRIBUTE_SEPARATOR output << tag_option(key, value) end end output unless output.empty? end # @api private # @since 2.0.0 def boolean_tag_option(key) %(#{key}="#{key}") end # @api private # @since 2.0.0 def tag_option(key, value) key = EscapeHelper.escape_xml_name(key) case value when Array, Hash value = TagHelper.build_tag_values(value) if key.to_s == "class" value = EscapeHelper.escape_join(value, " ") when Regexp value = EscapeHelper.escape_html(value.source) else value = EscapeHelper.escape_html(value) end value = value.gsub('"', """) if value.include?('"') %(#{key}="#{value}") end private # @api private # @since 2.0.0 def method_missing(called, *args, **options, &block) tag_string(called, *args, **options, &block) end # @api private # @since 2.0.0 def respond_to_missing?(*args) true end # @api private # @since 2.0.0 def prefix_tag_option(prefix, key, value) key = "#{prefix}-#{inflector.dasherize(key.to_s)}" unless value.is_a?(String) || value.is_a?(Symbol) value = value.to_json end tag_option(key, value) end end end end end end