Creating the next set of branches.

That is, the buffered branch is being removed and replaced with the edge 
branch.

This marks the beginning of the edge branch.



git-svn-id: svn://hamptoncatlin.com/haml/branches/edge@80 7063305b-7217-0410-af8c-cdc13e5119b9
This commit is contained in:
hcatlin 2006-10-14 23:50:07 +00:00
parent 4266688e30
commit 89a10b9dfe
19 changed files with 1132 additions and 227 deletions

346
README
View File

@ -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
<tt><element></element></tt> use the <tt>%</tt> 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
<tt><element></element></tt>. For example:
%one
%meta{:content => 'something'}/
%two
%three Hey there
is compiled to:
<one>
<two>
<meta content='something' />
<three>Hey there</three>
</two>
</one>
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/ # => <br />
%meta{:http-equiv => 'Content-Type', :content => 'text/html'}/
# => <meta http-equiv='Content-Type' content='text/html' />
==== {}
HTML div elements are assumed when no <tt>%tag</tt> is present and the line is
preceeded by either the <tt>#</tt> or the <tt>.</tt> characters. This convention
uses familiar CSS semantics: <tt>#</tt> denotes the id of the element,
<tt>.</tt> 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:
<head name="doc_head">
<script src='javascripts/script_9' type='text/javascript'>
</script>
</head>
==== []
<div id='collection'>
<div class='item'>Broken record album</div>
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:
<div class="crazy_user" id="crazy_user_15">
<bar class="fixnum" id="fixnum_581" />
Hello!
</div>
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 <tt>#</tt> or the
<tt>.</tt> 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:
<br />
<meta http-equiv='Content-Type' content='text/html' />
==== . and #
The period and pound sign are borrowed from CSS and used as shortcuts to specify the
<tt>class</tt> and <tt>id</tt> 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:
<div id='things'>
<span id='rice'>Chicken Fried</span>
<p class='beans'>The magical fruit</p>
<p class='beans' food='true'>The magical fruit</p>
<h1 class='class' id='id'>La La La</h1>
</div>
=== 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 <tt>.</tt> or <tt>#</tt> 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:
<div id='collection'>
<div class='item'>Broken record album</div>
<div class='description'>What a cool item!</div>
</div>
==== = and ~
<tt>=</tt> and <tt>~</tt> 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 <tt>=</tt> and <tt>~</tt> 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:
<p>
hello
</p>
<h1>
3
</h1>
=== 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:
<gee>
<whiz>
Wow this is cool!
</whiz>
</gee>
==== !!!
When describing XHTML documents with Haml, you can have a document type
generated automatically by including the characters <tt>!!!</tt> as the first
line in your document. Example:
@ -139,11 +255,158 @@ is compiled to:
<p>Sign my guestbook</p>
</body>
</html>
==== /
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:
<billabong>
<!-- This is the billabong element -->
I like billabongs!
</billabong>
==== |
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 <tt>|</tt> 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:
<p>
hi there reader!
yo
</p>
==== ~
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 <pre> this is \n</pre> crazy"
%foo2
~ "Woah <pre> this is \n</pre> crazy"
is compiled to:
<foo>
Woah <pre> this is
</pre> crazy
</foo>
<foo2>
Woah <pre> this is &#x000A;</pre> crazy
</foo2>
==== -
The hyphen character makes the text following it into "silent script", or
Ruby script that is evaluated, but not output.
<b>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.</b>
For example:
- foo = "hello"
- foo << " there"
- foo << " you!"
%p= foo
is compiled to:
<p>
hello there you!
</p>
===== 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:
<p>
42
</p>
<p>
43
</p>
<p>
44
</p>
<p>
45
</p>
<p>
46
</p>
Another example:
%p
- case 2
- when 1
= "1!"
- when 2
= "2?"
- when 3
= "3."
is compiled to:
<p>
2?
</p>
== 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:
</html>
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 <tt>=</tt> 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:
<div id='content'>
<div class='title'>
@ -206,8 +466,6 @@ is be compiled to:
</div>
---
Copyright (c) 2006 Hampton Catlin
Licensed under the MIT License

View File

@ -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 = <<END
Benchmark HAML against ERb.
TIMES=n sets the number of runs. Defaults to 100.
END
desc temp_desc.chomp
task :benchmark do
puts '-'*51, "+ Benchmark: HAML vs. ERb", '-'*51
require 'test/benchmark'
puts '-'*51, "Benchmark: HAML vs. ERb", '-'*51
puts "Running benchmark #{ENV['TIMES']} times..." if ENV['TIMES']
puts `ruby test/benchmark.rb #{ENV['TIMES']}`
args = []
args.push ENV['TIMES'].to_i if ENV['TIMES']
benchmarker = Haml::Benchmarker.new
puts benchmarker.benchmark(*args)
puts '-'*51
end
desc 'Generate documentation for the haml plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
# ----- Documentation -----
rdoc_task = Proc.new do |rdoc|
rdoc.title = 'Haml'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('lib/**/*.rb')
rdoc.rdoc_files.exclude('lib/haml/buffer.rb')
end
task :rcov do
`rcov test/*.rb`
Rake::RDocTask.new do |rdoc|
rdoc_task.call(rdoc)
rdoc.rdoc_dir = 'rdoc'
end
Rake::RDocTask.new(:rdoc_devel) do |rdoc|
rdoc_task.call(rdoc)
rdoc.rdoc_dir = 'rdoc_devel'
rdoc.options << '--all'
rdoc.rdoc_files.include('test/*.rb')
rdoc.rdoc_files = Rake::FileList.new(*rdoc.rdoc_files.to_a)
rdoc.rdoc_files.include('lib/haml/buffer.rb')
end
# ----- Coverage -----
unless not_loaded.include? 'rcov/rcovtask'
Rcov::RcovTask.new do |t|
t.libs << "test"
t.test_files = FileList['test/*_test.rb']
t.verbose = true
end
end
# ----- Profiling -----
temp_desc = <<END
Run a profile of HAML.
TIMES=n sets the number of runs. Defaults to 100.
FILE=n sets the file to profile. Defaults to 'standard'.
END
desc temp_desc.chomp
task :profile do
require 'test/profile'
puts '-'*51, "Profiling HAML::Template", '-'*51
args = []
args.push ENV['TIMES'].to_i if ENV['TIMES']
args.push ENV['FILE'] if ENV['FILE']
profiler = Haml::Profiler.new
res = profiler.profile(*args)
puts res
puts '-'*51
end

140
lib/haml/buffer.rb Normal file
View File

@ -0,0 +1,140 @@
module Haml
# This class is used only internally. It holds the buffer of XHTML that
# is eventually output by Haml::Engine's to_html method. It's called
# from within the precompiled code, and helps reduce the amount of
# processing done within instance_eval'd code.
class Buffer
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
# 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 << "</#{name}>\n"
@one_liner_pending = false
else
push_text("</#{name}>", tabulation)
end
end
private
# Gets <tt>count</tt> tabs. Mostly for internal use.
def tabs(count)
' ' * count
end
# Iterates through the classes and ids supplied through <tt>.</tt>
# and <tt>#</tt> 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

View File

@ -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. <tt><p>Hello world</p></tt>
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 <tt>%p no!</tt>, because <tt>else</tt>
# 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:
#
# [<tt>scope_object</tt>] 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.
# [<tt>suppress_eval</tt>] Whether or not attribute hashes and Ruby scripts
# designated by <tt>=</tt> or <tt>~</tt> 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 << %|<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n|
# Processes a single line of HAML. <tt>count</tt> 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 <tt>@precompiled</tt>.
def process_line(count, line, index)
if line.lstrip[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_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, <tt>count</tt> 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 <tt>@precompiled</tt>, a string buffer of Ruby code, and
# evaluates it in the context of <tt>@scope_object</tt>, after preparing
# <tt>@scope_object</tt>. The code in <tt>@precompiled</tt> populates
# <tt>@buffer</tt> 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 = <<END
_hamlout = @haml_stack[-1]
_erbout = _hamlout.buffer
#{@precompiled}
END
# Evaluate the buffer in the context of the scope object
begin
@scope_object.instance_eval @precompiled
rescue Exception => 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 "</#{@to_close_queue.pop}>"
end
def one_line_tag(name, value, attributes = {})
add "<#{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 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 <tt>text</tt> in the context of <tt>@scope_object</tt>, 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. <tt><hello /></tt>
def atomic_tag(name, attributes = {})
add "<#{name.to_s}#{build_attributes(attributes)} />"
# Adds <tt>text</tt> to <tt>@buffer</tt> 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 <tt>text</tt> to be evaluated in the context of
# <tt>@scope_object</tt> and the result to be added to <tt>@buffer</tt>.
#
# If <tt>flattened</tt> is true, Haml::Helpers#find_and_flatten is run on
# the result before it is added to <tt>@buffer</tt>
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 <tt>@to_close_stack</tt>.
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 "<!-- #{line[1..line.length].strip} -->"
# Puts a line in <tt>@precompiled</tt> 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 <tt>@precompiled</tt>.
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
# <tt>.</tt> or <tt>#</tt>.
def render_div(line, index)
render_tag('%div' + line, index)
end
# Renders an XHTML comment.
def render_comment(line)
push_text "<!-- #{line[1..line.length].strip} -->"
end
end
end

View File

@ -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/, '&#x000A;').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 <li> tags.
# Takes an array and a block and iterates the array,
# yielding each element to the block and putting the
# result into <tt>li</tt> 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
# <li>hello</li>
# <li>yall</li>
#
def list_of(array)
def list_of(array) # :yields: item
(array.collect { |i| "<li>#{yield(i)}</li>" }).join("\n")
end
end

View File

@ -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
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

View File

@ -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 <tt>template_name</tt> is specified,
# looks for a haml template in ./templates and an rhtml template in
# ./rhtml with the name <tt>template_name</tt>. Otherwise, uses
# <tt>haml_template</tt> and <tt>rhtml_template</tt> 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

View File

@ -1,3 +1,5 @@
#!/usr/bin/env ruby
require 'test/unit'
require File.dirname(__FILE__) + '/../lib/haml/engine'

View File

@ -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
"<pre>Two&#x000A;lines</pre>\n<pre>a&#x000A;b&#x000A;c</pre>")
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("<li>1</li>\n<li>2</li>", (list_of([1, 2]) { |i| i.to_s}))
assert_equal("<li>1</li>", (list_of([[1]]) { |i| i.first}))

45
test/profile.rb Normal file
View File

@ -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

View File

@ -1 +1,25 @@
&amp;&amp;&amp;&amp;&amp;&amp;&amp;&amp;&amp;&amp;&amp;
<div>
<p class='title'>Title</p>
<p class='text'>
Woah this is really crazy
I mean wow,
man.
</p>
</div>
<div>
<p class='title'>Title</p>
<p class='text'>
Woah this is really crazy
I mean wow,
man.
</p>
</div>
<div>
<p class='title'>Title</p>
<p class='text'>
Woah this is really crazy
I mean wow,
man.
</p>
</div>

View File

@ -2,4 +2,4 @@
<h1>Hello</h1>
<div>World</div>
</div>
<div class='article full' id='article_1'>boo</div>
<div class='article full' id='article_1'>boo</div>

View File

@ -0,0 +1,58 @@
<div>
<h1>I can count!</h1>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<h1>I know my ABCs!</h1>
<ul>
<li>a</li>
<li>b</li>
<li>c</li>
<li>d</li>
<li>e</li>
<li>f</li>
<li>g</li>
<li>h</li>
<li>i</li>
<li>j</li>
<li>k</li>
<li>l</li>
<li>m</li>
<li>n</li>
<li>o</li>
<li>p</li>
<li>q</li>
<li>r</li>
<li>s</li>
<li>t</li>
<li>u</li>
<li>v</li>
<li>w</li>
<li>x</li>
<li>y</li>
<li>z</li>
</ul>
<h1>I can catch errors!</h1>
Oh no! "uninitialized constant Foo" happened!
<p>
"false" is:
false
</p>
</div>

View File

@ -2,7 +2,7 @@
<html xml-lang='en-US'>
<head>
<title>Hampton Catlin Is Totally Awesome</title>
<meta content='text/html; charset=utf-8' http-equiv='Content-Type' />
<meta http-equiv='Content-Type' content='text/html; charset=utf-8' />
</head>
<body>
<!-- You're In my house now! -->
@ -19,6 +19,17 @@
PipesIgnored|PipesIgnored|PipesIgnored|
1|2|3
</p>
<div class='silent'>
this shouldn't evaluate but now it should!
</div>
<ul class='really cool'>
<li>a</li>
<li>b</li>
<li>c</li>
<li>d</li>
<li>e</li>
<li>f</li>
</ul>
<div class='of_divs_with_underscore' id='combo'>with this text</div>
<div class='footer'>
<strong class='shout'>

View File

@ -1,7 +1,7 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xml-lang='en-US'>
<head>
<title><%= "Hampton Catlin Is Totally Awesome" %></title>
<title>Hampton Catlin Is Totally Awesome</title>
<meta content='text/html; charset=utf-8' http-equiv='Content-Type' />
</head>
<body>
@ -11,8 +11,32 @@
Fantastic! This should be multi-line output
The question is if this would translate! Ahah!
<%= 1 + 9 + 8 + 2 %>
<%# numbers should work and this should be ignored %>
</div>
<div id='body'><%= " Quotes should be loved! Just like people!" %></div>
Wow.
<p>
<%= "Holy cow " +
"multiline " +
"tags! " +
"A pipe (|) even!" %>
<%= [1, 2, 3].collect { |n| "PipesIgnored|" } %>
<%= [1, 2, 3].collect { |n|
n.to_s
}.join("|") %>
</p>
<div class='silent'>
<% foo = String.new
foo << "this"
foo << " shouldn't"
foo << " evaluate" %>
<%= foo + "but now it should!" %>
<%# Woah crap a comment! %>
</div>
<ul class='really cool'>
<% ('a'..'f').each do |a|%>
<li><%= a %>
<% end %>
<div class='of_divs_with_underscore' id='combo'><%= @should_eval = "with this text" %></div>
<div class='footer'>
<strong class='shout'>

View File

@ -1,3 +1,5 @@
#!/usr/bin/env ruby
require 'test/unit'
require 'rubygems'
require 'active_support'
@ -7,6 +9,17 @@ require File.dirname(__FILE__) + '/../lib/haml/template'
require File.dirname(__FILE__) + '/mocks/article'
class TemplateTest < Test::Unit::TestCase
# These are specific lines of templates that, for one reason or
# another, might not be exactly equivalent to the pre-rendered
# version.
EXCEPTIONS = {
'standard' => [
# Line 4 has many attributes; because attributes aren't sorted,
# this can vary unpredictably.
4
]
}
def setup
ActionView::Base.register_template_handler("haml", Haml::Template)
@base = ActionView::Base.new(File.dirname(__FILE__) + "/../test/templates/")
@ -24,8 +37,15 @@ class TemplateTest < Test::Unit::TestCase
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)
load_result(name).split("\n").zip(@base.render(name).split("\n")).each_with_index do |pair, line|
if (EXCEPTIONS['name'].nil? || EXCEPTIONS['name'].include?(line))
if pair.first != pair.last
puts "\nWarning: line #{line} of template \"#{name}\" may have rendered incorrectly."
end
else
message = "template: #{name}\nline: #{line}"
assert_equal(pair.first, pair.last, message)
end
end
end
@ -34,7 +54,8 @@ class TemplateTest < Test::Unit::TestCase
end
def test_templates_should_render_correctly
%w{very_basic standard helpers whitespace_handling original_engine list helpful}.each do |template|
%w{very_basic standard helpers whitespace_handling
original_engine list helpful silent_script}.each do |template|
assert_renders_correctly template
end
end
@ -66,4 +87,30 @@ class TemplateTest < Test::Unit::TestCase
def test_template_renders_should_eval
assert_equal("2\n", render("= 1+1"))
end
def test_exceptions_should_work_correctly
template = <<END
%p
%h1 Hello!
= "lots of lines"
- raise "Oh no!"
%p
this is after the exception
%strong yes it is!
ho ho ho.
END
@base.haml_filename = "(test)"
begin
render(template.chomp)
rescue Exception => e
assert_equal("(test).haml:4", e.backtrace[0])
end
@base.haml_filename = nil
begin
render(template.chomp)
rescue Exception => e
assert_equal("(haml):4", e.backtrace[0])
end
end
end

View File

@ -1 +1,10 @@
= h("&&&&&&&&&&&") #this is an ActionView Helper... should load
= h("&&&&&&&&&&&") # This is an ActionView Helper... should load
- foo = capture do # This ActionView Helper is designed for ERB, but should work with HAML
%div
%p.title Title
%p.text
Woah this is really crazy
I mean wow,
man.
- 3.times do
= foo

View File

@ -0,0 +1,19 @@
%div
%h1 I can count!
- (1..20).each do |i|
= i
%h1 I know my ABCs!
%ul
- ('a'..'z').each do |i|
%li= i
%h1 I can catch errors!
- begin
- Foo.silly
- rescue NameError => e
= "Oh no! \"#{e}\" happened!"
%p
"false" is:
- if false
= "true"
- else
= "false"

View File

@ -21,6 +21,16 @@
= [1, 2, 3].collect { |n| |
n.to_s |
}.join("|") |
%div.silent
- foo = String.new
- foo << "this"
- foo << " shouldn't"
- foo << " evaluate"
= foo + " but now it should!"
-# Woah crap a comment!
%ul.really.cool
- ('a'..'f').each do |a|
%li= a
#combo.of_divs_with_underscore= @should_eval = "with this text"
.footer
%strong.shout= "This is a really long ruby quote. It should be loved and wrapped because its more than 50 characters. This value may change in the future and this test may look stupid. \nSo, I'm just making it *really* long. God, I hope this works"