require 'lotus/utils' # RUBY_VERSION >= '2.2' require 'lotus/utils/escape' require 'lotus/helpers/html_helper/empty_html_node' require 'lotus/helpers/html_helper/html_node' module Lotus module Helpers module HtmlHelper # HTML Builder # # @since 0.1.0 class HtmlBuilder # HTML5 content tags # # @since 0.1.0 # @api private # # @see Lotus::Helpers::HtmlHelper::HtmlNode # @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element CONTENT_TAGS = [ 'a', 'abbr', 'address', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'blockquote', 'body', 'button', 'canvas', 'caption', 'cite', 'code', 'colgroup', 'data', 'datalist', 'del', 'details', 'dfn', 'div', 'dl', 'dt', 'em', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'i', 'iframe', 'ins', 'kbd', 'label', 'legend', 'li', 'link', 'main', 'map', 'mark', 'math', 'menu', 'meter', 'nav', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'section', 'select', 'small', 'span', 'strong', 'style', 'sub', 'summary', 'sup', 'svg', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'u', 'ul', 'video', ].freeze # HTML5 empty tags # # @since 0.1.0 # @api private # # @see Lotus::Helpers::HtmlHelper::EmptyHtmlNode # @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element EMPTY_TAGS = [ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr', ].freeze # New line separator # # @since 0.1.0 # @api private NEWLINE = "\n".freeze CONTENT_TAGS.each do |tag| class_eval %{ def #{ tag }(content = nil, attributes = nil, &blk) @nodes << HtmlNode.new(:#{ tag }, blk || content, attributes || content) self end } end EMPTY_TAGS.each do |tag| class_eval %{ def #{ tag }(attributes = nil) @nodes << EmptyHtmlNode.new(:#{ tag }, attributes) self end } end # Initialize a new builder # # @return [Lotus::Helpers::HtmlHelper::HtmlBuilder] the builder # # @since 0.1.0 # @api private def initialize @nodes = [] end # Define a custom tag # # @param name [Symbol,String] the name of the tag # @param content [String,Lotus::Helpers::HtmlHelper::HtmlBuilder,NilClass] the optional content # @param attributes [Hash,NilClass] the optional tag attributes # @param blk [Proc] the optional nested content espressed as a block # # @return [self] # # @since 0.1.0 # @api public # # @see Lotus::Helpers::HtmlHelper # # @example # html.tag(:custom) # => # # html.tag(:custom, 'foo') # => foo # # html.tag(:custom, html.p('hello')) # =>

hello

# # html.tag(:custom) { 'foo' } # # => # # # # foo # # # # html.tag(:custom) do # p 'hello' # end # # => # # # #

hello

# #
# # html.tag(:custom, 'hello', id: 'foo', 'data-xyz': 'bar') # => hello # # html.tag(:custom, id: 'foo') { 'hello' } # # => # # # # hello # # def tag(name, content = nil, attributes = nil, &blk) @nodes << HtmlNode.new(name, blk || content, attributes || content) self end # Defines a custom empty tag # # @param name [Symbol,String] the name of the tag # @param attributes [Hash,NilClass] the optional tag attributes # # @return [self] # # @since 0.1.0 # @api public # # @see Lotus::Helpers::HtmlHelper # # @example # html.empty_tag(:xr) # => # # html.empty_tag(:xr, id: 'foo') # => # # html.empty_tag(:xr, id: 'foo', 'data-xyz': 'bar') # => def empty_tag(name, attributes = nil) @nodes << EmptyHtmlNode.new(name, attributes) self end # Resolves all the nodes and generates the markup # # @return [Lotus::Utils::Escape::SafeString] the output # # @since 0.1.0 # @api private # # @see http://www.rubydoc.info/gems/lotus-utils/Lotus/Utils/Escape/SafeString def to_s Utils::Escape::SafeString.new(@nodes.map(&:to_s).join(NEWLINE)) end # Check if there are nested nodes # # @return [TrueClass,FalseClass] the result of the check # # @since 0.1.0 # @api private def nested? @nodes.any? end # Resolve the context for nested contents # # @since 0.1.0 # @api private if RUBY_VERSION >= '2.2' && !Utils.jruby? def resolve(&blk) @context = blk.binding.receiver instance_exec(&blk) end else def resolve(&blk) @context = eval 'self', blk.binding instance_exec(&blk) end end # Forward missing methods to the current context. # This allows to access views local variables from nested content blocks. # # @since 0.1.0 # @api private def method_missing(m, *args, &blk) @context.__send__(m, *args, &blk) end end end end end