vendor/plugins/haml/lib/haml/precompiler.rb in radiant-0.9.1 vs vendor/plugins/haml/lib/haml/precompiler.rb in radiant-1.0.0.rc1
- old
+ new
@@ -6,64 +6,50 @@
# which then runs the final creation of the HTML string.
module Precompiler
include Haml::Util
# Designates an XHTML/XML element.
- # @private
ELEMENT = ?%
# Designates a `<div>` element with the given class.
- # @private
DIV_CLASS = ?.
# Designates a `<div>` element with the given id.
- # @private
DIV_ID = ?#
# Designates an XHTML/XML comment.
- # @private
COMMENT = ?/
# Designates an XHTML doctype or script that is never HTML-escaped.
- # @private
DOCTYPE = ?!
# Designates script, the result of which is output.
- # @private
SCRIPT = ?=
# Designates script that is always HTML-escaped.
- # @private
SANITIZE = ?&
# Designates script, the result of which is flattened and output.
- # @private
FLAT_SCRIPT = ?~
# Designates script which is run but not output.
- # @private
SILENT_SCRIPT = ?-
# When following SILENT_SCRIPT, designates a comment that is not output.
- # @private
SILENT_COMMENT = ?#
# Designates a non-parsed line.
- # @private
ESCAPE = ?\\
# Designates a block of filtered text.
- # @private
FILTER = ?:
# Designates a non-parsed line. Not actually a character.
- # @private
PLAIN_TEXT = -1
# Keeps track of the ASCII values of the characters that begin a
# specially-interpreted line.
- # @private
SPECIAL_CHARACTERS = [
ELEMENT,
DIV_CLASS,
DIV_ID,
COMMENT,
@@ -76,11 +62,10 @@
FILTER
]
# The value of the character that designates that a line is part
# of a multiline string.
- # @private
MULTILINE_CHAR_VALUE = ?|
# Regex to match keywords that appear in the middle of a Ruby block
# with lowered indentation.
# If a block has been started using indentation,
@@ -92,19 +77,16 @@
# - else
# %p no!
#
# The block is ended after `%p no!`, because `else`
# is a member of this array.
- # @private
MID_BLOCK_KEYWORD_REGEX = /^-\s*(#{%w[else elsif rescue ensure when end].join('|')})\b/
# The Regex that matches a Doctype command.
- # @private
DOCTYPE_REGEX = /(\d(?:\.\d)?)?[\s]*([a-z]*)/i
# The Regex that matches a literal string or symbol value
- # @private
LITERAL_VALUE_REGEX = /:(\w*)|(["'])((?![\\#]|\2).|\\.)*\2/
private
# Returns the precompiled string with the preamble and postamble
@@ -134,12 +116,12 @@
def locals_code(names)
names = names.keys if Hash == names
names.map do |name|
# Can't use || because someone might explicitly pass in false with a symbol
- sym_local = "_haml_locals[#{name.to_sym.inspect}]"
- str_local = "_haml_locals[#{name.to_s.inspect}]"
+ sym_local = "_haml_locals[#{inspect_obj(name.to_sym)}]"
+ str_local = "_haml_locals[#{inspect_obj(name.to_s)}]"
"#{name} = #{sym_local}.nil? ? #{str_local} : #{sym_local}"
end.join(';') + ';'
end
# @private
@@ -251,40 +233,40 @@
- else
Not foo.
%p This line is un-indented, so it isn't part of the "if" block
END
+ text = handle_ruby_multiline(text)
push_silent(text[1..-1], true)
newline_now
# Handle stuff like - end.join("|")
@to_close_stack.last << false if text =~ /^-\s*end\b/ && !block_opened?
- case_stmt = text =~ /^-\s*case\b/
keyword = mid_block_keyword?(text)
block = block_opened? && !keyword
# It's important to preserve tabulation modification for keywords
# that involve choosing between posible blocks of code.
if %w[else elsif when].include?(keyword)
- # @to_close_stack may not have a :script on top
- # when the preceding "- if" has nothing nested
- if @to_close_stack.last && @to_close_stack.last.first == :script
+ # Whether a script block has already been opened immediately above this line
+ was_opened = @to_close_stack.last && @to_close_stack.last.first == :script
+ if was_opened
@dont_indent_next_line, @dont_tab_up_next_text = @to_close_stack.last[1..2]
- else
- push_and_tabulate([:script, @dont_indent_next_line, @dont_tab_up_next_text])
end
# when is unusual in that either it will be indented twice,
- # or the case won't have created its own indentation
- if keyword == "when"
- push_and_tabulate([:script, @dont_indent_next_line, @dont_tab_up_next_text, false])
+ # or the case won't have created its own indentation.
+ # Also, if no block has been opened yet, we need to make sure we add an end
+ # once we de-indent.
+ if !was_opened || keyword == "when"
+ push_and_tabulate([
+ :script, @dont_indent_next_line, @dont_tab_up_next_text,
+ !was_opened])
end
- elsif block || case_stmt
+ elsif block || text =~ /^-\s*(case|if)\b/
push_and_tabulate([:script, @dont_indent_next_line, @dont_tab_up_next_text])
- elsif block && case_stmt
- push_and_tabulate([:script, @dont_indent_next_line, @dont_tab_up_next_text])
end
when FILTER; start_filtered(text[1..-1].downcase)
when DOCTYPE
return render_doctype(text) if text[0...3] == '!!!'
return push_plain(text[3..-1].strip, :escape_html => false) if text[1..2] == "=="
@@ -330,30 +312,41 @@
end
def flush_merged_text
return if @to_merge.empty?
- text, tab_change = @to_merge.inject(["", 0]) do |(str, mtabs), (type, val, tabs)|
+ str = ""
+ mtabs = 0
+ newlines = 0
+ @to_merge.each do |type, val, tabs|
case type
when :text
- [str << val.inspect[1...-1], mtabs + tabs]
+ str << inspect_obj(val)[1...-1]
+ mtabs += tabs
when :script
if mtabs != 0 && !@options[:ugly]
val = "_hamlout.adjust_tabs(#{mtabs}); " + val
end
- [str << "\#{#{val}}", 0]
+ str << "\#{#{"\n" * newlines}#{val}}"
+ mtabs = 0
+ newlines = 0
+ when :newlines
+ newlines += val
else
raise SyntaxError.new("[HAML BUG] Undefined entry in Haml::Precompiler@to_merge.")
end
end
- @precompiled <<
- if @options[:ugly]
- "_hamlout.buffer << \"#{text}\";"
- else
- "_hamlout.push_text(\"#{text}\", #{tab_change}, #{@dont_tab_up_next_text.inspect});"
- end
+ unless str.empty?
+ @precompiled <<
+ if @options[:ugly]
+ "_hamlout.buffer << \"#{str}\";"
+ else
+ "_hamlout.push_text(\"#{str}\", #{mtabs}, #{@dont_tab_up_next_text.inspect});"
+ end
+ end
+ @precompiled << "\n" * newlines
@to_merge = []
@dont_tab_up_next_text = false
end
# Renders a block of text as plain text.
@@ -385,37 +378,38 @@
#
# If `opts[:preserve_script]` is true, Haml::Helpers#find_and_flatten is run on
# the result before it is added to `@buffer`
def push_script(text, opts = {})
raise SyntaxError.new("There's no Ruby code for = to evaluate.") if text.empty?
+ text = handle_ruby_multiline(text)
return if options[:suppress_eval]
opts[:escape_html] = options[:escape_html] if opts[:escape_html].nil?
args = %w[preserve_script in_tag preserve_tag escape_html nuke_inner_whitespace]
args.map! {|name| opts[name.to_sym]}
args << !block_opened? << @options[:ugly]
no_format = @options[:ugly] &&
!(opts[:preserve_script] || opts[:preserve_tag] || opts[:escape_html])
- output_temp = "(haml_very_temp = haml_temp; haml_temp = nil; haml_very_temp)"
- out = "_hamlout.#{static_method_name(:format_script, *args)}(#{output_temp});"
+ output_expr = "(#{text}\n)"
+ static_method = "_hamlout.#{static_method_name(:format_script, *args)}"
# Prerender tabulation unless we're in a tag
push_merged_text '' unless opts[:in_tag]
unless block_opened?
- @to_merge << [:script, no_format ? "#{text}\n" : "haml_temp = #{text}\n#{out}"]
+ @to_merge << [:script, no_format ? "#{text}\n" : "#{static_method}(#{output_expr});"]
concat_merged_text("\n") unless opts[:in_tag] || opts[:nuke_inner_whitespace]
@newlines -= 1
return
end
flush_merged_text
push_silent "haml_temp = #{text}"
newline_now
- push_and_tabulate([:loud, "_hamlout.buffer << #{no_format ? "#{output_temp}.to_s;" : out}",
+ push_and_tabulate([:loud, "_hamlout.buffer << #{no_format ? "haml_temp.to_s;" : "#{static_method}(haml_temp);"}",
!(opts[:in_tag] || opts[:nuke_inner_whitespace] || @options[:ugly])])
end
# Causes `text` to be evaluated, and Haml::Helpers#find_and_flatten
# to be run on it afterwards.
@@ -489,16 +483,18 @@
def close_nil(*args)
@template_tabs -= 1
end
+ # This is a class method so it can be accessed from {Haml::Helpers}.
+ #
# Iterates through the classes and ids supplied through `.`
# and `#` syntax, and returns a hash with them as attributes,
# that can then be merged with another attributes hash.
- def parse_class_and_id(list)
+ def self.parse_class_and_id(list)
attributes = {}
- list.scan(/([#.])([-_a-zA-Z0-9]+)/) do |type, property|
+ list.scan(/([#.])([-:_a-zA-Z0-9]+)/) do |type, property|
case type
when '.'
if attributes['class']
attributes['class'] += " "
else
@@ -529,13 +525,22 @@
# This is a class method so it can be accessed from Buffer.
def self.build_attributes(is_html, attr_wrapper, attributes = {})
quote_escape = attr_wrapper == '"' ? """ : "'"
other_quote_char = attr_wrapper == '"' ? "'" : '"'
+ if attributes['data'].is_a?(Hash)
+ attributes = attributes.dup
+ attributes =
+ Haml::Util.map_keys(attributes.delete('data')) {|name| "data-#{name}"}.merge(attributes)
+ end
+
result = attributes.collect do |attr, value|
next if value.nil?
+ value = filter_and_join(value, ' ') if attr == 'class'
+ value = filter_and_join(value, '_') if attr == 'id'
+
if value == true
next " #{attr}" if is_html
next " #{attr}=#{attr_wrapper}#{attr}#{attr_wrapper}"
elsif value == false
next
@@ -555,31 +560,38 @@
" #{attr}=#{this_attr_wrapper}#{value}#{this_attr_wrapper}"
end
result.compact.sort.join
end
+ def self.filter_and_join(value, separator)
+ return "" if value == ""
+ value = [value] unless value.is_a?(Array)
+ value = value.flatten.collect {|item| item ? item.to_s : nil}.compact.join(separator)
+ return !value.empty? && value
+ end
+
def prerender_tag(name, self_close, attributes)
attributes_string = Precompiler.build_attributes(html?, @options[:attr_wrapper], attributes)
"<#{name}#{attributes_string}#{self_close && xhtml? ? ' /' : ''}>"
end
# Parses a line into tag_name, attributes, attributes_hash, object_ref, action, value
def parse_tag(line)
- raise SyntaxError.new("Invalid tag: \"#{line}\".") unless match = line.scan(/%([-:\w]+)([-\w\.\#]*)(.*)/)[0]
+ raise SyntaxError.new("Invalid tag: \"#{line}\".") unless match = line.scan(/%([-:\w]+)([-:\w\.\#]*)(.*)/)[0]
tag_name, attributes, rest = match
new_attributes_hash = old_attributes_hash = last_line = object_ref = nil
- attributes_hashes = []
+ attributes_hashes = {}
while rest
case rest[0]
when ?{
break if old_attributes_hash
old_attributes_hash, rest, last_line = parse_old_attributes(rest)
- attributes_hashes << [:old, old_attributes_hash]
+ attributes_hashes[:old] = old_attributes_hash
when ?(
break if new_attributes_hash
new_attributes_hash, rest, last_line = parse_new_attributes(rest)
- attributes_hashes << [:new, new_attributes_hash]
+ attributes_hashes[:new] = new_attributes_hash
when ?[
break if object_ref
object_ref, rest = balance(rest, ?[, ?])
else; break
end
@@ -648,11 +660,11 @@
dynamic_attributes = "{"
attributes.each do |name, (type, val)|
if type == :static
static_attributes[name] = val
else
- dynamic_attributes << name.inspect << " => " << val << ","
+ dynamic_attributes << inspect_obj(name) << " => " << val << ","
end
end
dynamic_attributes << "}"
dynamic_attributes = nil if dynamic_attributes == "{}"
@@ -683,11 +695,11 @@
content << [:ruby, balance(scanner, ?{, ?}, 1).first[0...-1]]
end
return name, [:static, content.first[1]] if content.size == 1
return name, [:dynamic,
- '"' + content.map {|(t, v)| t == :str ? v.inspect[1...-1] : "\#{#{v}}"}.join + '"']
+ '"' + content.map {|(t, v)| t == :str ? inspect_obj(v)[1...-1] : "\#{#{v}}"}.join + '"']
end
# Parses a line that will render as an XHTML tag, and adds the code that will
# render that tag to `@precompiled`.
def render_tag(line)
@@ -742,36 +754,44 @@
value = ''
end
object_ref = "nil" if object_ref.nil? || @options[:suppress_eval]
- attributes = parse_class_and_id(attributes)
- attributes_hashes.map! do |syntax, attributes_hash|
- if syntax == :old
- static_attributes = parse_static_hash(attributes_hash)
- attributes_hash = nil if static_attributes || @options[:suppress_eval]
- else
- static_attributes, attributes_hash = attributes_hash
- end
+ attributes = Precompiler.parse_class_and_id(attributes)
+ attributes_list = []
+
+ if attributes_hashes[:new]
+ static_attributes, attributes_hash = attributes_hashes[:new]
Buffer.merge_attrs(attributes, static_attributes) if static_attributes
- attributes_hash
- end.compact!
+ attributes_list << attributes_hash
+ end
+ if attributes_hashes[:old]
+ static_attributes = parse_static_hash(attributes_hashes[:old])
+ Buffer.merge_attrs(attributes, static_attributes) if static_attributes
+ attributes_list << attributes_hashes[:old] unless static_attributes || @options[:suppress_eval]
+ end
+
+ attributes_list.compact!
+
raise SyntaxError.new("Illegal nesting: nesting within a self-closing tag is illegal.", @next_line.index) if block_opened? && self_closing
- raise SyntaxError.new("Illegal nesting: content can't be both given on the same line as %#{tag_name} and nested within it.", @next_line.index) if block_opened? && !value.empty?
raise SyntaxError.new("There's no Ruby code for #{action} to evaluate.", last_line - 1) if parse && value.empty?
raise SyntaxError.new("Self-closing tags can't have content.", last_line - 1) if self_closing && !value.empty?
- self_closing ||= !!( !block_opened? && value.empty? && @options[:autoclose].include?(tag_name) )
+ if block_opened? && !value.empty? && !is_ruby_multiline?(value)
+ raise SyntaxError.new("Illegal nesting: content can't be both given on the same line as %#{tag_name} and nested within it.", @next_line.index)
+ end
+
+ self_closing ||= !!(!block_opened? && value.empty? && @options[:autoclose].any? {|t| t === tag_name})
value = nil if value.empty? && (block_opened? || self_closing)
dont_indent_next_line =
(nuke_outer_whitespace && !block_opened?) ||
(nuke_inner_whitespace && block_opened?)
# Check if we can render the tag directly to text and not process it in the buffer
- if object_ref == "nil" && attributes_hashes.empty? && !preserve_script
+ if object_ref == "nil" && attributes_list.empty? && !preserve_script
tag_closed = !block_opened? && !self_closing && !parse
open_tag = prerender_tag(tag_name, self_closing, attributes)
if tag_closed
open_tag << "#{value}</#{tag_name}>"
@@ -785,23 +805,23 @@
@dont_indent_next_line = dont_indent_next_line
return if tag_closed
else
flush_merged_text
- content = parse ? 'nil' : value.inspect
- if attributes_hashes.empty?
- attributes_hashes = ''
- elsif attributes_hashes.size == 1
- attributes_hashes = ", #{attributes_hashes.first}"
+ content = parse ? 'nil' : inspect_obj(value)
+ if attributes_list.empty?
+ attributes_list = ''
+ elsif attributes_list.size == 1
+ attributes_list = ", #{attributes_list.first}"
else
- attributes_hashes = ", (#{attributes_hashes.join(").merge(")})"
+ attributes_list = ", (#{attributes_list.join(").merge(")})"
end
args = [tag_name, self_closing, !block_opened?, preserve_tag, escape_html,
attributes, nuke_outer_whitespace, nuke_inner_whitespace
- ].map { |v| v.inspect }.join(', ')
- push_silent "_hamlout.open_tag(#{args}, #{object_ref}, #{content}#{attributes_hashes})"
+ ].map {|v| inspect_obj(v)}.join(', ')
+ push_silent "_hamlout.open_tag(#{args}, #{object_ref}, #{content}#{attributes_list})"
@dont_tab_up_next_text = @dont_indent_next_line = dont_indent_next_line
end
return if self_closing
@@ -879,10 +899,11 @@
else
case type
when "strict"; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
when "frameset"; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">'
when "mobile"; '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">'
+ when "rdfa"; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.0//EN" "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd">'
when "basic"; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">'
else '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
end
end
@@ -957,43 +978,62 @@
@template.unshift line
@template_index -= 1
end
def handle_multiline(line)
- if is_multiline?(line.text)
- line.text.slice!(-1)
- while new_line = raw_next_line.first
- break if new_line == :eod
- newline and next if new_line.strip.empty?
- break unless is_multiline?(new_line.strip)
- line.text << new_line.strip[0...-1]
- newline
- end
- un_next_line new_line
- resolve_newlines
+ return unless is_multiline?(line.text)
+ line.text.slice!(-1)
+ while new_line = raw_next_line.first
+ break if new_line == :eod
+ newline and next if new_line.strip.empty?
+ break unless is_multiline?(new_line.strip)
+ line.text << new_line.strip[0...-1]
+ newline
end
+ un_next_line new_line
+ resolve_newlines
end
# Checks whether or not +line+ is in a multiline sequence.
def is_multiline?(text)
text && text.length > 1 && text[-1] == MULTILINE_CHAR_VALUE && text[-2] == ?\s
end
+ def handle_ruby_multiline(text)
+ text = text.rstrip
+ return text unless is_ruby_multiline?(text)
+ un_next_line @next_line.full
+ begin
+ new_line = raw_next_line.first
+ break if new_line == :eod
+ newline and next if new_line.strip.empty?
+ text << " " << new_line.strip
+ newline
+ end while is_ruby_multiline?(new_line.strip)
+ next_line
+ resolve_newlines
+ text
+ end
+
+ def is_ruby_multiline?(text)
+ text && text.length > 1 && text[-1] == ?, && text[-2] != ?? && text[-3..-2] != "?\\"
+ end
+
def contains_interpolation?(str)
str.include?('#{')
end
def unescape_interpolation(str, opts = {})
res = ''
- rest = Haml::Shared.handle_interpolation str.dump do |scan|
+ rest = Haml::Shared.handle_interpolation inspect_obj(str) do |scan|
escapes = (scan[2].size - 1) / 2
res << scan.matched[0...-3 - escapes]
if escapes % 2 == 1
res << '#{'
else
content = eval('"' + balance(scan, ?{, ?}, 1)[0][0...-1] + '"')
- content = "Haml::Helpers.html_escape(#{content})" if opts[:escape_html]
+ content = "Haml::Helpers.html_escape((#{content}))" if opts[:escape_html]
res << '#{' + content + "}"# Use eval to get rid of string escapes
end
end
res + rest
end
@@ -1028,12 +1068,11 @@
@newlines -= 1
end
def resolve_newlines
return unless @newlines > 0
- flush_merged_text unless @to_merge.all? {|type, *_| type == :text}
- @precompiled << "\n" * @newlines
+ @to_merge << [:newlines, @newlines]
@newlines = 0
end
# Get rid of and whitespace at the end of the buffer
# or the merged text
@@ -1052,9 +1091,11 @@
@to_merge.slice! index
rstrip_buffer! index
end
when :script
last[1].gsub!(/\(haml_temp, (.*?)\);$/, '(haml_temp.rstrip, \1);')
+ rstrip_buffer! index - 1
+ when :newlines
rstrip_buffer! index - 1
else
raise SyntaxError.new("[HAML BUG] Undefined entry in Haml::Precompiler@to_merge.")
end
end