From 563d5d748ed736455a695a74ea7609ea3e525a82 Mon Sep 17 00:00:00 2001 From: packagethief Date: Fri, 29 Sep 2006 18:39:13 +0000 Subject: [PATCH] Enginification git-svn-id: svn://hamptoncatlin.com/haml/trunk@48 7063305b-7217-0410-af8c-cdc13e5119b9 --- init.rb | 2 +- lib/haml/engine.rb | 148 ++++++++++++------------- lib/haml/template.rb | 32 ++++++ test/benchmark.rb | 10 +- test/engine_test.rb | 91 ++++++--------- test/helper_test.rb | 32 +++--- test/results/content_for_layout.xhtml | 10 ++ test/template_test.rb | 68 ++++++++++++ test/templates/content_for_layout.haml | 6 + 9 files changed, 238 insertions(+), 161 deletions(-) create mode 100644 lib/haml/template.rb create mode 100644 test/results/content_for_layout.xhtml create mode 100644 test/template_test.rb create mode 100644 test/templates/content_for_layout.haml diff --git a/init.rb b/init.rb index f7119e55..e90f5632 100644 --- a/init.rb +++ b/init.rb @@ -1,4 +1,4 @@ require 'haml/engine' require 'haml/helpers' -ActionView::Base.register_template_handler('haml', Haml::Engine) \ No newline at end of file +ActionView::Base.register_template_handler('haml', Haml::Template) \ No newline at end of file diff --git a/lib/haml/engine.rb b/lib/haml/engine.rb index a6f6f6b2..b6fb9668 100644 --- a/lib/haml/engine.rb +++ b/lib/haml/engine.rb @@ -3,53 +3,34 @@ require File.dirname(__FILE__) + '/helpers' module Haml #:nodoc: 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.

Hello world

ONE_LINER_LENGTH = 50 MULTILINE_CHAR_VALUE = '|'[0] - - def initialize(view) - @view = view + + def initialize(template, action_view=nil) + @view = action_view + @template = template #String @result = String.new @to_close_queue = [] end - - def render(template, local_assigns={}) - assigns = @view.assigns.dup - - # Do content for layout on its own to keep things working in partials - if content_for_layout = @view.instance_variable_get("@content_for_layout") - assigns['content_for_layout'] = content_for_layout - end - - # Get inside the view object's world - @view.instance_eval do - # Set all the instance variables - assigns.each do |key,val| - instance_variable_set "@#{key}", val - end - # Set all the local assigns - local_assigns.each do |key,val| - class << self; self; end.send(:define_method, key) { val } - end - end - - # Process each line of the template returning the resulting string - template.each_with_index do |line, index| + + def to_html + # Process each line of the template + @template.each_with_index do |line, index| count, line = count_soft_tabs(line) - surpress_render, line, count = handle_multiline(count, line) if !surpress_render && count && line count, line = process_line(count, line) end end - + # Close all the open tags @to_close_queue.length.times { close_tag } - + # Return the result string @result end @@ -61,7 +42,7 @@ module Haml #:nodoc: if count <= @to_close_queue.size && @to_close_queue.size > 0 (@to_close_queue.size - count).times { close_tag } end - + case line.first when '.', '#' render_div(line) @@ -70,9 +51,9 @@ module Haml #:nodoc: when '/' render_comment(line) when '=' - add template_eval(line[1, line.length]).to_s + add template_eval(line[1, line.length]).to_s if @view when '~' - add find_and_flatten(template_eval(line[1, line.length])).to_s + add find_and_flatten(template_eval(line[1, line.length])).to_s if @view else add line.strip end @@ -81,22 +62,28 @@ module Haml #:nodoc: end def handle_multiline(count, line) - # The code to handle how a multi-line object should work. - if @multiline_buffer && line[-1] == MULTILINE_CHAR_VALUE # '|' is 124 + # Multilines are denoting by ending with a `|` (124) + if @multiline_buffer && line[-1] == MULTILINE_CHAR_VALUE + # A multiline string is active, and is being continued @multiline_buffer += line[0...-1] supress_render = true + elsif line[-1] == MULTILINE_CHAR_VALUE + # A multiline string has just been activated, start adding the lines @multiline_buffer = line[0...-1] @multiline_count = count supress_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 supress_render = false end + return supress_render, line, count end @@ -107,15 +94,27 @@ module Haml #:nodoc: end end + def build_attributes(attributes = {}) + attributes.empty? ? String.new : String.new(' ') << (attributes.collect {|a,v| "#{a.to_s}='#{v.to_s}'" unless v.nil? }).compact.join(' ') + end + def open_tag(name, attributes = {}) add "<#{name.to_s}#{build_attributes(attributes)}>" @to_close_queue.push name end + def close_tag + add "" + end + def one_line_tag(name, value, attributes = {}) add "<#{name.to_s}#{build_attributes(attributes)}>#{value}" end + def one_liner?(value) + value.length <= ONE_LINER_LENGTH && value.scan(/\n/).empty? + end + def print_tag(name, value, attributes = {}) unless value.empty? if one_liner? value @@ -136,45 +135,6 @@ module Haml #:nodoc: add "<#{name.to_s}#{build_attributes(attributes)} />" end - def build_attributes(attributes = {}) - attributes.empty? ? String.new : String.new(' ') << (attributes.collect {|a,v| "#{a.to_s}='#{v.to_s}'" unless v.nil? }).compact.join(' ') - end - - def close_tag - add "" - end - - def render_div(line) - render_tag('%div' + line) - end - - def render_comment(line) - add "" - 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) - attributes.merge!(template_eval(attributes_hash)) unless (attributes_hash.nil? || attributes_hash.empty?) - - 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 - - if action == '/' - atomic_tag(tag_name, attributes) - elsif action == '=' || action == '~' - value = template_eval(value) - value = find_and_flatten(value) if action == '~' - print_tag(tag_name, value.to_s, attributes) if value - else - print_tag(tag_name, value.to_s.strip, attributes) - end - end - end - - # Searches for `#` and `.` characters indicating id and class attributes def parse_class_and_id(list) attributes = {} list.scan(/([#.])([-a-zA-Z_()]+)/).each do |type, property| @@ -188,11 +148,45 @@ module Haml #:nodoc: attributes end - def one_liner?(value) - value.length <= ONE_LINER_LENGTH && value.scan(/\n/).empty? + 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) + + unless (attributes_hash.nil? || attributes_hash.empty?) + # Determine whether to eval the attributes hash in the context of a template + add_attributes = @view ? template_eval(attributes_hash) : eval(attributes_hash) + attributes.merge!(add_attributes) + end + + + if @view + 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 + end + + case action + when '/' + atomic_tag(tag_name, attributes) + when '=', '~' + value = template_eval(value) if @view + value = find_and_flatten(value) if action == '~' and @view + print_tag(tag_name, value.to_s, attributes) if value + else + print_tag(tag_name, value.to_s.strip, attributes) + end + end end - # Evaluates input in the context of the current ActionView instance + def render_div(line) + render_tag('%div' + line) + end + + def render_comment(line) + add "" + end + def template_eval(args) @view.instance_eval(args) end diff --git a/lib/haml/template.rb b/lib/haml/template.rb new file mode 100644 index 00000000..7e9bfe0c --- /dev/null +++ b/lib/haml/template.rb @@ -0,0 +1,32 @@ +require File.dirname(__FILE__) + '/engine' + +module Haml + class Template + def initialize(view) + @view = view + end + + def render(template, local_assigns={}) + assigns = @view.assigns.dup + + # Do content for layout on its own to keep things working in partials + if content_for_layout = @view.instance_variable_get("@content_for_layout") + assigns['content_for_layout'] = content_for_layout + end + + # Get inside the view object's world + @view.instance_eval do + # Set all the instance variables + assigns.each do |key,val| + instance_variable_set "@#{key}", val + end + # Set all the local assigns + local_assigns.each do |key,val| + class << self; self; end.send(:define_method, key) { val } + end + end + + Haml::Engine.new(template, @view).to_html + end + end +end \ No newline at end of file diff --git a/test/benchmark.rb b/test/benchmark.rb index dcaf77f9..5e82858d 100644 --- a/test/benchmark.rb +++ b/test/benchmark.rb @@ -1,18 +1,14 @@ -require File.dirname(__FILE__) + '/../lib/haml/engine' - -$:.unshift File.join(File.dirname(__FILE__), "..", "lib") - require 'rubygems' require 'action_view' -include Haml::Helpers +require File.dirname(__FILE__) + '/../lib/haml/template' -ActionView::Base.register_template_handler("haml", Haml::Engine) +ActionView::Base.register_template_handler("haml", Haml::Template) @base = ActionView::Base.new(File.dirname(__FILE__)) RUNS = (ARGV[0] || 100).to_i Benchmark.bm do |b| b.report("haml: ") { RUNS.times { @base.render "templates/standard" } } - b.report("rhtml:") { RUNS.times { @base.render "rhtml/standard" } } + b.report("erb: ") { RUNS.times { @base.render "rhtml/standard" } } end diff --git a/test/engine_test.rb b/test/engine_test.rb index 1571e61e..ddb46275 100644 --- a/test/engine_test.rb +++ b/test/engine_test.rb @@ -1,80 +1,53 @@ require 'test/unit' require File.dirname(__FILE__) + '/../lib/haml/engine' -require File.dirname(__FILE__) + '/mocks/article' - -$:.unshift File.join(File.dirname(__FILE__), "..", "lib") - -require 'rubygems' -require 'action_view' - -class HamlTest < Test::Unit::TestCase - include Haml::Helpers +class EngineTest < Test::Unit::TestCase def setup - ActionView::Base.register_template_handler("haml", Haml::Engine) - @base = ActionView::Base.new(File.dirname(__FILE__) + "/templates/") - @engine = Haml::Engine.new(@base) - @base.instance_variable_set("@article", Article.new) end def render(text) - @engine.render(text) + Haml::Engine.new(text).to_html end - - def load_result(name) - @result = '' - File.new(File.dirname(__FILE__) + "/results/#{name}.xhtml").each_line { |l| @result += l} - @result - end - - def assert_renders_correctly(name) - load_result(name).split("\n").zip(@base.render(name).split("\n")).each do |pair| - assert_equal(pair.first, pair.last) - #puts pair.inspect - end - end - - # Make sure our little environment builds - def test_build_stub - assert_not_nil(@engine) - assert @engine.is_a?(Haml::Engine) - assert_equal(Haml::Engine, @engine.class) - end - - def test_empty_render + + def test_empty_render_should_remain_empty assert_equal('', render('')) end - def test_renderings - assert_renders_correctly("very_basic") - assert_renders_correctly("standard") - assert_renders_correctly("helpers") - assert_renders_correctly("whitespace_handling") - assert_renders_correctly("original_engine") - assert_renders_correctly("list") - assert_renders_correctly("helpful") + def test_normal_renders_should_not_eval + assert_equal("", render("= 1+1")) + assert_equal("", render("= @content_for_layout")) + assert_equal("", render("~ @foobar")) end - def test_instance_variables - @base.instance_eval("@content_for_layout = 'Hampton'") - @base.instance_eval("@assigns['last_name'] = 'Catlin'") - #make sure to reload! - @engine = Haml::Engine.new(@base) - assert_equal("
Hampton Catlin
\n", render(".author= @content_for_layout + ' ' + @last_name")) + # This is ugly because Hashes are unordered; we don't always know the order + # in which attributes will be returned. + # There is probably a better way to do this. + def test_attributes_should_render_correctly + assert_equal("
\n
", render(".atlantis{:style => 'ugly'}").chomp) + rescue + assert_equal("
\n
", render(".atlantis{:style => 'ugly'}").chomp) end - def test_instance_variables_changing - @base.instance_eval("@author = 'Hampton'") - assert_equal("Hampton\n", render("= @author")) + def test_ruby_code_should_work_inside_attributes + author = 'hcatlin' + assert_equal("

foo

", render("%p{:class => 1+2} foo").chomp) end - def test_nil_attribute - assert_equal("
\n
\n", - render(".no_attributes{:nil => nil}")) + def test_nil_should_render_empty_tag + assert_equal("
\n
", + render(".no_attributes{:nil => nil}").chomp) end - def test_stripped_strings - assert_equal("
This should have no spaces in front of it
\n", - render(".stripped This should have no spaces in front of it")) + def test_strings_should_get_stripped_inside_tags + assert_equal("
This should have no spaces in front of it
", + render(".stripped This should have no spaces in front of it").chomp) + end + + def test_one_liner_should_be_one_line + assert_equal("

Hello

", render('%p Hello').chomp) + end + + def test_long_liner_should_not_print_on_one_line + assert_equal("
\n #{'x' * 51}\n
", render("%div #{'x' * 51}").chomp) end end diff --git a/test/helper_test.rb b/test/helper_test.rb index b9116fa4..95c522b4 100644 --- a/test/helper_test.rb +++ b/test/helper_test.rb @@ -1,36 +1,34 @@ -require 'test/unit' -require File.dirname(__FILE__) + '/../lib/haml/engine' +require File.dirname(__FILE__) + '/../lib/haml/helpers' -$:.unshift File.join(File.dirname(__FILE__), "..", "lib") - -require 'rubygems' -require 'action_view' -require 'active_support' - -class HamlTest < Test::Unit::TestCase +class HelperTest < Test::Unit::TestCase include Haml::Helpers def test_find_and_flatten assert_equal(find_and_flatten("

"), "

") + assert_equal(find_and_flatten("TEST!\t\t

"), "TEST!\t\t

") + assert_equal(find_and_flatten("
Hello\nWorld!\nYOU ARE \rFLAT?\n\rOMGZ!

"), "
Hello
World!
YOU ARE FLAT?
OMGZ!

") - assert_equal( "
\n \n
\n", - find_and_flatten("
\n \n
\n")) - assert_equal( "Two lines
a
b
c
", - find_and_flatten("Two\nlines
a\nb\nc
")) - assert_equal( "
Two
lines
\n
a
b
c
", - find_and_flatten("
Two\nlines
\n
a\nb\nc
")) + + assert_equal(find_and_flatten("
\n \n
\n"), + "
\n \n
\n") + + assert_equal(find_and_flatten("Two\nlines
a\nb\nc
"), + "Two lines
a
b
c
") + + assert_equal(find_and_flatten("
Two\nlines
\n
a\nb\nc
"), + "
Two
lines
\n
a
b
c
") end - def test_tabs + def test_tabs_should_render_correctly assert_equal(" ", tabs(1)) assert_equal(" ", tabs(5)) end - def test_list_of + def test_list_of_should_render_correctly assert_equal("
  • 1
  • \n
  • 2
  • ", (list_of([1, 2]) { |i| i.to_s})) assert_equal("
  • 1
  • ", (list_of([[1]]) { |i| i.first})) end diff --git a/test/results/content_for_layout.xhtml b/test/results/content_for_layout.xhtml new file mode 100644 index 00000000..e8a5ab57 --- /dev/null +++ b/test/results/content_for_layout.xhtml @@ -0,0 +1,10 @@ + + + + + +
    + Lorem ipsum dolor sit amet +
    + + diff --git a/test/template_test.rb b/test/template_test.rb new file mode 100644 index 00000000..bcf20886 --- /dev/null +++ b/test/template_test.rb @@ -0,0 +1,68 @@ +require 'test/unit' +require 'rubygems' +require 'action_view' + +require File.dirname(__FILE__) + '/../lib/haml/template' +require File.dirname(__FILE__) + '/mocks/article' + +class TemplateTest < Test::Unit::TestCase + def setup + ActionView::Base.register_template_handler("haml", Haml::Template) + @base = ActionView::Base.new(File.dirname(__FILE__) + "/../test/templates/") + @base.instance_variable_set("@article", Article.new) + end + + def render(text) + Haml::Engine.new(text, @base).to_html + end + + def load_result(name) + @result = '' + File.new(File.dirname(__FILE__) + "/results/#{name}.xhtml").each_line { |l| @result += l } + @result + end + + def assert_renders_correctly(name) + load_result(name).split("\n").zip(@base.render(name).split("\n")).each do |pair| + assert_equal(pair.first, pair.last) + end + end + + def test_empty_render_should_remain_empty + assert_equal('', render('')) + end + + def test_templates_should_render_correctly + %w{very_basic standard helpers whitespace_handling original_engine list helpful}.each do |template| + assert_renders_correctly template + end + end + + def test_action_view_templates_render_correctly + @base.instance_variable_set("@content_for_layout", 'Lorem ipsum dolor sit amet') + assert_renders_correctly 'content_for_layout' + end + + def test_instance_variables_should_work_inside_templates + @base.instance_variable_set("@content_for_layout", 'something') + assert_equal("

    something

    ", render("%p= @content_for_layout").chomp) + + @base.instance_eval("@author = 'Hampton Catlin'") + assert_equal("
    Hampton Catlin
    ", render(".author= @author").chomp) + + @base.instance_eval("@author = 'Hampton'") + assert_equal("Hampton", render("= @author").chomp) + + @base.instance_eval("@author = 'Catlin'") + assert_equal("Catlin", render("= @author").chomp) + end + + def test_instance_variables_should_work_inside_attributes + @base.instance_eval("@author = 'hcatlin'") + assert_equal("

    foo

    ", render("%p{:class => @author} foo").chomp) + end + + def test_template_renders_should_eval + assert_equal("2\n", render("= 1+1")) + end +end diff --git a/test/templates/content_for_layout.haml b/test/templates/content_for_layout.haml new file mode 100644 index 00000000..94589336 --- /dev/null +++ b/test/templates/content_for_layout.haml @@ -0,0 +1,6 @@ +!!! +%html + %head + %body + #content + = @content_for_layout \ No newline at end of file