haml--haml/lib/haml/engine.rb

242 lines
7.3 KiB
Ruby

require File.dirname(__FILE__) + '/helpers'
module Haml
class Engine
include Haml::Helpers
# Set the maximum length for a line to be considered a one-liner
# Lines <= the maximum will be rendered on one line,
# i.e. <tt><p>Hello world</p></tt>
ONE_LINER_LENGTH = 50
SPECIAL_CHARACTERS = %w(# . = ~ % /).collect { |c| c[0] }
MULTILINE_CHAR_VALUE = '|'[0]
MULTILINE_STARTERS = SPECIAL_CHARACTERS - ["/"[0]]
def initialize(template, options = {})
#turn each of the options into instance variables for the object
options.each { |k,v| eval("@#{k} = v") }
@template = template #String
@result, @precompiled, @to_close_stack = String.new, String.new, []
@scope_object = Object.new if @scope_object.nil?
end
def to_html
# Process each line of the template
@template.each_with_index do |line, index|
count, line = count_soft_tabs(line)
suppress_render, line, count = handle_multiline(count, line)
if !suppress_render && count && line
count, line = process_line(count, line)
end
end
# Make sure an ending multiline gets closed
handle_multiline(0, nil)
# Close all the open tags
@to_close_stack.length.times { close_tag }
# Compile the @precompiled buffer
compile
# Return the result string
@result
end
def process_line(count, line)
if line.strip[0, 3] == '!!!'
push_text %|<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">|
else
if count <= @to_close_stack.size && @to_close_stack.size > 0
(@to_close_stack.size - count).times { close_tag }
end
if line.length > 0
case line[0].chr
when '.', '#'
render_div(line)
when '%'
render_tag(line)
when '/'
render_comment(line)
when '='
push_script(line[1, line.length])
when '~'
push_script(line[1, line.length], true)
else
push_text line.strip
end
end
end
end
def handle_multiline(count, line)
# Multilines are denoting by ending with a `|` (124)
if line && (line[-1] == MULTILINE_CHAR_VALUE) && @multiline_buffer
# A multiline string is active, and is being continued
@multiline_buffer += line[0...-1]
suppress_render = true
elsif line && (line[-1] == MULTILINE_CHAR_VALUE) && (MULTILINE_STARTERS.include? line[0])
# A multiline string has just been activated, start adding the lines
@multiline_buffer = line[0...-1]
@multiline_count = count
suppress_render = true
elsif @multiline_buffer
# A multiline string has just ended, make line into the result
process_line(@multiline_count, @multiline_buffer)
@multiline_buffer = nil
suppress_render = false
end
return suppress_render, line, count
end
def compile
# Set the local variables pointing to the buffer
result = @result
@scope_object.instance_eval do
@haml_stack ||= Array.new
@haml_stack.push(result)
self.class.instance_eval { include Haml::Helpers }
end
# Evaluate the buffer in the context of the scope object
# This automatically dumps the result into @result
@scope_object.instance_eval @precompiled
# Get rid of the current buffer
@scope_object.instance_eval do
@haml_stack.pop
end
end
def push_visible(text)
@precompiled << "@haml_stack[-1] << #{tabs(@to_close_stack.size).dump} << #{text}\n"
end
def push_silent(text)
@precompiled << "#{text}\n"
end
def push_text(text)
push_visible("#{text.dump} << \"\\n\"")
end
def push_script(text, flattened = false)
unless @suppress_eval
push_silent("haml_temp = #{text}")
if flattened
push_silent("haml_temp = find_and_flatten(haml_temp)")
end
push_visible("#{wrap_script("haml_temp", @to_close_stack.size)} << \"\\n\"")
end
end
def build_attributes(attributes = {})
result = attributes.collect do |a,v|
unless v.nil?
first_quote_type = v.to_s.scan(/['"]/).first
quote_type = (first_quote_type == "'") ? '"' : "'"
"#{a.to_s}=#{quote_type}#{v.to_s}#{quote_type}"
end
end
result = result.compact.join(' ')
(attributes.empty? ? String.new : String.new(' ')) + result
end
def open_tag(name, attributes = {})
push_text "<#{name.to_s}#{build_attributes(attributes)}>"
@to_close_stack.push name
end
def close_tag
push_text "</#{@to_close_stack.pop}>"
end
def one_line_tag(name, value, attributes = {})
push_text "<#{name.to_s}#{build_attributes(attributes)}>#{value}</#{name.to_s}>"
end
def one_liner?(value)
value.length <= ONE_LINER_LENGTH && value.scan(/\n/).empty?
end
def push_tag(name, value, attributes = {}, parse = false, flattened = false)
unless value.empty?
if !parse && one_liner?(value)
one_line_tag(name, value, attributes)
else
open_tag(name, attributes)
if parse
push_script(value, flattened)
else
push_text(value)
end
close_tag
end
else
open_tag(name, attributes)
end
end
# Creates single line tags, i.e. <tt><hello /></tt>
def atomic_tag(name, attributes = {})
push_text "<#{name.to_s}#{build_attributes(attributes)} />"
end
def parse_class_and_id(list)
attributes = {}
list.scan(/([#.])([-a-zA-Z_()]+)/).each do |type, property|
case type
when '.'
attributes[:class] = property
when '#'
attributes[:id] = property
end
end
attributes
end
def render_tag(line)
line.scan(/[%]([-_a-z1-9]+)([-_a-z\.\#]*)(\{.*\})?(\[.*\])?([=\/\~]?)?(.*)?/).each do |tag_name, attributes, attributes_hash, object_ref, action, value|
attributes = parse_class_and_id(attributes.to_s)
#SimplyHelpful style logic with the [@model] helper
if object_ref && (object_ref = template_eval(object_ref).first)
class_name = object_ref.class.to_s.underscore
attributes.merge!(:id => "#{class_name}_#{object_ref.id}", :class => class_name)
end
unless (attributes_hash.nil? || attributes_hash.empty?)
# Determine whether to eval the attributes hash in the context of a template
add_attributes = template_eval(attributes_hash)
attributes.merge!(add_attributes)
end
case action
when '/'
atomic_tag(tag_name, attributes)
when '=', '~'
flattened = (action == '~')
push_tag(tag_name, value.to_s, attributes, true, flattened) if value
else
push_tag(tag_name, value.to_s.strip, attributes)
end
end
end
def render_div(line)
render_tag('%div' + line)
end
def render_comment(line)
push_text "<!-- #{line[1..line.length].strip} -->"
end
def template_eval(args)
!@suppress_eval ? @scope_object.instance_eval(args) : ""
end
end
end