# Wrapper class that understands HTML
module Wunderbar
# factored out so that these methods can be overriden (e.g., by opal.rb)
class Overridable < BuilderBase
def _script(*args, &block)
args << {} unless Hash === args.last
args.last[:lang] ||= 'text/javascript'
proxiable_tag! 'script', ScriptNode, *args, &block
end
def _style(*args, &block)
if args == [:system]
args[0] = %{
pre._stdin {font-weight: bold; color: #800080; margin: 1em 0 0 0}
pre._stdout {font-weight: bold; color: #000; margin: 0}
pre._stderr {font-weight: bold; color: #F00; margin: 0}
}
end
args << {} unless Hash === args.last
args.last[:type] ||= 'text/css'
proxiable_tag! 'style', StyleNode, *args, &block
end
end
class HtmlMarkup < Overridable
VOID = %w(
area base br col command embed hr img input keygen
link meta param source track wbr
)
HTML5_BLOCK = %w(
# https://developer.mozilla.org/en/HTML/Block-level_elements
address article aside audio blockquote br canvas dd div dl fieldset
figcaption figcaption figure footer form h1 h2 h3 h4 h5 h6 header hgroup
hr noscript ol output p pre section table tfoot ul video
)
HEAD = %w(title base link style meta)
def initialize(scope)
@_scope = scope
@_x = XmlMarkup.new :scope => scope
end
def html(*args, &block)
# default namespace
args << {} if args.empty?
if Hash === args.first
args.first[:xmlns] ||= 'http://www.w3.org/1999/xhtml'
@_x._width = args.first.delete(:_width).to_i if args.first[:_width]
end
bom = "\ufeff"
title = args.shift if String === args.first
@_x.declare! :DOCTYPE, :html
html = tag! :html, *args do
set_variables_from_params
_title title if title
instance_eval(&block)
end
pending_head = []
pending_body = []
head = nil
body = nil
html.children.each do |child|
next unless child
if String === child
pending_body << child
elsif child.name == 'head'
head = child
elsif child.name == 'body'
body = child
elsif HEAD.include? child.name
pending_head << child
elsif child.name == 'script'
if pending_body.empty?
pending_head << child
else
pending_body << child
end
else
pending_body << child
end
end
@_x.instance_eval {@node = html}
head = _head_ if not head
body = _body nil if not body
html.children.unshift(head.parent.children.delete(head))
html.children.push(body.parent.children.delete(body))
head.parent = body.parent = html
head.children.compact!
body.children.compact!
[ [pending_head, head], [pending_body, body] ].each do |list, node|
list.each do |child|
html.children.delete(child)
node.add_child child
end
end
def find_name(name)
proc {|child| child.respond_to? :name and child.name == name}
end
if not head.children.any? &find_name('title')
h1 = body.children.find &find_name('h1')
head.add_child Node.new('title', h1.text) if h1 and h1.text
end
base = head.children.index &find_name('base')
if base
head.children.insert 1, head.children.delete_at(base) if base > 1
# compute relative path from base to the current working directory
require 'pathname'
base = @_scope.env['DOCUMENT_ROOT'] if @_scope.env.respond_to? :[]
base ||= Dir.pwd
base += (head.children[1].attrs[:href] || '')
base += 'index.html' if base.end_with? '/'
base = Pathname.new(base).parent
prefix = Pathname.new(Dir.pwd).relative_path_from(base).to_s + '/'
prefix = nil if prefix == './'
head.children.insert 2, *Asset.declarations(head, prefix)
else
head.children.insert 1, *Asset.declarations(head, nil)
end
title = head.children.index {|child| child.name == 'title'}
if title and title > 1
head.children.insert 1, head.children.delete_at(title)
end
bom + @_x.target!
end
def _html(*args, &block)
html(*args, &block)
end
def method_missing(name, *args, &block)
if name =~ /^_(\w+)(!|\?|)$/
name, flag = $1, $2
elsif @_scope and @_scope.respond_to? name
return @_scope.__send__ name, *args, &block
else
err = NameError.new "undefined local variable or method `#{name}'", name
err.set_backtrace caller
raise err
end
if name.sub!(/_$/,'')
@_x.spaced!
if flag != '!' and respond_to? "_#{name}"
return __send__ "_#{name}#{flag}", *args, &block
end
end
name = name.to_s.gsub('_', '-')
if flag != '!'
if String === args.first and args.first.respond_to? :html_safe?
if args.first.html_safe? and not block and args.first =~ /[>&]/
markup = args.shift
block = Proc.new {_ {markup}}
end
end
end
if flag == '!'
@_x.compact! { tag! name, *args, &block }
elsif flag == '?'
# capture exceptions, produce filtered tracebacks
tag!(name, *args) do
begin
block.call
rescue ::Exception => exception
options = (Hash === args.last)? args.last : {}
options[:log_level] = 'warn'
_exception exception, options
end
end
elsif Wunderbar.templates.include? name
x = self.class.new({})
instance_variables.each do |ivar|
x.instance_variable_set ivar, instance_variable_get(ivar)
end
if Hash === args.last
args.last.each do |name, value|
x.instance_variable_set "@#{name}", value
end
end
save_yield = Wunderbar.templates['yield']
begin
Wunderbar.templates['yield'] = block if block
x.instance_eval &Wunderbar.templates[name]
ensure
Wunderbar.templates['yield'] = save_yield
Wunderbar.templates.delete 'yield' unless save_yield
end
else
tag! name, *args, &block
end
end
def tag!(name, *args, &block)
node = @_x.tag! name, *args, &block
if !block and args.empty?
CssProxy.new(self, node)
else
node
end
end
def proxiable_tag!(name, *args, &block)
node = @_x.tag! name, *args, &block
if !block
CssProxy.new(self, node)
else
node
end
end
def _exception(*args)
exception = args.first
if exception.respond_to? :backtrace
options = (Hash === args.last)? args.last : {}
traceback_class = options.delete(:traceback_class)
traceback_style = options.delete(:traceback_style)
traceback_style ||= 'background-color:#ff0; margin: 1em 0; ' +
'padding: 1em; border: 4px solid red; border-radius: 1em'
text = exception.inspect
log_level = options.delete(:log_level) || :error
Wunderbar.send log_level, text
exception.backtrace.each do |frame|
next if Wunderbar::CALLERS_TO_IGNORE.any? {|re| frame =~ re}
Wunderbar.send log_level, " #{frame}"
text += "\n #{frame}"
end
if traceback_class
tag! :pre, text, :class=>traceback_class
else
tag! :pre, text, :style=>traceback_style
end
else
super
end
end
def _head(*args, &block)
tag!('head', *args) do
tag! :meta, :charset => 'utf-8'
block.call if block
end
end
def _p(*args, &block)
if args.length >= 1 and String === args.first and args.first.include? "\n"
text = args.shift
tag! :p, *args do
@_x.indented_text! text
end
else
super
end
end
def _svg(*args, &block)
args << {} if args.empty?
args.first['xmlns'] = 'http://www.w3.org/2000/svg' if Hash === args.first
proxiable_tag! :svg, *args, &block
end
def _math(*args, &block)
args << {} if args.empty?
if Hash === args.first
args.first['xmlns'] = 'http://www.w3.org/1998/Math/MathML'
end
proxiable_tag! :math, *args, &block
end
def _pre(*args, &block)
args.first.chomp! if String === args.first and args.first.end_with? "\n"
@_x.compact! do
proxiable_tag! :pre, PreformattedNode, *args, &block
end
end
def _textarea(*args, &block)
proxiable_tag! :textarea, PreformattedNode, *args, &block
end
def _ul(*args, &block)
iterable = args.first.respond_to? :each
if iterable and (args.length > 1 or not args.first.respond_to? :to_hash)
list = args.shift.dup
tag!(:ul, *args) {list.each {|arg| _li arg }}
else
super
end
end
def _ol(*args, &block)
iterable = args.first.respond_to? :each
if iterable and (args.length > 1 or not args.first.respond_to? :to_hash)
list = args.shift
tag!(:ol, *args) {list.each {|arg| _li arg }}
else
super
end
end
def _tr(*args, &block)
iterable = args.first.respond_to? :each
if iterable and (args.length > 1 or not args.first.respond_to? :to_hash)
list = args.shift
tag!(:tr, *args) {list.each {|arg| _td arg }}
else
super
end
end
def _!(text)
@_x.text! text.to_s.chomp
end
def _(text=nil, &block)
unless block
if text
if text.respond_to? :html_safe? and text.html_safe?
_ {text}
else
@_x.indented_text! text.to_s
end
end
return @_x
end
children = instance_eval &block
if String === children
safe = !children.tainted?
safe ||= children.html_safe? if children.respond_to? :html_safe?
safe &&= defined? Nokogiri
ok = safe || defined? Sanitize
safe = true
if ok and (children.include? '<' or children.include? '&')
if defined? Nokogiri::HTML5.fragment
doc = Nokogiri::HTML5.fragment(children.to_s)
else
doc = Nokogiri::HTML.fragment(children.to_s)
end
Sanitize.new.clean_node! doc.dup.untaint if not safe
children = doc.children.to_a
# ignore leading whitespace
while not children.empty? and children.first.text?
break unless children.first.text.strip.empty?
children.shift
end
# gather up candidate head elements
pending_head = []
while not children.empty? and children.first.element?
break unless (HEAD+['script']).include? children.first.name
pending_head << children.shift
end
# rebuild head element if any candidates were found
unless pending_head.empty?
head = Nokogiri::XML::Node.new('head', pending_head.first.document)
pending_head.each {|child| head << child}
children.unshift head
end
else
return @_x.indented_text! children
end
elsif children.nil? or Wunderbar::Node === children
return children
end
@_x[*children]
end
def __(text=nil, &block)
if text
@_x.spaced!
@_x.indented_text! text
elsif block
@_x.spaced!
_ &block
else
@_x.text! ""
end
end
def clear!
@_x.clear!
end
def self.flatten?(children)
# do any of the text nodes need special processing to preserve spacing?
flatten = false
space = true
if children.any? {|child| child.text? and !child.text.strip.empty?}
children.each do |child|
if child.text? or child.element?
unless child.text == ''
flatten = true if not space and not child.text =~ /\A\s/
space = (child.text =~ /\s\Z/)
end
space = true if child.element? and HTML5_BLOCK.include? child.name
end
end
end
flatten
end
end
end