module WWW_App::TO

Constants

COMMA
GEM_PATH
INVALID_SCRIPT_TYPE_CHARS
JS_FILE_PATHS
KEY_REQUIRED
NOTHING
SPACE
VERSION

Public Instance Methods

to_html(*args) click to toggle source
# File lib/www_app/TO.rb, line 168
    def to_html *args
      return @mustache.render(*args) if instance_variable_defined?(:@mustache)
      instance_eval(&@blok) if @tags.empty?

      final     = ""
      indent    = 0
      todo      = @tags.dup
      last      = nil
      stacks    = {:js=>[], :script_tags=>[]}
      last_open = nil

      script_libs_added = false

      doc = [
        doc_type   = {:tag_name=>:doc_type , :text => "<!DOCTYPE html>"},
        html       = {:tag_name=>:html     , :children=>[
          head     = {:tag_name=>:head     , :lang=>'en', :children=>[]},
          body     = {:tag_name=>:body     , :children=>[]}
        ]}
      ]
      style_tags = {:tag_name => :style_tags, :children => []}

      tags = @tags.dup
      while (t = tags.shift)
        t_name = t[:tag_name]
        parent = t[:parent]

        case # ==============
        when t_name == :title && !parent
          fail "Title already set." if head[:children].detect { |c| c[:tag_name] == :title }
          head[:children] << t

        when t_name == :meta
          head[:children] << t

        when t_name == :style
          style_tags[:children] << t

        when t_name == :link
          head[:children] << t

        when t_name == :_ && !parent
          body[:css] = (body[:css] || {}).merge(t[:css]) if t[:css]
          body[:class] = (body[:class] || []).concat(t[:class]) if t[:class]

          if t[:id]
            fail ":body already has id: #{body[:id].inspect}, #{t[:id]}" if body[:id]
            body[:id]  = t[:id] 
          end

          if t[:children]
            body[:children].concat t[:children]
            tags = t[:children].dup.concat(tags)
          end

        else # ==============
          if !parent
            body[:children] << t
          end

          if t[:css]
            style_tags[:children] << t
          end

          if t[:children]
            tags = t[:children].dup.concat(tags)
          end

          if t_name == :script
            stacks[:script_tags] << t
          end

          if t_name == :js
            stacks[:js].concat [css_selector(t[:parent])].concat(t[:value])
          end

        end # === case ========
      end # === while

      if body[:css] && !body[:css].empty?
        style_tags[:children] << body
      end

      is_fragment = stacks[:script_tags].empty? && stacks[:js].empty? && style_tags[:children].empty? && head[:children].empty? && body.values_at(:css, :id, :class).compact.empty?

      if is_fragment
        doc = body[:children]

      else # is doc
        head[:children] << style_tags
        content_type = head[:children].detect { |t| t[:tag_name] == :meta && t[:http_equiv] && t[:http_equiv].downcase=='Content-Type'.downcase }
        if !content_type
          head[:children].unshift(
            {:tag_name=>:meta, :http_equiv=>'Content-Type', :content=>"text/html; charset=UTF-8"}
          )
        end

      end # if is_fragment

      todo = doc.dup
      while (tag = todo.shift)
        t_name = tag.is_a?(Hash) && tag[:tag_name]

        case

        when tag == :new_line
          final << NEW_LINE

        when tag == :open
          attributes = stacks.delete :attributes

          tag_sym = todo.shift

          if [:script].include?(tag_sym) || (todo.first != :close && !indent.zero? && !HTML::NO_NEW_LINES.include?(last_open))
            final << NEW_LINE << SPACES(indent)
          end

          if HTML::SELF_CLOSING_TAGS.include?(tag_sym)
            final << (
              attributes ?
              "<#{tag_sym} #{attributes} />\n" :
              "<#{tag_sym} />\n"
            )
            if todo.first == :close && todo[1] == tag_sym
              todo.shift
              todo.shift
            end

          else # === has closing tag
            if attributes
              final << "<#{tag_sym} #{attributes}>"
            else
              final << "<#{tag_sym}>"
            end
          end # === if HTML

          last = indent
          indent += 2
          last_open = tag_sym


        when tag == :close
          indent -= 2
          if last != indent
            final << SPACES(indent)
          end
          last = indent
          final << "</#{todo.shift}>"

        when tag == :clean_attrs
          attributes = todo.shift
          target     = todo.shift
          tag_name   = target[:tag_name]

          attributes.each { |attr, val|
            attributes[attr] = case

                               when attr == :src && tag_name == :script
                                 fail ::ArgumentError, "Invalid type: #{val.inspect}" unless val.is_a?(String)
                                 Clean.relative_href val

                               when attr == :type && tag_name == :script
                                 clean = val.gsub(INVALID_SCRIPT_TYPE_CHARS, '')
                                 clean = 'text/unknown' if clean.empty?
                                 clean

                               when attr == :type && val == :hidden
                                 'hidden'

                               when attr == :href && tag_name == :a
                                 if val.is_a? Symbol
                                   Clean.mustache :href, val
                                 else
                                   Clean.href val
                                 end

                               when [:action, :src, :href].include?(attr)
                                 Clean.relative_href(val)

                               when attr == :id
                                 Clean.html_id(val.to_s)

                               when attr == :class
                                 val.map { |name|
                                   Clean.css_class_name(name.to_s)
                                 }.join(" ".freeze)

                               when tag_name == :style && attr == :type
                                 'text/css'

                               when tag_name == :label && attr == :for && val.is_a?(::Symbol)
                                 Clean.html(val.to_s)

                               when ::WWW_App::HTML::TAGS_TO_ATTRIBUTES[tag_name].include?(attr)
                                 Clean.html(val)

                               else
                                 fail "Invalid attr: #{attr.inspect}"

                               end # case attr

          } # === each attr

          stacks[:attributes] = attributes.inject([]) { |memo, (k,v)|
            memo << "#{k}=\"#{v}\""
            memo
          }.join " ".freeze

        when t_name == :doc_type
          if tag[:text] == "<!DOCTYPE html>"
            final << tag[:text]
            final << NEW_LINE
          else
            fail "Unknown doc type: #{tag[:text].inspect}"
          end

        when t_name == :text
          final.<<(
            tag[:skip_escape] ?
            tag[:value] :
            Clean.html(tag[:value])
          )

        when t_name == :link
          final << (
            %^#{SPACES(indent)}<link type="text/css" rel="stylesheet" href="#{::Escape_Escape_Escape.relative_href tag[:href]}" />^
          )

        when t_name == :meta
          case
          when tag[:http_equiv]
            key_name    = "http-equiv"
            key_content = tag[:http_equiv].gsub(/[^a-zA-Z\/\;\ 0-9\=\-]+/, '')
            content     = tag[:content].gsub(/[^a-zA-Z\/\;\ 0-9\=\-]+/, '')
          else
            fail ArgumentError, tag.keys.inspect
          end

          final << (
            %^#{SPACES(indent)}<meta #{key_name}="#{key_content}" content="#{content}" />\n^
          )

        when t_name == :html       # === :html tag ================
          todo = [
            :clean_attrs, {:lang=>(tag[:lang] || 'en')}, tag,
            :open, :html
          ].concat(tag[:children]).concat([:new_line, :close, :html]).concat(todo)

        when t_name == :head       # === :head tag =================
          todo = [ :open, :head, :new_line ].
            concat(tag[:children] || []).
            concat([:new_line, :close, :head]).
            concat(todo)

        when t_name == :title && !parent(tag)
          todo = [
            :open, :title
          ].concat(tag[:children]).concat([:close, :title]).concat(todo)

        when t_name == :_       # =============== :_ tag ========
          nil # do nothing

        when t_name == :js
          next

        when t_name == :script  # =============== :script tag ===

          attrs = tag.select { |k, v|
            k == :src || k == :type || k == :class
          }

          new_todo = []

          if attrs[:src] && !script_libs_added
            new_todo << {:tag_name=>:js_to_script_tag}
            script_libs_added = true
          end

          new_todo.concat [
            :clean_attrs, attrs, tag,
            :open, :script,
          ]

          new_todo.concat(tag[:children]) if tag[:children]

          if tag[:children] && !tag[:children].empty? && tag[:children].first[:tag_name] != :text && tag[:children].last[:tag_name] != :text
            new_todo << :new_line
          end

          new_todo.concat [
            :close, :script
          ].concat(todo)

          todo = new_todo

        when t_name == :js_to_script_tag
          next if stacks[:js].empty? && stacks[:script_tags].empty?
          stacks[:clean_text] ||= lambda { |raw_x|
            x = case raw_x
                when ::Symbol, ::String
                  Clean.html(raw_x.to_s)
                when ::Array
                  raw_x.map { |x| stacks[:clean_text].call x }
                when ::Numeric
                  x
                else
                  fail "Unknown type for json: #{raw_x.inspect}"
                end
          }

          script_tag = {:tag_name=>:script}.freeze

          new_todo = []
          JS_FILE_PATHS.each { |path|
            new_todo.concat [
              :clean_attrs, {:src=>path}, script_tag,
              :open, :script,
              :close, :script
            ]
          }

          if !stacks[:js].empty?
            clean_vals = stacks[:js].map { |raw_x| stacks[:clean_text].call(raw_x) }
            content = <<-EOF
            \n#{SPACES(indent)}WWW_App.run( #{::Escape_Escape_Escape.json_encode(code: clean_vals)} );
            EOF

            new_todo.concat [
              :clean_attrs, {:type=>'application/javascript'}, script_tag,
              :open, :script,
              {:tag_name=>:text, :skip_escape=>true, :value=> content },
              :close, :script
            ]
          end

          todo = new_todo.concat(todo)

        when tag == :javascript
          vals = todo.shift

        when tag == :to_json
          vals = todo.shift
          ::Escape_Escape_Escape.json_encode(to_clean_text(:javascript, vals))


        when t_name == :style
          next

        when t_name == :style_tags # =============== <style ..> TAG =================
          next if tag[:children].empty?

          style_and_other_tags = tag[:children].dup
          flatten = []

          # === flatten groups
          #  style
          #    div, span {
          #      a:link, a:visited {
          #  --->
          #  style
          #    div a:link, div a:visited, span a:link, span a:visited  {
          #
          prev = nil

          while e = style_and_other_tags.shift
            case
            when e[:tag_name] == :style
              style_and_other_tags = e[:children].dup.concat(style_and_other_tags)

            when e[:tag_name] == :group
              style_and_other_tags = e[:children].dup.concat(style_and_other_tags)
              prev = nil
              flatten << e

            when parent?(e, :group)
              if e[:__]
                e[:__children] = []
              end

              if prev && prev[:__]
                prev[:__children] << e
                e[:__parent] = prev
              end
              prev = e

            else
              flatten << e

            end # === case
          end # === while

          todo = [
            :clean_attrs, {:type=>'text/css'}, {:tag_name=>:style},
            :open, :style,
            :flat_style_groups, flatten,
            :close, :style
          ].concat(todo)

        when tag == :flat_style_groups
          indent += 2
          css_final = ""
          flatten   = todo.shift

          #
          # Each produces:
          #
          #   selectors {
          #     escaped/sanitized css;
          #   }
          #
          flatten.each { |style|
            next if !style[:css] || style[:css].empty?

            css_final << "\n" << SPACES(indent) << css_selector(style, :full) << " {\n".freeze

            the_css = style[:css] || (parent?(style, :group) && style[:parent][:css])
            if the_css
              indent += 2
              the_css.each { |raw_k, raw_val|
                name = begin
                          clean_k = ::WWW_App::Clean.css_attr(raw_k.to_s.gsub('_','-'))
                          fail("Invalid name for css property name: #{raw_k.inspect}") if !clean_k || clean_k.empty?
                          clean_k
                        end

                raw_val  = raw_val.is_a?(Array) ? raw_val.join(COMMA) : raw_val.to_s

                v = case

                    when name[IMAGE_AT_END]
                      case raw_val
                      when 'inherit', 'none'
                        raw_val
                      else
                        "url(#{Clean.href(raw_val)})"
                      end

                    when ::WWW_App::CSS::PROPERTIES.include?(raw_k)
                      Clean.css raw_k, raw_val

                    else
                      fail "Invalid css attr: #{name.inspect}"

                    end # === case

                css_final << SPACES(indent) << "#{name}: #{v};\n"
              } # === each style
              indent -= 2
            end # === if style[:css]

            css_final << SPACES(indent) << "}\n".freeze << SPACES(indent - 2)
          }

          indent -= 2
          todo = [
            {:tag_name=>:text, :skip_escape=>true, :value=>css_final}
          ].concat(todo)

        when tag == :script # ============
          h = vals

          if h[:tag] == :script && h[:content] && !h[:content].empty?
            return <<-EOF
              <script type="text/css">
                WWW_App.run(
                  #{to_clean_text :to_json, h[:content]}
                );
              </script>
            EOF
          end

          html = h[:childs].map { |tag_index|
            to_clean_text(:html, @tag_arr[tag_index])
          }.join(NEW_LINE).strip

          return unless  h[:render?]

          if html.empty? && h[:text]
            html = if h[:text].is_a?(::Symbol)
                     h[:text].to_mustache(:html)
                   else
                     if h[:text].is_a?(::Hash)
                       if h[:text][:escape] == false
                         h[:text][:value]
                       else
                         Clean.html(h[:text][:value].strip)
                       end
                     else
                       Clean.html(h[:text].strip)
                     end
                   end
          end # === if html.empty?

          (html = nil) if html.empty?

          case
          when h[:tag] == :render_if
            key   = h[:attrs][:key]
            open  = "{{# coll.#{key} }}"
            close = "{{/ coll.#{key} }}"

          when h[:tag] == :render_unless
            key   = h[:attrs][:key]
            open  = "{{^ coll.#{key} }}"
            close = "{{/ coll.#{key} }}"

          when Methods[:elements].include?(h[:tag])
            open  = "<#{h[:tag]}#{to_clean_text(:attrs, h)}"
            if NO_END_TAGS.include?(h[:tag])
              open += ' />'
              close = nil
            else
              open += '>'
              close = "</#{h[:tag]}>"
            end

          else
            fail "Unknown html tag: #{h[:tag].inspect}"

          end # === case h[:tag]

          if h[:tag]
            [open, html, close].compact.join
          else
            html
          end # === if

        when t_name && ::WWW_App::HTML::TAGS.include?(t_name) # === HTML tags =====

          # ================================
          # === Save this for last to allow
          # certain tags
          # to be over-riddent,
          # like :script
          # ================================

          attrs = {}
          attrs.default KEY_REQUIRED

          new_todo = []
          t2a = ::WWW_App::HTML::TAGS_TO_ATTRIBUTES

          tag.each { |attr_name, v|
            if t2a[:all].include?(attr_name) || (t2a[tag[:tag_name]] && t2a[tag[:tag_name]].include?(attr_name))
              attrs[attr_name] = v
            end
          }

          if !attrs.empty?
            new_todo.concat [:clean_attrs, attrs, tag]
          end

          new_todo.concat [:open, tag[:tag_name]]

          if tag[:children] && !tag[:children].empty?
            new_todo.concat tag[:children]
            if tag[:children].detect { |t| HTML::TAGS.include?(t[:tag_name]) }
              new_todo << :new_line
            end
          end
          new_todo.concat [:close, tag[:tag_name]]
          todo = new_todo.concat(todo)

        else
          fail "Unknown: #{tag.inspect[0,30]}"
        end # === case
      end # === while

      final

      @mustache ||= begin
                      mustache = ::Mustache.new
                      mustache.template = Clean.clean_utf8(final)
                      mustache
                    end

      to_html(*args)
    end
to_raw_text() click to toggle source
# File lib/www_app/TO.rb, line 146
def to_raw_text
  instance_eval(&@blok) if @tags.empty?
  str    = ""
  indent = 0
  print_tag = lambda { |t|
    info          = t.select { |n| [:id, :class, :closed, :pseudo].include?( n ) }
    info[:parent] = t[:parent] && t[:parent][:tag_name]

    str += "#{" " * indent}#{t[:tag_name].inspect} -- #{info.inspect.gsub(/\A\{|\}\Z/, '')}\n"
    indent += 1
    if t[:children]
      t[:children].each { |c|
        str << print_tag.call(c)
      }
    end
    indent -= 1
  }

  @tags.each { |t| print_tag.call(t) }
  str
end