module Erector
module HTML
module ClassMethods
# Tags which are always self-closing. Click "[Source]" to see the full list.
def empty_tags
['area', 'base', 'br', 'col', 'embed', 'frame',
'hr', 'img', 'input', 'link', 'meta', 'param']
end
# Tags which can contain other stuff. Click "[Source]" to see the full list.
def full_tags
[
'a', 'abbr', 'acronym', 'address', 'article', 'aside', 'audio',
'b', 'bdo', 'big', 'blockquote', 'body', 'button',
'canvas', 'caption', 'center', 'cite', 'code', 'colgroup', 'command',
'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'div', 'dl', 'dt',
'em',
'fieldset', 'figure', 'footer', 'form', 'frameset',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'html', 'i',
'iframe', 'ins', 'keygen', 'kbd', 'label', 'legend', 'li',
'map', 'mark', 'meter',
'nav', 'noframes', 'noscript',
'object', 'ol', 'optgroup', 'option',
'p', 'pre', 'progress',
'q', 'ruby', 'rt', 'rp', 's',
'samp', 'script', 'section', 'select', 'small', 'source', 'span', 'strike',
'strong', 'style', 'sub', 'sup',
'table', 'tbody', 'td', 'textarea', 'tfoot',
'th', 'thead', 'time', 'title', 'tr', 'tt',
'u', 'ul',
'var', 'video'
]
end
def all_tags
full_tags + empty_tags
end
def def_empty_tag_method(tag_name)
self.class_eval(<<-SRC, __FILE__, __LINE__ + 1)
def #{tag_name}(*args, &block)
__empty_element__('#{tag_name}', *args, &block)
end
SRC
end
def def_full_tag_method(tag_name)
self.class_eval(<<-SRC, __FILE__, __LINE__ + 1)
def #{tag_name}(*args, &block)
__element__(false, '#{tag_name}', *args, &block)
end
def #{tag_name}!(*args, &block)
__element__(true, '#{tag_name}', *args, &block)
end
SRC
end
end
def self.included(base)
base.extend ClassMethods
base.full_tags.each do |tag_name|
base.def_full_tag_method(tag_name)
end
base.empty_tags.each do |tag_name|
base.def_empty_tag_method(tag_name)
end
end
# Internal method used to emit an HTML/XML element, including an open tag,
# attributes (optional, via the default hash), contents (also optional),
# and close tag.
#
# Using the arcane powers of Ruby, there are magic methods that call
# +element+ for all the standard HTML tags, like +a+, +body+, +p+, and so
# forth. Look at the source of #full_tags for the full list.
# Unfortunately, this big mojo confuses rdoc, so we can't see each method
# in this rdoc page, but trust us, they're there.
#
# When calling one of these magic methods, put attributes in the default
# hash. If there is a string parameter, then it is used as the contents.
# If there is a block, then it is executed (yielded), and the string
# parameter is ignored. The block will usually be in the scope of the
# child widget, which means it has access to all the methods of Widget,
# which will eventually end up appending text to the +output+ string. See
# how elegant it is? Not confusing at all if you don't think about it.
#
def element(*args, &block)
__element__(false, *args, &block)
end
# Like +element+, but string parameters are not escaped.
def element!(*args, &block)
__element__(true, *args, &block)
end
# Internal method used to emit a self-closing HTML/XML element, including
# a tag name and optional attributes (passed in via the default hash).
#
# Using the arcane powers of Ruby, there are magic methods that call
# +empty_element+ for all the standard HTML tags, like +img+, +br+, and so
# forth. Look at the source of #empty_tags for the full list.
# Unfortunately, this big mojo confuses rdoc, so we can't see each method
# in this rdoc page, but trust us, they're there.
#
def empty_element(*args, &block)
__empty_element__(*args, &block)
end
# Returns an HTML-escaped version of its parameter. Leaves the output
# string untouched. This method is idempotent: h(h(text)) will not
# double-escape text. This means that it is safe to do something like
# text(h("2<4")) -- it will produce "2<4", not "2<4".
def h(content)
if content.respond_to?(:html_safe?) && content.html_safe?
content
else
raw(CGI.escapeHTML(content.to_s))
end
end
# Emits an open tag, comprising '<', tag name, optional attributes, and '>'
def open_tag(tag_name, attributes={})
output.newline if newliney?(tag_name) && !output.at_line_start?
output << raw("<#{tag_name}#{format_attributes(attributes)}>")
output.indent
end
# Emits a close tag, consisting of '<', '/', tag name, and '>'
def close_tag(tag_name)
output.undent
output << raw("#{tag_name}>")
if newliney?(tag_name)
output.newline
end
end
# Returns text which will *not* be HTML-escaped.
def raw(value)
RawString.new(value.to_s)
end
# Emits text. If a string is passed in, it will be HTML-escaped. If the
# result of calling methods such as raw is passed in, the HTML will not be
# HTML-escaped again. If another kind of object is passed in, the result
# of calling its to_s method will be treated as a string would be.
#
# You shouldn't pass a widget in to this method, as that will cause
# performance problems (as well as being semantically goofy). Use the
# #widget method instead.
def text(value)
if value.is_a? Widget
widget value
else
output << h(value)
end
nil
end
# Emits text which will *not* be HTML-escaped. Same effect as text(raw(s))
def text!(value)
text raw(value)
end
alias rawtext text!
# Returns a copy of value with spaces replaced by non-breaking space characters.
# With no arguments, return a single non-breaking space.
# The output uses the escaping format ' ' since that works
# in both HTML and XML (as opposed to ' ' which only works in HTML).
def nbsp(value = " ")
raw(h(value).gsub(/ /,' '))
end
# Return a character given its unicode code point or unicode name.
def character(code_point_or_name)
if code_point_or_name.is_a?(Symbol)
found = Erector::CHARACTERS[code_point_or_name]
if found.nil?
raise "Unrecognized character #{code_point_or_name}"
end
raw("#{sprintf '%x', found};")
elsif code_point_or_name.is_a?(Integer)
raw("#{sprintf '%x', code_point_or_name};")
else
raise "Unrecognized argument to character: #{code_point_or_name}"
end
end
# Emits an XML instruction, which looks like this:
def instruct(attributes={:version => "1.0", :encoding => "UTF-8"})
output << raw("")
end
# Emits an HTML comment (<!-- ... -->) surrounding +text+ and/or the output of +block+.
# see http://www.w3.org/TR/html4/intro/sgmltut.html#h-3.2.4
#
# If +text+ is an Internet Explorer conditional comment condition such as "[if IE]",
# the output includes the opening condition and closing "[endif]". See
# http://www.quirksmode.org/css/condcom.html
#
# Since "Authors should avoid putting two or more adjacent hyphens inside comments,"
# we emit a warning if you do that.
def comment(text = '', &block)
puts "Warning: Authors should avoid putting two or more adjacent hyphens inside comments." if text =~ /--/
conditional = text =~ /\[if .*\]/
rawtext "\n"
end
# Emits a javascript block inside a +script+ tag, wrapped in CDATA
# doohickeys like all the cool JS kids do.
def javascript(*args, &block)
if args.length > 2
raise ArgumentError, "Cannot accept more than two arguments"
end
attributes, value = nil, nil
arg0 = args[0]
if arg0.is_a?(Hash)
attributes = arg0
else
value = arg0
arg1 = args[1]
if arg1.is_a?(Hash)
attributes = arg1
end
end
attributes ||= {}
attributes[:type] = "text/javascript"
open_tag 'script', attributes
# Shouldn't this be a "cdata" HtmlPart?
# (maybe, but the syntax is specific to javascript; it isn't
# really a generic XML CDATA section. Specifically,
# ]]> within value is not treated as ending the
# CDATA section by Firefox2 when parsing text/html,
# although I guess we could refuse to generate ]]>
# there, for the benefit of XML/XHTML parsers).
rawtext "\n// "
output.append_newline # this forces a newline even if we're not in pretty mode
close_tag 'script'
rawtext "\n"
end
protected
def __element__(raw, tag_name, *args, &block)
if args.length > 2
raise ArgumentError, "Cannot accept more than four arguments"
end
attributes, value = nil, nil
arg0 = args[0]
if arg0.is_a?(Hash)
attributes = arg0
else
value = arg0
arg1 = args[1]
if arg1.is_a?(Hash)
attributes = arg1
end
end
attributes ||= {}
open_tag tag_name, attributes
if block && value
raise ArgumentError, "You can't pass both a block and a value to #{tag_name} -- please choose one."
end
if block
block.call
elsif raw
text! value
else
text value
end
close_tag tag_name
end
def __empty_element__(tag_name, attributes={})
output << raw("<#{tag_name}#{format_attributes(attributes)} />")
output.newline if newliney?(tag_name)
end
def format_attributes(attributes)
if !attributes || attributes.empty?
""
else
format_sorted(sorted(attributes))
end
end
def format_sorted(sorted)
results = ['']
sorted.each do |key, value|
if value
if value.is_a?(Array)
value = value.flatten
next if value.empty?
value = value.join(' ')
end
results << "#{key}=\"#{h(value)}\""
end
end
return results.join(' ')
end
def sorted(attributes)
stringized = []
attributes.each do |key, value|
stringized << [key.to_s, value]
end
return stringized.sort
end
def sort_for_xml_declaration(attributes)
# correct order is "version, encoding, standalone" (XML 1.0 section 2.8).
# But we only try to put version before encoding for now.
stringized = []
attributes.each do |key, value|
stringized << [key.to_s, value]
end
return stringized.sort{|a, b| b <=> a}
end
NON_NEWLINEY = {'i' => true, 'b' => true, 'small' => true,
'img' => true, 'span' => true, 'a' => true,
'input' => true, 'textarea' => true, 'button' => true, 'select' => true
}
def newliney?(tag_name)
!NON_NEWLINEY.include?(tag_name)
end
end
end