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 << "#{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 ::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 # ===============