diff --git a/README b/README index 5333b261..d20674f6 100644 --- a/README +++ b/README @@ -22,6 +22,9 @@ HAML was originally created by Hampton Catlin (hcatlin). Help with the Ruby On Rails implementation and much of the documentation by Jeff Hardy (packagethief). +Nathan Weizenbaum (Nex3) contribued the buffered-engine code along with many +other enhancements including the silent-line syntax ("-"). + If you use this software, you must pay Hampton a compliment. Say something nice about it. Beyond that, the implementation is licensed under the MIT License. Ok, fine, I guess that means compliments aren't *required*. @@ -50,72 +53,185 @@ is compiled to: == Characters with meaning to Haml -Haml responds to certain special characters. To create an element in the form of - use the % character, immediately followed -by the element name. To specify attributes, include a hash of attributes inside -curly braces. Example: +Various characters, when placed at a certain point in a line, instruct HAML +to render different types of things. + +=== XHTML Tags + +These characters render XHTML tags. + +==== % + +This element is placed at the beginning of a line. It's followed immediately +by the name of an element, then optionally by modifiers (see below), a space, +and text to be rendered inside the element. It creates an element in the form of +. For example: %one - %meta{:content => 'something'}/ %two %three Hey there - + is compiled to: - Hey there Any string is a valid element name; Haml will automatically generate opening and -closing tags for any element. When you want to force the output of a -self-closing tag, use the forward slash character. Example: +closing tags for any element. - %br/ # =>
- %meta{:http-equiv => 'Content-Type', :content => 'text/html'}/ - # => +==== {} -HTML div elements are assumed when no %tag is present and the line is -preceeded by either the # or the . characters. This convention -uses familiar CSS semantics: # denotes the id of the element, -. denotes its class name. Example: +Brackets represent a Ruby hash that is used for specifying the attributes of an +element. It is literally evaluated as a Ruby hash, so logic will work in it. At +the moment, though, it doesn't see local variables. The hash is placed after +the tag is defined. For example: - #collection - .item - Broken record album - -is the same as: + %head{ :name => "doc_head" } + %script{ 'type' => "text/" + "javascript", :src => "javascripts/script_#{2 + 7}" } - %div{:id => collection} - %div{:class => 'item'} - Broken record album +is compiled to: -and is comiled to: + + + + +==== [] -
-
Broken record album
+Square brackets follow a tag definiton and contain a Ruby object that is used to +set the class and id of that tag. The class is set to the object's class +(transformed to use underlines rather than camel case), and the id is set to the +object's class followed by its id. Because the id of an object is normally an +obscure implementation detail, this is most useful for elements that represent +instances of Models. For example: + + # file: app/controllers/users_controller.rb + + def show + @user = CrazyUser.find(15) + end + + # file: app/views/users/show.haml + + %div[@user] + %bar[290]/ + Hello! + +is compiled to: + +
+ + Hello!
-There is a shortcut when you want to specify either the id or class attributes -of an element: follow the element name with either the # or the -. characters. Example: +This is based off of DHH's SimplyHelpful syntax as presented at RailsConf Europe 2006. - #things +==== / + +The forward slash character, when placed at the end of a tag definition, causes +the tag to be self-closed. For example: + + %br/ + %meta{:http-equiv => 'Content-Type', :content => 'text/html'}/ + +is compiled to: + +
+ + +==== . and # + +The period and pound sign are borrowed from CSS and used as shortcuts to specify the +class and id attributes of an element, respectively. They are +placed immediately after the tag, and before an attributes hash. For example: + + div#things %span#rice Chicken Fried - %p.beans The magical fruit + %p.beans{ :food => 'true' } The magical fruit + %h1.class#id La La La is compiled to:
Chicken Fried -

The magical fruit

+

The magical fruit

+

La La La

-=== Specifying a document type +==== Assumed Divs -When describing xhtml documents with Haml, you can have a document type +Because the div element is used so often, it is the default element. If you only +define a class and/or id using the . or # syntax, a div element +is automatically used. For example: + + #collection + .item + .description What a cool item! + +is the same as: + + %div{:id => collection} + %div{:class => 'item'} + %div{:class => 'description'} What a cool item! + +and is compiled to: + +
+
Broken record album
+
What a cool item!
+
+ +==== = and ~ + += and ~ are placed at the end of a tag definition, after class, +id, and attribute declarations. They're just shortcuts for inserting Ruby code +into an element. They work the same as = and ~ without a tag; +see below for documentation of those. For example: + + %p= "hello" + %h1~ 1 + 2 + +is the same as: + + %p + = "hello" + %h1 + ~ 1 + 2 + +and is compiled to: + +

+ hello +

+

+ 3 +

+ +=== XHTML Helpers + +==== No Special Character + +If no special character appears at the beginning of a line, it is rendered as plain +text. For example: + + %gee + %whiz + Wow this is cool! + +is compiled to: + + + + Wow this is cool! + + + +==== !!! + +When describing XHTML documents with Haml, you can have a document type generated automatically by including the characters !!! as the first line in your document. Example: @@ -139,11 +255,158 @@ is compiled to:

Sign my guestbook

+ +==== / + +The forward slash character, when placed at the beginning of a line, wraps all +text after it in an HTML comment. For example: + + %billabong + / This is the billabong element + I like billabongs! + +is compiled to: + + + + I like billabongs! + + +==== | + +The pipe character designates a multiline string. It's placed at the end of a line, +and means that all following lines that end with | will be evaluated as +though they were on the same line. For example: + + %whoo + %hoo I think this might get | + pretty long so I should | + probably make it | + multiline so it doesn't | + look awful. | + %p This is short. + +is compiled to: + + %hoo I think this might get | + pretty long so I should | + probably make it | + multiline so it doesn't | + look awful. | + +=== Ruby evaluators + +==== = + +The equals character is followed by Ruby code, which is evaluated and the output +inserted into the document as plain text. For example: + + %p + = ['hi', 'there', 'reader!'].join " " + = "yo" + +is compiled to: + +

+ hi there reader! + yo +

+ +==== ~ + +The tilde character works the same as the equals character, but the output is +modified in such a way that newlines in whitespace-sensitive elements work +properly. For example: + + %foo + = "Woah
  this is   \n
crazy" + %foo2 + ~ "Woah
  this is   \n
crazy" + +is compiled to: + + + Woah
  this is   
+    
crazy +
+ + Woah
  this is   
crazy +
+ +==== - + +The hyphen character makes the text following it into "silent script", or +Ruby script that is evaluated, but not output. + +It is not reccomended that you use this widely; almost all processing +code and logic should be kept to the Controller, the Helper, or partials. + +For example: + + - foo = "hello" + - foo << " there" + - foo << " you!" + %p= foo + +is compiled to: + +

+ hello there you! +

+ +===== Blocks + +Like XHTML tags, you don't need to explicity close your Ruby blocks in +HAML. Rather, they're automatically closed based on tabs. A block begins +whenever the indentation is increased after a silent script command, and +ends when the indentation decreases (as long as it's not an +else+ clause +or something similar). For example: + + - (42...47).each do |i| + %p= i + %p See, I can count! + +is compiled to: + +

+ 42 +

+

+ 43 +

+

+ 44 +

+

+ 45 +

+

+ 46 +

+ +Another example: + + %p + - case 2 + - when 1 + = "1!" + - when 2 + = "2?" + - when 3 + = "3." + +is compiled to: + +

+ 2? +

== Using Haml as a Rails plugin Write Rails templates with the .haml extension. Example: + # file: app/views/movies/teen_wolf.haml + %html %head %title= "Teen Wolf (1985)" @@ -176,12 +439,9 @@ is compiled to: You can access instance variables in Haml templates the same way you do in ERb -templates. Helper methods are also available in Haml templates. To specify that -a line should be evaulated as Ruby, use the = character at the begining -of a line, or immediately following an element name. The return value of the -method call will be inserted into the stream. Example: +templates. Helper methods are also available in Haml templates. Example: - file: app/controllers/movies_controller.rb + # file: app/controllers/movies_controller.rb class MoviesController < ApplicationController def index @@ -189,14 +449,14 @@ method call will be inserted into the stream. Example: end end - file: app/views/movies/index.haml + # file: app/views/movies/index.haml #content .title %h1= @title = link_to 'Home', home_url -is be compiled to: +may be compiled to:
@@ -206,8 +466,6 @@ is be compiled to:
- - --- Copyright (c) 2006 Hampton Catlin Licensed under the MIT License diff --git a/Rakefile b/Rakefile index a70ef857..3dc87b8e 100644 --- a/Rakefile +++ b/Rakefile @@ -1,7 +1,19 @@ +require 'rubygems' require 'rake' require 'rake/testtask' require 'rake/rdoctask' -$:.unshift File.join(File.dirname(__FILE__), "..", "lib") + +volatile_requires = ['rcov/rcovtask'] +not_loaded = [] +volatile_requires.each do |file| + begin + require file + rescue LoadError + not_loaded.push file + end +end + +# ----- Default: Testing ------ desc 'Default: run unit tests.' task :default => :test @@ -13,23 +25,79 @@ Rake::TestTask.new(:test) do |t| t.verbose = true end -desc 'Benchmark HAML against ERb. The benchmark routine is run 100. Use TIMES=n to override' +# ----- Benchmarking ----- + +temp_desc = <

Hello world

+ ONE_LINER_LENGTH = 50 + + # The string that holds the compiled XHTML. This is aliased as + # _erbout for compatibility with ERB-specific code. + attr_accessor :buffer + + # Creates a new buffer. + def initialize + @buffer = "" + @one_liner_pending = false + end + + # Renders +text+ with the proper tabulation. This also deals with + # making a possible one-line tag one line or not. + def push_text(text, tabulation) + if @one_liner_pending && one_liner?(text) + @buffer << text + else + if @one_liner_pending + @buffer << "\n" + @one_liner_pending = false + end + @buffer << "#{tabs(tabulation)}#{text}\n" + end + end + + # Properly formats the output of a script that was run in the + # instance_eval. + def push_script(result, tabulation, flattened) + if flattened + result = find_and_flatten(result) + end + unless result.nil? + result = result.to_s.chomp.gsub("\n", "\n#{tabs(tabulation)}") + push_text result, tabulation + end + nil + end + + # Takes the various information about the opening tag for an + # element, formats it, and adds it to the buffer. + def open_tag(name, tabulation, atomic, try_one_line, class_id, attributes_hash, obj_ref) + attributes = {} + attributes.merge!(parse_object_ref(obj_ref)) if obj_ref + attributes.merge!(parse_class_and_id(class_id)) if class_id + attributes.merge!(attributes_hash) unless attributes_hash.nil? || attributes_hash.empty? + + @buffer << "#{tabs(tabulation)}<#{name}#{build_attributes(attributes)}" + @one_liner_pending = false + if atomic + @buffer << " />\n" + else + if try_one_line + @one_liner_pending = true + @buffer << ">" + else + @buffer << ">\n" + end + end + end + + + # Creates a closing tag with the given name. + def close_tag(name, tabulation) + if @one_liner_pending + @buffer << "\n" + @one_liner_pending = false + else + push_text("", tabulation) + end + end + + private + + # Gets count tabs. Mostly for internal use. + def tabs(count) + ' ' * count + end + + # 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) + attributes = {} + list.scan(/([#.])([-a-zA-Z_()]+)/) do |type, property| + case type + when '.' + if attributes[:class] + attributes[:class] += " " + else + attributes[:class] = "" + end + attributes[:class] += property + when '#' + attributes[:id] = property + end + end + attributes + end + + # Takes an array of objects and uses the class and id of the first + # one to create an attributes hash. + def parse_object_ref(ref) + ref = ref[0] + class_name = ref.class.to_s.underscore + {:id => "#{class_name}_#{ref.id}", :class => class_name} + end + + # Takes a hash and builds a list of XHTML attributes from it, returning + # the result. + 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 + + # Returns whether or not the given value is short enough to be rendered + # on one line. + def one_liner?(value) + value.length <= ONE_LINER_LENGTH && value.scan(/\n/).empty? + end + end +end diff --git a/lib/haml/engine.rb b/lib/haml/engine.rb index 05975743..f8033aa9 100644 --- a/lib/haml/engine.rb +++ b/lib/haml/engine.rb @@ -1,198 +1,330 @@ require File.dirname(__FILE__) + '/helpers' +require File.dirname(__FILE__) + '/buffer' +require 'profiler' -module Haml #:nodoc: +module Haml + # This is the class where all the parsing and processing of the HAML + # template is done. It can be directly used by the user by creating a + # new instance and calling to_html to render the template. For example: + # + # template = File.load('templates/really_cool_template.haml') + # haml_engine = Haml::Engine.new(template) + # output = haml_engine.to_html + # puts output 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 + + # Keeps track of the ASCII values of the characters that begin a + # specially-interpreted line. SPECIAL_CHARACTERS = %w(# . = ~ % /).collect { |c| c[0] } - MULTILINE_CHAR_VALUE = '|'[0] - MULTILINE_STARTERS = SPECIAL_CHARACTERS - ["/"[0]] + # The value of the character that designates that a line is part + # of a multiline string. + MULTILINE_CHAR_VALUE = '|'[0] + + # Characters that designate that a multiline string may be about + # to begin. + MULTILINE_STARTERS = SPECIAL_CHARACTERS - ["/"[0]] + + # Keywords that appear in the middle of a Ruby block with lowered + # indentation. If a block has been started using indentation, + # lowering the indentation with one of these won't end the block. + # For example: + # + # - if foo + # %p yes! + # - else + # %p no! + # + # The block is ended after %p no!, because else + # is a member of this array. + MID_BLOCK_KEYWORDS = ['else', 'elsif', 'rescue', 'ensure', 'when'] + + # Creates a new instace of Haml::Engine to compile the given + # template string. + # + # Available options are: + # + # [scope_object] The object within which the template will + # be compiled, via instance_eval. For a Rails + # application, this will typically be an + # instance of ActionView::Base. If not specified, + # this defaults to an instance of the Object class. + # [suppress_eval] Whether or not attribute hashes and Ruby scripts + # designated by = or ~ should be + # evaluated. If this is true, said scripts are + # rendered as empty strings. Defaults to false. 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, @to_close_queue = String.new, [] + @buffer = Haml::Buffer.new + @precompiled = String.new + @to_close_stack = [] + @tabulation = 0 + @scope_object = Object.new if @scope_object.nil? end + # Processes the template and returns the resulting (X)HTML code as + # a string. 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) + suppress_render = handle_multiline(count, line, index) - if !surpress_render && count && line - count, line = process_line(count, line) + if !suppress_render && count && line + count, line = process_line(count, line, index) end end - + + # Make sure an ending multiline gets closed + handle_multiline(0, nil, 0) + # Close all the open tags - @to_close_queue.length.times { close_tag } - + @to_close_stack.length.times { close } + + # Compile the @precompiled buffer + compile + # Return the result string - @result + @buffer.buffer end + + private - def process_line(count, line) - if line.strip[0, 3] == '!!!' - @result << %|\n| + # Processes a single line of HAML. count does *not* represent the + # line number; rather, it represents the tabulation count (the number of + # spaces before the line divided by two). + # + # This method doesn't return anything; it simply processes the line and + # adds the appropriate code to @precompiled. + def process_line(count, line, index) + if line.lstrip[0, 3] == '!!!' + push_text '' + else - if count <= @to_close_queue.size && @to_close_queue.size > 0 - (@to_close_queue.size - count).times { close_tag } + if count > @to_close_stack.size + + # Indentation has been increased without a new tag + if @latest_command == 45 # '-' + + # The indentation was increased after silent script, + # it must be a block + @to_close_stack.push '_haml_end_block' + end + + elsif count <= @to_close_stack.size && @to_close_stack.size > 0 && + (line.length == 0 || line[0] != 45 || !MID_BLOCK_KEYWORDS.include?(line[1..-1].split[0])) + + # The tabulation has gone down, and it's not because of one of + # Ruby's mid-block keywords + (@to_close_stack.size - count).times { close } end - - case line[0..0] - when '.', '#' - render_div(line) - when '%' - render_tag(line) - when '/' - render_comment(line) - when '=' - add template_eval(line[1, line.length]).to_s - when '~' - add find_and_flatten(template_eval(line[1, line.length])).to_s - else - add line.strip + + if line.length > 0 + @latest_command = line[0] + case @latest_command + when 46, 35 # '.', '#' + render_div(line, index) + when 37 # '%' + render_tag(line, index) + when 47 # '/' + render_comment(line) + when 61 # '=' + push_script(line[1..-1], false, index) + when 126 # '~' + push_script(line[1..-1], true, index) + when 45 # '-' + sub_line = line[1..-1] + unless sub_line[0] == 35 # '#' + push_silent(sub_line, index) + else + @latest_command = 35 + end + else + push_text line.strip + end end + end - return count, line end - def handle_multiline(count, line) + # Deals with all the logic of figuring out whether a given line is + # the beginning, continuation, or end of a multiline sequence. Like + # process_line, count represents the tabulation, not line + # number. + # + # This returns whether or not the line should be + # rendered normally. + def handle_multiline(count, line, index) # Multilines are denoting by ending with a `|` (124) - if (line[-1] == MULTILINE_CHAR_VALUE) && @multiline_buffer + if line && (line[-1] == MULTILINE_CHAR_VALUE) && @multiline_buffer # A multiline string is active, and is being continued @multiline_buffer += line[0...-1] - supress_render = true - elsif (line[-1] == MULTILINE_CHAR_VALUE) && (MULTILINE_STARTERS.include? line[0]) + 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 - supress_render = true + @multiline_index = index + suppress_render = true elsif @multiline_buffer # A multiline string has just ended, make line into the result - process_line(@multiline_count, @multiline_buffer) + process_line(@multiline_count, @multiline_buffer, @multiline_index) @multiline_buffer = nil - supress_render = false + suppress_render = false + end + + return suppress_render + end + + # Takes @precompiled, a string buffer of Ruby code, and + # evaluates it in the context of @scope_object, after preparing + # @scope_object. The code in @precompiled populates + # @buffer with the compiled XHTML code. + def compile + # Set the local variables pointing to the buffer + buffer = @buffer + @scope_object.instance_eval do + @haml_stack ||= Array.new + @haml_stack.push(buffer) + self.class.instance_eval { include Haml::Helpers } + + class << self + attr :haml_lineno + end end - return supress_render, line, count - end - - def add(line) - return if line.nil? - line.to_s.each_line do |me| - @result << tabs(@to_close_queue.size) << me.chomp << "\n" + @precompiled = < e + filename = "(haml)" + if @scope_object.methods.include? "haml_filename" + # For some reason that I can't figure out, + # @scope_object.methods.include? "haml_filename" && @scope_object.haml_filename + # is false when it shouldn't be. Nested if statements work, though. + + if @scope_object.haml_filename + filename = "#{@scope_object.haml_filename}.haml" + end + end + e.backtrace.unshift "#{filename}:#{@scope_object.haml_lineno}" + raise e end + + # Get rid of the current buffer + @scope_object.instance_eval do + @haml_stack.pop + end end - def build_attributes(attributes = {}) - result = attributes.collect { |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 - } - result = result.compact.join(' ') - (attributes.empty? ? String.new : String.new(' ')) + result - 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 - one_line_tag(name, value, attributes) - else - open_tag(name, attributes) - add value - close_tag - end + # Evaluates text in the context of @scope_object, but + # does not output the result. + def push_silent(text, index = nil) + if index + @precompiled << "@haml_lineno = #{index + 1}\n#{text}\n" else - open_tag(name, attributes) - add value + # Not really DRY, but probably faster + @precompiled << "#{text}\n" end end - # Creates single line tags, i.e. - def atomic_tag(name, attributes = {}) - add "<#{name.to_s}#{build_attributes(attributes)} />" + # Adds text to @buffer with appropriate tabulation + # without parsing it. + def push_text(text) + @precompiled << "_hamlout.push_text(#{text.dump}, #{@tabulation})\n" 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 '=', '~' - value = template_eval(value) - value = find_and_flatten(value) if action == '~' - print_tag(tag_name, value.to_s, attributes) - else - print_tag(tag_name, value.to_s.strip, attributes) - end + # Causes text to be evaluated in the context of + # @scope_object and the result to be added to @buffer. + # + # If flattened is true, Haml::Helpers#find_and_flatten is run on + # the result before it is added to @buffer + def push_script(text, flattened, index) + unless @suppress_eval + push_silent("haml_temp = #{text}", index) + @precompiled << "haml_temp = _hamlout.push_script(haml_temp, #{@tabulation}, #{flattened})\n" end end - def render_div(line) - render_tag('%div' + line) + # Closes the most recent item in @to_close_stack. + def close + tag = @to_close_stack.pop + if tag == '_haml_end_block' + close_block + else + close_tag tag + end end - def render_comment(line) - add "" + # Puts a line in @precompiled that will add the closing tag of + # the most recently opened tag. + def close_tag(tag) + @tabulation -= 1 + @precompiled << "_hamlout.close_tag(#{tag.dump}, #{@tabulation})\n" end - def template_eval(args) - !@suppress_eval ? @scope_object.instance_eval(args) : "" + # Closes a Ruby block. + def close_block + push_silent "end" + 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, index) + line.scan(/[%]([-_a-z1-9]+)([-_a-z\.\#]*)(\{.*\})?(\[.*\])?([=\/\~]?)?(.*)?/) do |tag_name, attributes, attributes_hash, object_ref, action, value| + value = value.to_s + + case action + when '/' + atomic = true + when '=', '~' + flattened = (action == '~') + parse = true + else + value = value.strip + end + + value_exists = !value.empty? + attributes_hash = "nil" unless attributes_hash + object_ref = "nil" unless object_ref + + @precompiled << "_hamlout.open_tag(#{tag_name.inspect}, #{@tabulation}, #{atomic.inspect}, #{value_exists.inspect}, #{attributes.inspect}, #{attributes_hash}, #{object_ref})\n" + + unless atomic + @to_close_stack.push tag_name + @tabulation += 1 + + if value_exists + if parse + push_script(value, flattened, index) + else + push_text(value) + end + close + end + end + end + end + + # Renders a line that creates an XHTML tag and has an implicit div because of + # . or #. + def render_div(line, index) + render_tag('%div' + line, index) + end + + # Renders an XHTML comment. + def render_comment(line) + push_text "" end end end diff --git a/lib/haml/helpers.rb b/lib/haml/helpers.rb index f2d53ad3..48eece95 100644 --- a/lib/haml/helpers.rb +++ b/lib/haml/helpers.rb @@ -1,11 +1,18 @@ module Haml + # This module contains various helpful methods to make it easier to do + # various tasks. Haml::Helpers is automatically included in the context + # that a HAML template is parsed in, so all these methods are at your + # disposal from within the template. module Helpers - # Flatten will take any string, find all the endlines (via \n) - # and convert them to html entities for endlines. + # Takes any string, finds all the endlines and converts them to + # html entities for endlines so they'll render correctly in + # whitespace-sensitive tags. def flatten(input) input.gsub(/\n/, ' ').gsub(/\r/, '') end + # Isolates the whitespace-sensitive tags in the string and uses flatten + # to convert any endlines inside them into html entities. def find_and_flatten(input) input.scan(/<(textarea|code|pre)[^>]*>(.*?)<\/\1>/im).each do |thing| input = input.gsub(thing[1], flatten(thing[1])) @@ -13,19 +20,15 @@ module Haml input end - def tabs(count) - ' ' * count - end - + # Counts the tabulation of a line. Mostly for internal use. def count_soft_tabs(line) line.index(/[^ ]/) ? [line.index(/[^ ]/)/2, line.strip] : [] end - # List_for is a really nifty little helper that helps - # cleanup your code. Basically, give it an array of - # objects, and then pass in a block that tells how - # what to put out, and you will get each block item - # in rows of
  • tags. + # Takes an array and a block and iterates the array, + # yielding each element to the block and putting the + # result into li elements, creating a list + # of the results of the block. For example: # # For instance: # list_of([['hello'], ['yall']]) { |i| i[0] } @@ -36,7 +39,7 @@ module Haml #
  • hello
  • #
  • yall
  • # - def list_of(array) + def list_of(array) # :yields: item (array.collect { |i| "
  • #{yield(i)}
  • " }).join("\n") end end diff --git a/lib/haml/template.rb b/lib/haml/template.rb index 07f8e4f7..0f8d4536 100644 --- a/lib/haml/template.rb +++ b/lib/haml/template.rb @@ -1,4 +1,6 @@ require File.dirname(__FILE__) + '/engine' +require 'active_support' +require 'action_view' module Haml class Template @@ -29,4 +31,16 @@ module Haml Haml::Engine.new(template, :scope_object => @view).to_html end end -end \ No newline at end of file +end + +module ActionView + class Base + attr :haml_filename, true + + alias haml_old_render_file render_file + def render_file(template_path, use_full_path = true, local_assigns = {}) + @haml_filename = File.basename(template_path) + haml_old_render_file(template_path, use_full_path, local_assigns) + end + end +end diff --git a/test/benchmark.rb b/test/benchmark.rb index 5e82858d..cce35e83 100644 --- a/test/benchmark.rb +++ b/test/benchmark.rb @@ -1,14 +1,58 @@ -require 'rubygems' -require 'action_view' - require File.dirname(__FILE__) + '/../lib/haml/template' +require 'rubygems' +require 'active_support' +require 'action_view' +require 'benchmark' +require 'stringio' -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("erb: ") { RUNS.times { @base.render "rhtml/standard" } } +module Haml + class Benchmarker + + # Creates a new benchmarker that looks for templates in the base + # directory. + def initialize(base = File.dirname(__FILE__)) + ActionView::Base.register_template_handler("haml", Haml::Template) + unless base.class == ActionView::Base + @base = ActionView::Base.new(base) + else + @base = base + end + end + + # Benchmarks HAML against ERb. If template_name is specified, + # looks for a haml template in ./templates and an rhtml template in + # ./rhtml with the name template_name. Otherwise, uses + # haml_template and rhtml_template as the location of + # the templates. + # + # Returns the results of the benchmarking as a string. + # + # :call-seq: + # benchmark(runs = 100, template_name = 'standard') + # benchmark(runs = 100, haml_template, rhtml_template) + # + def benchmark(runs = 100, template_name = 'standard', other_template = nil) + if other_template.nil? + haml_template = "templates/#{template_name}" + rhtml_template = "rhtml/#{template_name}" + else + haml_template = template_name + rhtml_template = other_template + end + + old_stdout = $stdout + $stdout = StringIO.new + + Benchmark.bmbm do |b| + b.report("haml:") { runs.times { @base.render haml_template } } + b.report("erb:") { runs.times { @base.render rhtml_template } } + end + + $stdout.pos = 0 + to_return = $stdout.read + $stdout = old_stdout + + to_return + end + end end diff --git a/test/engine_test.rb b/test/engine_test.rb index 0b54bbee..927c5019 100644 --- a/test/engine_test.rb +++ b/test/engine_test.rb @@ -1,3 +1,5 @@ +#!/usr/bin/env ruby + require 'test/unit' require File.dirname(__FILE__) + '/../lib/haml/engine' diff --git a/test/helper_test.rb b/test/helper_test.rb index 3b9bf548..c06fbea2 100644 --- a/test/helper_test.rb +++ b/test/helper_test.rb @@ -1,3 +1,5 @@ +#!/usr/bin/env ruby + require 'test/unit' require File.dirname(__FILE__) + '/../lib/haml/helpers' @@ -24,11 +26,6 @@ class HelperTest < Test::Unit::TestCase "
    Two
    lines
    \n
    a
    b
    c
    ") end - def test_tabs_should_render_correctly - assert_equal(" ", tabs(1)) - assert_equal(" ", tabs(5)) - end - 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})) diff --git a/test/profile.rb b/test/profile.rb new file mode 100644 index 00000000..95ec5846 --- /dev/null +++ b/test/profile.rb @@ -0,0 +1,45 @@ +require File.dirname(__FILE__) + '/../lib/haml/template' +require 'rubygems' +require 'active_support' +require 'action_view' +require 'profiler' +require 'stringio' + +module Haml + # A profiler for HAML, mostly for development use. This simply implements + # the Ruby profiler for profiling HAML code. + class Profiler + + # Creates a new profiler that looks for templates in the base + # directory. + def initialize(base = File.join(File.dirname(__FILE__), 'templates')) + ActionView::Base.register_template_handler("haml", Haml::Template) + unless base.class == ActionView::Base + @base = ActionView::Base.new(base) + else + @base = base + end + end + + # Profiles HAML on the given template with the given number of runs. + # The template name shouldn't have a file extension; this will + # automatically look for a HAML template. + # + # Returns the results of the profiling as a string. + def profile(runs = 100, template_name = 'standard') + # Runs the profiler, collects information + Profiler__::start_profile + runs.times { @base.render template_name } + Profiler__::stop_profile + + # Outputs information to a StringIO, returns result + io = StringIO.new + Profiler__::print_profile(io) + io.pos = 0 + result = io.read + io.close + return result + end + + end +end diff --git a/test/results/helpers.xhtml b/test/results/helpers.xhtml index 29099e37..c477300f 100644 --- a/test/results/helpers.xhtml +++ b/test/results/helpers.xhtml @@ -1 +1,25 @@ &&&&&&&&&&& +
    +

    Title

    +

    + Woah this is really crazy + I mean wow, + man. +

    +
    +
    +

    Title

    +

    + Woah this is really crazy + I mean wow, + man. +

    +
    +
    +

    Title

    +

    + Woah this is really crazy + I mean wow, + man. +

    +
    diff --git a/test/results/helpful.xhtml b/test/results/helpful.xhtml index 7be92c36..da91467d 100644 --- a/test/results/helpful.xhtml +++ b/test/results/helpful.xhtml @@ -2,4 +2,4 @@

    Hello

    World
    -
    boo
    \ No newline at end of file +
    boo
    diff --git a/test/results/silent_script.xhtml b/test/results/silent_script.xhtml new file mode 100644 index 00000000..3b2892ab --- /dev/null +++ b/test/results/silent_script.xhtml @@ -0,0 +1,58 @@ +
    +

    I can count!

    + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + 15 + 16 + 17 + 18 + 19 + 20 +

    I know my ABCs!

    + +

    I can catch errors!

    + Oh no! "uninitialized constant Foo" happened! +

    + "false" is: + false +

    +
    diff --git a/test/results/standard.xhtml b/test/results/standard.xhtml index be70cf25..d2634d68 100644 --- a/test/results/standard.xhtml +++ b/test/results/standard.xhtml @@ -2,7 +2,7 @@ Hampton Catlin Is Totally Awesome - + @@ -19,6 +19,17 @@ PipesIgnored|PipesIgnored|PipesIgnored| 1|2|3

    +
    + this shouldn't evaluate but now it should! +
    +
    with this text
    <%= " Quotes should be loved! Just like people!" %>
    + Wow. +

    + <%= "Holy cow " + + "multiline " + + "tags! " + + "A pipe (|) even!" %> + <%= [1, 2, 3].collect { |n| "PipesIgnored|" } %> + <%= [1, 2, 3].collect { |n| + n.to_s + }.join("|") %> +

    +
    + <% foo = String.new + foo << "this" + foo << " shouldn't" + foo << " evaluate" %> + <%= foo + "but now it should!" %> + <%# Woah crap a comment! %> +
    +