2007-07-05 00:36:59 +02:00
|
|
|
require 'html5/constants'
|
2007-06-22 10:12:08 +02:00
|
|
|
|
2007-07-05 00:36:59 +02:00
|
|
|
module HTML5
|
2007-06-22 10:12:08 +02:00
|
|
|
|
|
|
|
class HTMLSerializer
|
|
|
|
|
|
|
|
def self.serialize(stream, options = {})
|
|
|
|
new(options).serialize(stream, options[:encoding])
|
|
|
|
end
|
|
|
|
|
|
|
|
def escape(string)
|
|
|
|
string.gsub("&", "&").gsub("<", "<").gsub(">", ">")
|
|
|
|
end
|
|
|
|
|
|
|
|
def initialize(options={})
|
2007-08-30 19:19:10 +02:00
|
|
|
@quote_attr_values = false
|
|
|
|
@quote_char = '"'
|
|
|
|
@use_best_quote_char = true
|
2007-06-22 10:12:08 +02:00
|
|
|
@minimize_boolean_attributes = true
|
|
|
|
|
2007-08-30 19:19:10 +02:00
|
|
|
@use_trailing_solidus = false
|
2007-06-22 10:12:08 +02:00
|
|
|
@space_before_trailing_solidus = true
|
2007-08-30 19:19:10 +02:00
|
|
|
@escape_lt_in_attrs = false
|
|
|
|
@escape_rcdata = false
|
2007-06-22 10:12:08 +02:00
|
|
|
|
|
|
|
@omit_optional_tags = true
|
2007-08-30 19:19:10 +02:00
|
|
|
@sanitize = false
|
2007-06-22 10:12:08 +02:00
|
|
|
|
|
|
|
@strip_whitespace = false
|
|
|
|
|
|
|
|
@inject_meta_charset = true
|
|
|
|
|
|
|
|
options.each do |name, value|
|
|
|
|
next unless instance_variables.include?("@#{name}")
|
|
|
|
@use_best_quote_char = false if name.to_s == 'quote_char'
|
|
|
|
instance_variable_set("@#{name}", value)
|
|
|
|
end
|
|
|
|
|
|
|
|
@errors = []
|
|
|
|
end
|
|
|
|
|
|
|
|
def serialize(treewalker, encoding=nil)
|
|
|
|
in_cdata = false
|
|
|
|
@errors = []
|
|
|
|
|
|
|
|
if encoding and @inject_meta_charset
|
2007-07-05 00:36:59 +02:00
|
|
|
require 'html5/filters/inject_meta_charset'
|
2007-06-22 10:12:08 +02:00
|
|
|
treewalker = Filters::InjectMetaCharset.new(treewalker, encoding)
|
|
|
|
end
|
|
|
|
|
|
|
|
if @strip_whitespace
|
2007-07-05 00:36:59 +02:00
|
|
|
require 'html5/filters/whitespace'
|
2007-06-22 10:12:08 +02:00
|
|
|
treewalker = Filters::WhitespaceFilter.new(treewalker)
|
|
|
|
end
|
|
|
|
|
|
|
|
if @sanitize
|
2007-07-05 00:36:59 +02:00
|
|
|
require 'html5/filters/sanitizer'
|
2007-06-22 10:12:08 +02:00
|
|
|
treewalker = Filters::HTMLSanitizeFilter.new(treewalker)
|
|
|
|
end
|
|
|
|
|
|
|
|
if @omit_optional_tags
|
2007-07-05 00:36:59 +02:00
|
|
|
require 'html5/filters/optionaltags'
|
2007-06-22 10:12:08 +02:00
|
|
|
treewalker = Filters::OptionalTagFilter.new(treewalker)
|
|
|
|
end
|
|
|
|
|
|
|
|
result = []
|
|
|
|
treewalker.each do |token|
|
|
|
|
type = token[:type]
|
|
|
|
if type == :Doctype
|
|
|
|
doctype = "<!DOCTYPE %s>" % token[:name]
|
|
|
|
result << doctype
|
|
|
|
|
|
|
|
elsif [:Characters, :SpaceCharacters].include? type
|
|
|
|
if type == :SpaceCharacters or in_cdata
|
|
|
|
if in_cdata and token[:data].include?("</")
|
2007-10-06 18:55:58 +02:00
|
|
|
serialize_error("Unexpected </ in CDATA")
|
2007-06-22 10:12:08 +02:00
|
|
|
end
|
|
|
|
result << token[:data]
|
|
|
|
else
|
|
|
|
result << escape(token[:data])
|
|
|
|
end
|
|
|
|
|
|
|
|
elsif [:StartTag, :EmptyTag].include? type
|
|
|
|
name = token[:name]
|
2007-07-05 00:36:59 +02:00
|
|
|
if RCDATA_ELEMENTS.include?(name) and not @escape_rcdata
|
2007-06-22 10:12:08 +02:00
|
|
|
in_cdata = true
|
|
|
|
elsif in_cdata
|
2007-08-30 19:19:10 +02:00
|
|
|
serialize_error(_("Unexpected child element of a CDATA element"))
|
2007-06-22 10:12:08 +02:00
|
|
|
end
|
|
|
|
attributes = []
|
|
|
|
for k,v in attrs = token[:data].to_a.sort
|
|
|
|
attributes << ' '
|
|
|
|
|
|
|
|
attributes << k
|
|
|
|
if not @minimize_boolean_attributes or \
|
|
|
|
(!(BOOLEAN_ATTRIBUTES[name]||[]).include?(k) \
|
|
|
|
and !BOOLEAN_ATTRIBUTES[:global].include?(k))
|
|
|
|
attributes << "="
|
|
|
|
if @quote_attr_values or v.empty?
|
|
|
|
quote_attr = true
|
|
|
|
else
|
|
|
|
quote_attr = (SPACE_CHARACTERS + %w(< > " ')).any? {|c| v.include?(c)}
|
|
|
|
end
|
|
|
|
v = v.gsub("&", "&")
|
|
|
|
v = v.gsub("<", "<") if @escape_lt_in_attrs
|
|
|
|
if quote_attr
|
|
|
|
quote_char = @quote_char
|
|
|
|
if @use_best_quote_char
|
|
|
|
if v.index("'") and !v.index('"')
|
|
|
|
quote_char = '"'
|
|
|
|
elsif v.index('"') and !v.index("'")
|
|
|
|
quote_char = "'"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
if quote_char == "'"
|
|
|
|
v = v.gsub("'", "'")
|
|
|
|
else
|
|
|
|
v = v.gsub('"', """)
|
|
|
|
end
|
|
|
|
attributes << quote_char << v << quote_char
|
|
|
|
else
|
|
|
|
attributes << v
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
if VOID_ELEMENTS.include?(name) and @use_trailing_solidus
|
|
|
|
if @space_before_trailing_solidus
|
|
|
|
attributes << " /"
|
|
|
|
else
|
|
|
|
attributes << "/"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
result << "<%s%s>" % [name, attributes.join('')]
|
|
|
|
|
|
|
|
elsif type == :EndTag
|
|
|
|
name = token[:name]
|
|
|
|
if RCDATA_ELEMENTS.include?(name)
|
|
|
|
in_cdata = false
|
|
|
|
elsif in_cdata
|
2007-08-30 19:19:10 +02:00
|
|
|
serialize_error(_("Unexpected child element of a CDATA element"))
|
2007-06-22 10:12:08 +02:00
|
|
|
end
|
|
|
|
end_tag = "</#{name}>"
|
|
|
|
result << end_tag
|
|
|
|
|
|
|
|
elsif type == :Comment
|
|
|
|
data = token[:data]
|
2007-10-28 00:34:29 +02:00
|
|
|
serialize_error("Comment contains --") if data.index("--")
|
2007-06-22 10:12:08 +02:00
|
|
|
comment = "<!--%s-->" % token[:data]
|
|
|
|
result << comment
|
|
|
|
|
|
|
|
else
|
2007-08-30 19:19:10 +02:00
|
|
|
serialize_error(token[:data])
|
2007-06-22 10:12:08 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if encoding and encoding != 'utf-8'
|
|
|
|
require 'iconv'
|
|
|
|
Iconv.iconv(encoding, 'utf-8', result.join('')).first
|
|
|
|
else
|
|
|
|
result.join('')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
alias :render :serialize
|
|
|
|
|
2007-08-30 19:19:10 +02:00
|
|
|
def serialize_error(data="XXX ERROR MESSAGE NEEDED")
|
2007-06-22 10:12:08 +02:00
|
|
|
# XXX The idea is to make data mandatory.
|
|
|
|
@errors.push(data)
|
|
|
|
if @strict
|
|
|
|
raise SerializeError
|
|
|
|
end
|
|
|
|
end
|
2007-08-30 19:19:10 +02:00
|
|
|
|
2007-06-22 10:12:08 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
# Error in serialized tree
|
|
|
|
class SerializeError < Exception
|
|
|
|
end
|
|
|
|
end
|