require 'mustache' require 'escape_escape_escape' # =================================================================== # === Symbol customizations: ======================================== # =================================================================== class Symbol def to_mustache *args WWW_App::Clean.mustache *args, self end end # === class Symbol # =================================================================== # =================================================================== # === Mustache customizations: ====================================== # =================================================================== Mustache.raise_on_context_miss = true class Mustache def render(data = template, ctx = {}) ctx = data tpl = templateify(template) begin context.push(ctx) tpl.render(context) ensure context.pop end end # === def render class Generator alias_method :w_syms_on_fetch, :on_fetch def on_fetch(names) if names.length == 2 "ctx[#{names.first.to_sym.inspect}, #{names.last.to_sym.inspect}]" else w_syms_on_fetch(names) end end end # === class Generator class Context def fetch *args raise ContextMiss.new("Can't find: #{args.inspect}") if args.size != 2 meth, key = args @stack.each { |frame| case when frame.is_a?(Hash) && meth == :coll && !frame.has_key?(key) return false when frame.is_a?(Hash) && meth == :coll && frame.has_key?(key) target = frame[key] if target == true || target == false || target == nil || target.is_a?(Array) || target.is_a?(Hash) return target end fail "Invalid value: #{key.inspect} (#{key.class})" when frame.is_a?(Hash) && frame.has_key?(key) return ::Escape_Escape_Escape.send(meth, frame[key]) end } raise ContextMiss.new("Can't find .#{meth}(#{key.inspect})") end # NOTE: :alias_method has to go after the re-definition of # :fetch or else it uses the original :fetch method/definition. alias_method :[], :fetch end # === class Context end # === class Mustache # =================================================================== class WWW_App class Clean MUSTACHE_Regex = /\A[a-z0-9\_\.]+\z/i PERIOD = '.'.freeze class << self def mustache *args case args.size when 2 meth, val = args escape_it = false when 3 escape_it, meth, val = args else fail ::ArgumentError, "Unknown args: #{args}" end v = meth.to_s + PERIOD + val.to_s fail "Unknown chars: #{args.inspect}" unless v[MUSTACHE_Regex] if escape_it "!{ #{v} }!" else "{{{ #{v} }}}" end end def method_missing name, *args ::Escape_Escape_Escape.send(name, *args) end end # === class << self end # === class Clean module TO COMMA = ", ".freeze SPACE = " ".freeze NOTHING = "".freeze GEM_PATH = File.dirname(__FILE__).sub('lib/www_app'.freeze, NOTHING) VERSION = File.read(GEM_PATH + '/VERSION').strip JS_FILE_PATHS = begin public = "#{GEM_PATH}/lib/public" all = Dir.glob("#{public}/**/*.{map,js}").map { |path| "/www_app-#{VERSION}/#{path.gsub("#{public}/", NOTHING)}" } special = all.select { |f| f[/(instruct.js|www_app.js)$/] } filtered = all.reject { |f| special.include?(f) } filtered + special end INVALID_SCRIPT_TYPE_CHARS = /[^a-z0-9\-\/\_]+/ KEY_REQUIRED = proc { |hash, k| fail "Key not set: #{k.inspect}" } def to_raw_text 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 def to_html *args return @mustache.render(*args) if instance_variable_defined?(:@mustache) 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 => ""}, 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 == :_ && !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 << "" 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 ::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] == "" 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 == :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)}^ ) 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 ] } 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(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 ] 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 # ===============