master
Alinson S. Xavier 18 years ago
parent c5cb69f277
commit 3f8c9f0ae6

@ -0,0 +1,20 @@
Copyright (c) 2006-2007 Hampton Catlin
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -0,0 +1,248 @@
= Haml and Sass
Haml and Sass are templating engines
for the two most common types of documents on the web:
HTML and CSS, respectively.
They are designed to make it both easier and more pleasant
to code HTML and CSS documents,
by eliminating redundancy,
reflecting the underlying structure that the document represents,
and providing elegant, easily understandable, and powerful syntax.
== Using
There are several ways to use Haml and Sass.
They can be used as a plugins for Rails or Merb,
or embedded on their own in other applications.
The first step of all of these is to install the Haml gem:
gem install haml
To install Haml and Sass as a Rails plugin,
just run <tt>haml --rails path/to/rails/app</tt>
and both Haml and Sass will be installed.
Views with the <tt>.haml</tt> (or <tt>.html.haml</tt> for edge)
extension will automatically use Haml.
Sass is a little more complicated;
<tt>.sass</tt> files should be placed in public/stylesheets/sass,
where they'll be automatically compiled
to corresponding CSS files in public/stylesheets when needed
(the Sass template directory is customizable...
see the Sass module docs for details).
For Merb, <tt>.html.haml</tt> views will work without any further modification.
To enable Sass, you also need to add it add a dependency.
To do so, just add
dependency "haml"
to config/dependencies.rb.
Then it'll work just like it does in Rails.
To use Haml and Sass programatically,
check out the RDocs for the Haml and Sass modules.
== Formatting
=== Haml
The most basic element of Haml
is a shorthand for creating HTML tags:
%tagname{ :attr1 => 'value1', :attr2 => 'value2' } Contents
No end-tag is needed; Haml handles that automatically.
Adding <tt>class</tt> and <tt>id</tt> attributes is even easier.
Haml uses the same syntax as the CSS that styles the document:
%tagname#id.class
In fact, when you're using the <tt><div></tt> tag,
it becomes <em>even easier</em>.
Because <tt><div></tt> is such a common element,
a tag without a name defaults to a div. So
#foo Hello!
becomes
<div id='foo'>Hello!</div>
Haml uses indentation
to bring the individual elements to represent the HTML structure.
A tag's children are indented two spaces more than the parent tag.
Again, a closing tag is automatically added.
For example:
%ul
%li Salt
%li Pepper
becomes:
<ul>
<li>Salt</li>
<li>Pepper</li>
</ul>
You can also put plain text as a child of an element:
%p
Hello,
World!
It's even possible to embed Ruby code into Haml documents.
An equals sign, <tt>=</tt>, will output the result of the code.
A hyphen, <tt>-</tt>, will run the code but not output the result.
You can even use control statements
like <tt>if</tt> and <tt>while</tt>:
%p
Date/Time:
- now = DateTime.now
%strong= now
- if now > DateTime.parse("December 31, 2006")
= "Happy new " + "year!"
Haml provides far more tools than those presented here.
Check out the reference documentation in the Haml module.
=== Sass
At its most basic,
Sass is just another way of writing CSS.
Although it's very much like normal CSS,
the basic syntax offers a few helpful features:
tabulation (using *two spaces*)
indicates the attributes in a rule,
rather than non-DRY brackets;
and newlines indicate the end of an attribute,
rather than a semicolon.
For example:
#main
:background-color #f00
:width 98%
becomes:
#main {
background-color: #f00;
width: 98% }
However, Sass provides much more than a way to make CSS look nice.
In CSS, it's important to have accurate selectors,
so your styles don't just apply to everything.
However, in order to do this,
you need to use nested element selectors.
These get very ugly very quickly.
I'm sure everyone's had to write something like
"#main .sidebar .top p h1 a",
followed by
"#main .sidebar .top p h1 a:visited" and
"#main .sidebar .top p h1 a:hover".
Well, Sass gets rid of that.
Like Haml, it uses indentation to indicate the structure of the document.
So, what was:
#main {
width: 90%;
}
#main p {
border-style: solid;
border-width: 1px;
border-color: #00f;
}
#main p a {
text-decoration: none;
font-weight: bold;
}
#main p a:hover {
text-decoration: underline;
}
becomes:
#main
:width 90%
p
:border-style solid
:border-width 1px
:border-color #00f
a
:text-decoration none
:font-weight bold
a:hover
:text-decoration underline
Pretty nice, no? Well, it gets better.
One of the main complaints against CSS is that it doesn't allow constants.
What if have a color or a width you re-use all the time?
In CSS, you just have to re-type it each time,
which is a nightmare when you decide to change it later.
Not so for Sass!
You can use the "!" character to set constants.
Then, if you put "=" after your attribute name,
you can set it to a constant.
For example:
!note_bg= #55aaff
#main
:width 70%
.note
:background-color= !note_bg
p
:width 5em
:background-color= !note_bg
becomes:
#main {
width: 70%; }
#main .note {
background-color: #55aaff; }
#main p {
width: 5em;
background-color: #55aaff; }
You can even do simple arithmetic operations with constants,
adding numbers and even colors together:
!main_bg= #46ar12
!main_width= 40em
#main
:background-color= !main_bg
:width= !main_width
.sidebar
:background-color= !main_bg + #333333
:width= !main_width - 25em
becomes:
#main {
background-color: #46a312;
width: 40em; }
#main .sidebar {
background-color: #79d645;
width: 15em; }
A comprehensive list of features is in
the documentation for the Sass module.
== Authors
Haml and Sass are designed by Hampton Catlin (hcatlin).
Help with the Ruby On Rails implementation and much of the documentation
by Jeff Hardy (packagethief).
Nathan Weizenbaum (Nex3) contributed the buffered-engine code to Haml,
along with many other enhancements
(including the silent-line syntax: "-").
He continues to actively work on both Haml and Sass.
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*.

@ -0,0 +1,174 @@
require 'rubygems'
require 'rake'
volatile_requires = ['rcov/rcovtask']
not_loaded = []
volatile_requires.each do |file|
begin
require file
rescue LoadError
not_loaded.push file
end
end
# ----- 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
require 'test/benchmark'
puts '-'*51, "Benchmark: Haml vs. ERb", '-'*51
puts "Running benchmark #{ENV['TIMES']} times..." if ENV['TIMES']
times = ENV['TIMES'].to_i if ENV['TIMES']
benchmarker = Haml::Benchmarker.new
puts benchmarker.benchmark(times || 100)
puts '-'*51
end
# Benchmarking gets screwed up if some other tasks have been
# initialized.
unless ARGV[0] == 'benchmark'
# ----- Default: Testing ------
desc 'Default: run unit tests.'
task :default => :test
require 'rake/testtask'
desc 'Test the Haml plugin'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
# ----- Packaging -----
require 'rake/gempackagetask'
spec = Gem::Specification.new do |spec|
spec.name = 'haml'
spec.summary = "An elegant, structured XHTML/XML templating engine.\nComes with Sass, a similar CSS templating engine."
spec.version = File.read('VERSION').strip
spec.author = 'Hampton Catlin'
spec.email = 'haml@googlegroups.com'
spec.description = <<-END
Haml (HTML Abstraction Markup Language) is a layer on top of XHTML or XML
that's designed to express the structure of XHTML or XML documents
in a non-repetitive, elegant, easy way,
using indentation rather than closing tags
and allowing Ruby to be embedded with ease.
It was originally envisioned as a plugin for Ruby on Rails,
but it can function as a stand-alone templating engine.
END
#'
readmes = FileList.new('*') do |list|
list.exclude(/[a-z]/)
list.exclude('TODO')
end.to_a
spec.executables = ['haml', 'html2haml', 'sass']
spec.files = FileList['lib/**/*', 'bin/*', 'test/**/*', 'Rakefile', 'init.rb'].to_a + readmes
spec.autorequire = ['haml', 'sass']
spec.homepage = 'http://haml.hamptoncatlin.com/'
spec.has_rdoc = true
spec.extra_rdoc_files = readmes
spec.rdoc_options += [
'--title', 'Haml',
'--main', 'README',
'--exclude', 'lib/haml/buffer.rb',
'--line-numbers',
'--inline-source'
]
spec.test_files = FileList['test/**/*_test.rb'].to_a
end
Rake::GemPackageTask.new(spec) do |pkg|
pkg.need_zip = true
pkg.need_tar_gz = true
pkg.need_tar_bz2 = true
end
task :install => [:package] do
sh %{gem install --no-ri pkg/haml-#{File.read('VERSION').strip}}
end
# ----- Documentation -----
require 'rake/rdoctask'
rdoc_task = Proc.new do |rdoc|
rdoc.title = 'Haml/Sass'
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')
rdoc.rdoc_files.exclude('lib/haml/util.rb')
rdoc.rdoc_files.exclude('lib/sass/tree/*')
end
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')
# Get rid of exclusion rules
rdoc.rdoc_files = Rake::FileList.new(*rdoc.rdoc_files.to_a)
rdoc.rdoc_files.include('lib/haml/buffer.rb')
rdoc.rdoc_files.include('lib/sass/tree/*')
end
# ----- Coverage -----
unless not_loaded.include? 'rcov/rcovtask'
Rcov::RcovTask.new do |t|
t.libs << "test"
t.test_files = FileList['test/**/*_test.rb']
t.rcov_opts << '-x' << '"^\/"'
if ENV['NON_NATIVE']
t.rcov_opts << "--no-rcovrt"
end
t.verbose = true
end
end
# ----- Profiling -----
temp_desc = <<-END
Run a profile of haml.
ENGINE=str sets the engine to be profiled (Haml or Sass).
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'
engine = ENV['ENGINE'] && ENV['ENGINE'].downcase == 'sass' ? Sass : Haml
puts '-'*51, "Profiling #{engine}", '-'*51
args = []
args.push ENV['TIMES'].to_i if ENV['TIMES']
args.push ENV['FILE'] if ENV['FILE']
profiler = engine::Profiler.new
res = profiler.profile(*args)
puts res
puts '-'*51
end
end

@ -0,0 +1,7 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../lib/haml'
require 'haml/exec'
opts = Haml::Exec::CSS2Sass.new(ARGV)
opts.parse!

@ -0,0 +1,8 @@
#!/usr/bin/env ruby
# The command line Haml parser.
require File.dirname(__FILE__) + '/../lib/haml'
require 'haml/exec'
opts = Haml::Exec::Haml.new(ARGV)
opts.parse!

@ -0,0 +1,7 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../lib/haml'
require 'haml/exec'
opts = Haml::Exec::HTML2Haml.new(ARGV)
opts.parse!

@ -0,0 +1,8 @@
#!/usr/bin/env ruby
# The command line Sass parser.
require File.dirname(__FILE__) + '/../lib/haml'
require 'haml/exec'
opts = Haml::Exec::Sass.new(ARGV)
opts.parse!

@ -0,0 +1,7 @@
require 'haml'
require 'haml/template'
require 'sass'
require 'sass/plugin'
ActionView::Base.register_template_handler('haml', Haml::Template)
Sass::Plugin.update_stylesheets

@ -0,0 +1,708 @@
dir = File.dirname(__FILE__)
$LOAD_PATH << dir unless $LOAD_PATH.include?(dir)
# = Haml (XHTML Abstraction Markup Language)
#
# Haml is a markup language
# that's used to cleanly and simply describe the XHTML of any web document,
# without the use of inline code.
# Haml functions as a replacement
# for inline page templating systems such as PHP, ERB, and ASP.
# However, Haml avoids the need for explicitly coding XHTML into the template,
# because it is actually an abstract description of the XHTML,
# with some code to generate dynamic content.
#
# == Features
#
# * Whitespace active
# * Well-formatted markup
# * DRY
# * Follows CSS conventions
# * Integrates Ruby code
# * Implements Rails templates with the .haml extension
#
# == Using Haml
#
# Haml can be used in two ways:
# as a plugin for Ruby on Rails,
# and as a standalone Ruby module.
#
# Sass can be used in several ways:
# As a template engine for Ruby on Rails or Merb,
# or as a standalone engine.
# The first step for all of these is to install the Haml gem:
#
# gem install haml
#
# To enable it as a Rails plugin,
# then run
#
# haml --rails path/to/rails/app
#
# Haml is enabled in Merb by default,
# so Merb users don't have to do anything more.
#
# Once it's installed, all view files with the ".haml" extension
# (or ".html.haml" for Merb or edge Rails)
# will be compiled using Haml.
#
# You can access instance variables in Haml templates
# the same way you do in ERb templates.
# Helper methods are also available in Haml templates.
# For example (this example uses Rails, but the principle for Merb is the same):
#
# # file: app/controllers/movies_controller.rb
#
# class MoviesController < ApplicationController
# def index
# @title = "Teen Wolf"
# end
# end
#
# -# file: app/views/movies/index.haml
#
# #content
# .title
# %h1= @title
# = link_to 'Home', home_url
#
# may be compiled to:
#
# <div id='content'>
# <div class='title'>
# <h1>Teen Wolf</h1>
# <a href='/'>Home</a>
# </div>
# </div>
#
# === Ruby Module
#
# Haml can also be used completely separately from Rails and ActionView.
# To do this, install the gem with RubyGems:
#
# gem install haml
#
# You can then use it by including the "haml" gem in Ruby code,
# and using Haml::Engine like so:
#
# engine = Haml::Engine.new("%p Haml code!")
# engine.render #=> "<p>Haml code!</p>\n"
#
# == Characters with meaning to Haml
#
# 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.
#
# ==== %
#
#
# The percent character 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
# %two
# %three Hey there
#
# is compiled to:
#
# <one>
# <two>
# <three>Hey there</three>
# </two>
# </one>
#
# Any string is a valid element name;
# Haml will automatically generate opening and closing tags for any element.
#
# ==== {}
#
# 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 and local variables may be used.
# Quote characters within the attribute
# will be replaced by appropriate escape sequences.
# The hash is placed after the tag is defined.
# For example:
#
# %head{ :name => "doc_head" }
# %script{ 'type' => "text/" + "javascript",
# :src => "javascripts/script_#{2 + 7}" }
#
# is compiled to:
#
# <head name="doc_head">
# <script src='javascripts/script_9' type='text/javascript'>
# </script>
# </head>
#
# ==== []
#
# Square brackets follow a tag definition 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>
#
# This is based off of DHH's SimplyHelpful syntax,
# as presented at RailsConf Europe 2006.
#
# ==== /
#
# 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' />
#
# Some tags are automatically closed, as long as they have no content.
# +meta+, +img+, +link+, +script+, +br+, and +hr+ tags are closed by default.
# This list can be customized by setting the <tt>:autoclose</tt> option (see below).
# For example:
#
# %br
# %meta{'http-equiv' => 'Content-Type', :content => 'text/html'}
#
# is also compiled to:
#
# <br />
# <meta http-equiv='Content-Type' content='text/html' />
#
# ==== . and #
#
# The period and pound sign are borrowed from CSS.
# They are used as shortcuts to specify the <tt>class</tt>
# and <tt>id</tt> attributes of an element, respectively.
# Multiple class names can be specified in a similar way to CSS,
# by chaining the class names together with periods.
# They are placed immediately after the tag and before an attributes hash.
# For example:
#
# %div#things
# %span#rice Chicken Fried
# %p.beans{ :food => 'true' } The magical fruit
# %h1.class.otherclass#id La La La
#
# is compiled to:
#
# <div id='things'>
# <span id='rice'>Chicken Fried</span>
# <p class='beans' food='true'>The magical fruit</p>
# <h1 class='class otherclass' id='id'>La La La</h1>
# </div>
#
# And,
#
# #content
# .articles
# .article.title
# Doogie Howser Comes Out
# .article.date
# 2006-11-05
# .article.entry
# Neil Patrick Harris would like to dispel any rumors that he is straight
#
# is compiled to:
#
# <div id="content">
# <div class="articles">
# <div class="article title">Doogie Howser Comes Out</div>
# <div class="article date">2006-11-05</div>
# <div class="article entry">
# Neil Patrick Harris would like to dispel any rumors that he is straight
# </div>
# </div>
# </div>
#
# ==== Implicit Div Elements
#
# 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'>
# <div class='description'>What a cool item!</div>
# </div>
# </div>
#
# ==== =
#
# <tt>=</tt> is placed at the end of a tag definition,
# after class, id, and attribute declarations.
# It's just a shortcut for inserting Ruby code into an element.
# It works the same as <tt>=</tt> without a tag:
# it inserts the result of the Ruby code into the template.
# However, if the result is short enough,
# it is displayed entirely on one line.
# For example:
#
# %p= "hello"
#
# is not quite the same as:
#
# %p
# = "hello"
#
# It's compiled to:
#
# <p>hello</p>
#
# === XHTML Helpers
#
# ==== No Special Character
#
# If no special character appears at the beginning of a line,
# the line 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 or XML prolog generated automatically
# by including the characters <tt>!!!</tt>.
# For example:
#
# !!! XML
# !!!
# %html
# %head
# %title Myspace
# %body
# %h1 I am the international space station
# %p Sign my guestbook
#
# is compiled to:
#
# <?xml version="1.0" encoding="utf-8" ?>
# <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
# <html>
# <head>
# <title>Myspace</title>
# </head>
# <body>
# <h1>I am the international space station</h1>
# <p>Sign my guestbook</p>
# </body>
# </html>
#
# You can also specify the version and type of XHTML after the <tt>!!!</tt>.
# XHTML 1.0 Strict, Transitional, and Frameset and XHTML 1.1 are supported.
# The default version is 1.0 and the default type is Transitional.
# For example:
#
# !!! 1.1
#
# is compiled to:
#
# <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
#
# and
#
# !!! Strict
#
# is compiled to:
#
# <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
#
# If you're not using the UTF-8 character set for your document,
# you can specify which encoding should appear
# in the XML prolog in a similar way.
# For example:
#
# !!! XML iso-8859-1
#
# is compiled to:
#
# <?xml version="1.0" encoding="iso-8859-1" ?>
#
# ==== /
#
# The forward slash character, when placed at the beginning of a line,
# wraps all text after it in an HTML comment.
# For example:
#
# %peanutbutterjelly
# / This is the peanutbutterjelly element
# I like sandwiches!
#
# is compiled to:
#
# <peanutbutterjelly>
# <!-- This is the peanutbutterjelly element -->
# I like sandwiches!
# </peanutbutterjelly>
#
# The forward slash can also wrap indented sections of code. For example:
#
# /
# %p This doesn't render...
# %div
# %h1 Because it's commented out!
#
# is compiled to:
#
# <!--
# <p>This doesn't render...</p>
# <div>
# <h1>Because it's commented out!</h1>
# </div>
# -->
#
# You can also use Internet Explorer conditional comments
# (about)[http://www.quirksmode.org/css/condcom.html]
# by enclosing the condition in square brackets after the <tt>/</tt>.
# For example:
#
# /[if IE]
# %a{ :href => 'http://www.mozilla.com/en-US/firefox/' }
# %h1 Get Firefox
#
# is compiled to:
#
# <!--[if IE]>
# <a href='http://www.mozilla.com/en-US/firefox/'>
# <h1>Get Firefox</h1>
# </a>
# <![endif]-->
#
# ==== \
#
# The backslash character escapes the first character of a line,
# allowing use of otherwise interpreted characters as plain text.
# For example:
#
# %title
# = @title
# \- MySite
#
# is compiled to:
#
# <title>
# MyPage
# - MySite
# </title>
#
# ==== |
#
# 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:
#
# <whoo>
# <hoo>
# I think this might get pretty long so I should probably make it multiline so it doesn't look awful.
# </hoo>
# </whoo>
#
# ==== :
#
# The colon character designates a filter.
# This allows you to pass an indented block of text as input
# to another filtering program and add the result to the output of Haml.
# The syntax is simply a colon followed by the name of the filter.
# For example,
#
# %p
# :markdown
# Textile
# =======
#
# Hello, *World*
#
# is compiled to
#
# <p>
# <h1>Textile</h1>
#
# <p>Hello, <em>World</em></p>
# </p>
#
# Haml has the following filters defined:
#
# [plain] Does not parse the filtered text.
# This is useful for large blocks of text without HTML tags,
# when you don't want lines starting with <tt>.</tt> or <tt>-</tt>
# to be parsed.
#
# [ruby] Parses the filtered text with the normal Ruby interpreter.
# All output sent to <tt>$stdout</tt>, like with +puts+,
# is output into the Haml document.
# Not available if the <tt>suppress_eval</tt> option is set to true.
#
# [preserve] Inserts the filtered text into the template with whitespace preserved.
# <tt>preserve</tt>d blocks of text aren't indented,
# and newlines are replaced with the HTML escape code for newlines,
# to preserve nice-looking output.
#
# [erb] Parses the filtered text with ERB, like an RHTML template.
# Not available if the <tt>suppress_eval</tt> option is set to true.
# At the moment, this doesn't support access to variables
# defined by Ruby on Rails or Haml code.
#
# [sass] Parses the filtered text with Sass to produce CSS output.
#
# [redcloth] Parses the filtered text with RedCloth (http://whytheluckystiff.net/ruby/redcloth),
# which uses both Textile and Markdown syntax.
# Only works if RedCloth is installed.
#
# [textile] Parses the filtered text with Textile (http://www.textism.com/tools/textile).
# Only works if RedCloth is installed.
#
# [markdown] Parses the filtered text with Markdown (http://daringfireball.net/projects/markdown).
# Only works if RedCloth or BlueCloth (http://www.deveiate.org/projects/BlueCloth)
# is installed
# (BlueCloth takes precedence if both are installed).
#
# You can also define your own filters (see Setting Options, below).
#
# === 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>
#
# You can also use two equal signs, <tt>==</tt>,
# along with conventional Ruby string-embedding syntax
# to easily embed Ruby code in otherwise static text.
# For example:
#
# %p
# == 1 + 1 = #{1 + 1}
#
# is compiled to:
#
# <p>
# 1 + 1 = 2
# </p>
#
# ==== -
#
# The hyphen character makes the text following it into "silent script":
# Ruby script that is evaluated, but not output.
#
# <b>It is not recommended that you use this widely;
# almost all processing code and logic should be restricted
# 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
#
# Ruby blocks, like XHTML tags, don't need to be explicitly closed in Haml.
# Rather, they're automatically closed, based on indentation.
# A block begins whenever the indentation is increased
# after a silent script command.
# It 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>
#
# ==== -#
#
# The hyphen followed immediately by the pound sign
# signifies a silent comment.
# Any text following this isn't rendered in the resulting document
# at all.
#
# For example:
#
# %p foo
# -# This is a comment
# %p bar
#
# is compiled to:
#
# <p>foo</p>
# <p>bar</p>
#
# == Other Useful Things
#
# === Helpers
#
# Haml offers a bunch of helpers that are useful
# for doing stuff like preserving whitespace,
# creating nicely indented output for user-defined helpers,
# and other useful things.
# The helpers are all documented in the Haml::Helpers and Haml::Helpers::ActionViewExtensions modules.
#
# === Haml Options
#
# Options can be set by setting the hash <tt>Haml::Template.options</tt>
# from <tt>environment.rb</tt> in Rails,
# or by passing an options hash to Haml::Engine.
# Available options are:
#
# [<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.
#
# [<tt>:attr_wrapper</tt>] The character that should wrap element attributes.
# This defaults to <tt>'</tt> (an apostrophe). Characters
# of this type within the attributes will be escaped
# (e.g. by replacing them with <tt>&apos;</tt>) if
# the character is an apostrophe or a quotation mark.
#
# [<tt>:filename</tt>] The name of the Haml file being parsed.
# This is only used as information when exceptions are raised.
# This is automatically assigned when working through ActionView,
# so it's really only useful for the user to assign
# when dealing with Haml programatically.
#
# [<tt>:filters</tt>] A hash of filters that can be applied to Haml code.
# The keys are the string names of the filters;
# the values are references to the classes of the filters.
# User-defined filters should always have lowercase keys,
# and should have:
# * An +initialize+ method that accepts one parameter,
# the text to be filtered.
# * A +render+ method that returns the result of the filtering.
#
# [<tt>:locals</tt>] The local variables that will be available within the
# template. For instance, if <tt>:locals</tt> is
# <tt>{ :foo => "bar" }</tt>, then within the template,
# <tt>= foo</tt> will produce <tt>bar</tt>.
#
# [<tt>:autoclose</tt>] A list of tag names that should be automatically self-closed
# if they have no content.
# Defaults to <tt>['meta', 'img', 'link', 'script', 'br', 'hr']</tt>.
#
module Haml; end
require 'haml/engine'

@ -0,0 +1,213 @@
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
# Gets the current tabulation of the document.
def tabulation
@real_tabs + @tabulation
end
# Sets the current tabulation of the document.
def tabulation=(val)
val = val - @real_tabs
@tabulation = val > -1 ? val : 0
end
# Creates a new buffer.
def initialize(options = {})
@options = options
@quote_escape = options[:attr_wrapper] == '"' ? "&quot;" : "&apos;"
@other_quote_char = options[:attr_wrapper] == '"' ? "'" : '"'
@buffer = ""
@one_liner_pending = false
@tabulation = 0
# The number of tabs that Engine thinks we should have
# @real_tabs + @tabulation is the number of tabs actually output
@real_tabs = 0
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 && Buffer.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 = Haml::Helpers.find_and_preserve(result)
end
result = result.to_s
while result[-1] == 10 # \n
# String#chomp is slow
result = result[0...-1]
end
result = result.gsub("\n", "\n#{tabs(tabulation)}")
push_text result, tabulation
nil
end
def open_prerendered_tag(tag, tabulation)
@buffer << "#{tabs(tabulation)}#{tag}"
@real_tabs += 1
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, obj_ref, attributes_hash)
attributes = class_id
if attributes_hash
attributes_hash.keys.each { |key| attributes_hash[key.to_s] = attributes_hash.delete(key) }
self.class.merge_attrs(attributes, attributes_hash)
end
self.class.merge_attrs(attributes, parse_object_ref(obj_ref)) if obj_ref
@one_liner_pending = false
if atomic
str = " />\n"
elsif try_one_line
@one_liner_pending = true
str = ">"
else
str = ">\n"
end
@buffer << "#{tabs(tabulation)}<#{name}#{build_attributes(attributes)}#{str}"
@real_tabs += 1
end
def self.merge_attrs(to, from)
if to['id'] && from['id']
to['id'] << '_' << from.delete('id')
end
if to['class'] && from['class']
# Make sure we don't duplicate class names
from['class'] = (from['class'].split(' ') | to['class'].split(' ')).join(' ')
end
to.merge!(from)
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
# Opens an XHTML comment.
def open_comment(try_one_line, conditional, tabulation)
conditional << ">" if conditional
@buffer << "#{tabs(tabulation)}<!--#{conditional.to_s} "
if try_one_line
@one_liner_pending = true
else
@buffer << "\n"
@real_tabs += 1
end
end
# Closes an XHTML comment.
def close_comment(has_conditional, tabulation)
close_tag = has_conditional ? "<![endif]-->" : "-->"
if @one_liner_pending
@buffer << " #{close_tag}\n"
@one_liner_pending = false
else
push_text(close_tag, tabulation)
end
end
# Some of these methods are exposed as public class methods
# so they can be re-used in helpers.
# 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|
v = v.to_s
unless v.nil? || v.empty?
attr_wrapper = @options[:attr_wrapper]
if v.include? attr_wrapper
if v.include? @other_quote_char
v = v.gsub(attr_wrapper, @quote_escape)
else
attr_wrapper = @other_quote_char
end
end
" #{a}=#{attr_wrapper}#{v}#{attr_wrapper}"
end
end
result.compact.sort.join
end
# Returns whether or not the given value is short enough to be rendered
# on one line.
def self.one_liner?(value)
value.length <= ONE_LINER_LENGTH && value.scan(/\n/).empty?
end
private
@@tab_cache = {}
# Gets <tt>count</tt> tabs. Mostly for internal use.
def tabs(count)
@real_tabs = count
tabs = count + @tabulation
' ' * tabs
@@tab_cache[tabs] ||= ' ' * tabs
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]
# Let's make sure the value isn't nil. If it is, return the default Hash.
return {} if ref.nil?
class_name = underscore(ref.class)
id = "#{class_name}_#{ref.id || 'new'}"
{'id' => id, 'class' => class_name}
end
# Changes a word from camel case to underscores.
# Based on the method of the same name in Rails' Inflector,
# but copied here so it'll run properly without Rails.
def underscore(camel_cased_word)
camel_cased_word.to_s.gsub(/::/, '_').
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
gsub(/([a-z\d])([A-Z])/,'\1_\2').
tr("-", "_").
downcase
end
end
end

@ -0,0 +1,876 @@
require 'haml/helpers'
require 'haml/buffer'
require 'haml/filters'
require 'haml/error'
require 'haml/util'
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 <tt>to_html</tt> to render the template. For example:
#
# template = File.read('templates/really_cool_template.haml')
# haml_engine = Haml::Engine.new(template)
# output = haml_engine.to_html
# puts output
class Engine
# Allow reading and writing of the options hash
attr :options, true
# Designates an XHTML/XML element.
ELEMENT = ?%
# Designates a <tt><div></tt> element with the given class.
DIV_CLASS = ?.
# Designates a <tt><div></tt> element with the given id.
DIV_ID = ?#
# Designates an XHTML/XML comment.
COMMENT = ?/
# Designates an XHTML doctype.
DOCTYPE = ?!
# Designates script, the result of which is output.
SCRIPT = ?=
# Designates script, the result of which is flattened and output.
FLAT_SCRIPT = ?~
# Designates script which is run but not output.
SILENT_SCRIPT = ?-
# When following SILENT_SCRIPT, designates a comment that is not output.
SILENT_COMMENT = ?#
# Designates a non-parsed line.
ESCAPE = ?\\
# Designates a block of filtered text.
FILTER = ?:
# Designates a non-parsed line. Not actually a character.
PLAIN_TEXT = -1
# Keeps track of the ASCII values of the characters that begin a
# specially-interpreted line.
SPECIAL_CHARACTERS = [
ELEMENT,
DIV_CLASS,
DIV_ID,
COMMENT,
DOCTYPE,
SCRIPT,
FLAT_SCRIPT,
SILENT_SCRIPT,
ESCAPE,
FILTER
]
# The value of the character that designates that a line is part
# of a multiline string.
MULTILINE_CHAR_VALUE = ?|
# Characters that designate that a multiline string may be about
# to begin.
MULTILINE_STARTERS = SPECIAL_CHARACTERS - [?/]
# 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']
# The Regex that matches an HTML comment command.
COMMENT_REGEX = /\/(\[[\w\s\.]*\])?(.*)/
# The Regex that matches a Doctype command.
DOCTYPE_REGEX = /(\d\.\d)?[\s]*([a-z]*)/i
# The Regex that matches an HTML tag command.
TAG_REGEX = /[%]([-:\w]+)([-\w\.\#]*)(\{.*\})?(\[.*\])?([=\/\~]?)?(.*)?/
# The Regex that matches a literal string or symbol value
LITERAL_VALUE_REGEX = /^\s*(:(\w*)|(('|")([^\\\#'"]*?)\4))\s*$/
FLAT_WARNING = <<END
Haml deprecation warning:
The ~ command is deprecated and will be removed in future Haml versions.
Use the :preserve filter, the preserve helper, or the find_and_preserve
helper instead.
END
# Creates a new instace of Haml::Engine that will compile the given
# template string when <tt>render</tt> is called.
# See README for available options.
#
#--
# When adding options, remember to add information about them
# to README!
#++
#
def initialize(template, l_options = {})
@options = {
:suppress_eval => false,
:attr_wrapper => "'",
:locals => {},
:autoclose => ['meta', 'img', 'link', 'br', 'hr', 'input', 'area'],
:filters => {
'sass' => Sass::Engine,
'plain' => Haml::Filters::Plain,
'preserve' => Haml::Filters::Preserve }
}
if !NOT_LOADED.include? 'redcloth'
@options[:filters].merge!({
'redcloth' => RedCloth,
'textile' => Haml::Filters::Textile,
'markdown' => Haml::Filters::Markdown
})
end
if !NOT_LOADED.include? 'bluecloth'
@options[:filters]['markdown'] = Haml::Filters::Markdown
end
@options.rec_merge! l_options
unless @options[:suppress_eval]
@options[:filters].merge!({
'erb' => ERB,
'ruby' => Haml::Filters::Ruby
})
end
@options[:filters].rec_merge! l_options[:filters] if l_options[:filters]
@template = template.strip #String
@to_close_stack = []
@output_tabs = 0
@template_tabs = 0
@index = 0
# This is the base tabulation of the currently active
# flattened block. -1 signifies that there is no such block.
@flat_spaces = -1
begin
# Only do the first round of pre-compiling if we really need to.
# They might be passing in the precompiled string.
requires_precompile = true
if @@method_names[@template]
# Check that the compiled method supports a superset of the local assigns we want to do
supported_assigns = @@supported_local_assigns[@template]
requires_precompile = !@options[:locals].keys.all? {|var| supported_assigns.include? var}
end
do_precompile if requires_precompile
rescue Haml::Error => e
e.add_backtrace_entry(@index, @options[:filename])
raise e
end
end
# Processes the template and returns the result as a string.
def render(scope = Object.new, &block)
@scope_object = scope
@buffer = Haml::Buffer.new(@options)
# Run the compiled evaluator function
compile &block
# Return the result string
@buffer.buffer
end
alias_method :to_html, :render
# This method is deprecated and shouldn't be used.
def precompiled
$stderr.puts <<END
The Haml precompiled method and :precompiled option
are deprecated and will be removed in version 2.0.
Haml::Engine now automatically handles caching.
END
nil
end
private
#Precompile each line
def do_precompile
@precompiled = ''
method_name = assign_method_name(@template, options[:filename])
push_silent <<-END
def #{method_name}(_haml_local_assigns)
@haml_is_haml = true
_hamlout = @haml_stack[-1]
_erbout = _hamlout.buffer
END
supported_local_assigns = {}
@@supported_local_assigns[@template] = supported_local_assigns
@options[:locals].each do |k,v|
supported_local_assigns[k] = true
push_silent "#{k} = _haml_local_assigns[:#{k}]"
end
old_line = nil
old_index = nil
old_spaces = nil
old_tabs = nil
old_uline = nil
(@template + "\n-#\n-#").each_with_index do |line, index|
spaces, tabs = count_soft_tabs(line)
uline = line.lstrip.chomp
line = uline.rstrip
if !line.empty?
if old_line
block_opened = tabs > old_tabs && !line.empty?
suppress_render = handle_multiline(old_tabs, old_line, old_index) unless @flat_spaces != -1
if !suppress_render
line_empty = old_line.empty?
process_indent(old_tabs, old_line) unless line_empty
flat = @flat_spaces != -1
if !flat && old_spaces != old_tabs * 2
raise SyntaxError.new("Illegal Indentation: Only two space characters are allowed as tabulation.")
end
if flat
push_flat(old_uline, old_spaces)
elsif !line_empty && !@haml_comment
process_line(old_line, old_index, block_opened)
end
if @flat_spaces == -1 && tabs - old_tabs > 1
raise SyntaxError.new("Illegal Indentation: Indenting more than once per line is illegal.")
end
end
end
old_line = line
old_index = index
old_spaces = spaces
old_tabs = tabs
old_uline = uline
elsif @flat_spaces != -1
process_indent(old_tabs, old_line) unless old_line.empty?
if @flat_spaces != -1
push_flat(old_line, old_spaces)
old_line = ''
old_uline = ''
old_spaces = 0
end
end
end
# Close all the open tags
@template_tabs.times { close }
push_silent "@haml_is_haml = false\nend\n"
end
# Processes and deals with lowering indentation.
def process_indent(count, line)
if count <= @template_tabs && @template_tabs > 0
to_close = @template_tabs - count
to_close.times do |i|
offset = to_close - 1 - i
unless offset == 0 && mid_block_keyword?(line)
close
end
end
end
end
# Processes a single line of Haml.
#
# This method doesn't return anything; it simply processes the line and
# adds the appropriate code to <tt>@precompiled</tt>.
def process_line(line, index, block_opened)
@index = index + 1
@block_opened = block_opened
case line[0]
when DIV_CLASS, DIV_ID
render_div(line)
when ELEMENT
render_tag(line)
when COMMENT
render_comment(line)
when SCRIPT
sub_line = line[1..-1]
if sub_line[0] == SCRIPT
push_script(unescape_interpolation(sub_line[1..-1].strip), false)
else
push_script(sub_line, false)
end
when FLAT_SCRIPT
push_flat_script(line[1..-1])
when SILENT_SCRIPT
sub_line = line[1..-1]
unless sub_line[0] == SILENT_COMMENT
mbk = mid_block_keyword?(line)
push_silent(sub_line, !mbk, true)
if (@block_opened && !mbk) || line[1..-1].split(' ', 2)[0] == "case"
push_and_tabulate([:script])
end
else
start_haml_comment
end
when FILTER
name = line[1..-1].downcase
start_filtered(options[:filters][name.to_s] || name)
when DOCTYPE
if line[0...3] == '!!!'
render_doctype(line)
else
push_plain line
end
when ESCAPE
push_plain line[1..-1]
else
push_plain line
end
end
# Returns whether or not the line is a silent script line with one
# of Ruby's mid-block keywords.
def mid_block_keyword?(line)
line.length > 2 && line[0] == SILENT_SCRIPT && MID_BLOCK_KEYWORDS.include?(line[1..-1].split[0])
end
# Deals with all the logic of figuring out whether a given line is
# the beginning, continuation, or end of a multiline sequence.
#
# This returns whether or not the line should be
# rendered normally.
def handle_multiline(count, line, index)
suppress_render = false
# Multilines are denoting by ending with a `|` (124)
if is_multiline?(line) && @multiline_buffer
# A multiline string is active, and is being continued
@multiline_buffer += line[0...-1]
suppress_render = true
elsif is_multiline?(line) && (MULTILINE_STARTERS.include? line[0])
# A multiline string has just been activated, start adding the lines
@multiline_buffer = line[0...-1]
@multiline_count = count
@multiline_index = index
process_indent(count, line)
suppress_render = true
elsif @multiline_buffer
# A multiline string has just ended, make line into the result
unless line.empty?
process_line(@multiline_buffer, @multiline_index, count > @multiline_count)
@multiline_buffer = nil
end
end
return suppress_render
end
# Checks whether or not +line+ is in a multiline sequence.
def is_multiline?(line) # ' '[0] == 32
line && line.length > 1 && line[-1] == MULTILINE_CHAR_VALUE && line[-2] == 32
end
# Method for generating compiled method names basically ripped out of ActiveView::Base
# If Haml is to be used as a standalone module without rails and still use the precompiled
# methods technique, it will end up duplicating this stuff. I can't decide whether
# checking compile times to decide whether to recompile a template belongs in here or
# out in template.rb
@@method_names = {}
@@supported_local_assigns = {}
@@render_method_count = 0
def assign_method_name(template, file_name)
@@render_method_count += 1
@@method_names[template] = "_render_haml_#{@@render_method_count}".intern
end
module CompiledTemplates
# holds compiled template code
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(&block)
# Set the local variables pointing to the buffer
buffer = @buffer
@scope_object.extend Haml::Helpers
@scope_object.instance_eval do
@haml_stack ||= Array.new
@haml_stack.push(buffer)
class << self
attr :haml_lineno # :nodoc:
end
end
@scope_object.class.instance_eval do
include CompiledTemplates
end
begin
method_name = @@method_names[@template]
unless @scope_object.respond_to?(method_name)
CompiledTemplates.module_eval @precompiled
end
@scope_object.send(method_name, options[:locals], &block)
rescue Exception => e
class << e
include Haml::Error
end
lineno = @scope_object.haml_lineno
# Get information from the exception and format it so that
# Rails can understand it.
compile_error = e.message.scan(/\(eval\):([0-9]*):in `[-_a-zA-Z]*': compile error/)[0]
if compile_error
if @precompiled
eval_line = compile_error[0].to_i
line_marker = @precompiled.split("\n")[0...eval_line].grep(/@haml_lineno = [0-9]*/)[-1]
lineno = line_marker.scan(/[0-9]+/)[0].to_i if line_marker
else
lineno = -1
end
end
e.add_backtrace_entry(lineno, @options[:filename])
raise e
end
# Get rid of the current buffer
@scope_object.instance_eval do
@haml_stack.pop
end
end
# Evaluates <tt>text</tt> in the context of <tt>@scope_object</tt>, but
# does not output the result.
def push_silent(text, add_index = false, can_suppress = false)
unless (can_suppress && options[:suppress_eval])
if add_index
@precompiled << "@haml_lineno = #{@index}\n#{text}\n"
else
# Not really DRY, but probably faster
@precompiled << "#{text}\n"
end
end
end
# Adds <tt>text</tt> to <tt>@buffer</tt> with appropriate tabulation
# without parsing it.
def push_text(text)
@precompiled << "_hamlout.push_text(#{text.dump}, #{@output_tabs})\n"
end
# Renders a block of text as plain text.
# Also checks for an illegally opened block.
def push_plain(text)
if @block_opened
raise SyntaxError.new("Illegal Nesting: Nesting within plain text is illegal.")
end
push_text text
end
# Adds +text+ to <tt>@buffer</tt> while flattening text.
def push_flat(text, spaces)
tabulation = spaces - @flat_spaces
tabulation = tabulation > -1 ? tabulation : 0
@filter_buffer << "#{' ' * tabulation}#{text}\n"
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)
unless options[:suppress_eval]
push_silent("haml_temp = #{text}", true)
out = "haml_temp = _hamlout.push_script(haml_temp, #{@output_tabs}, #{flattened})\n"
if @block_opened
push_and_tabulate([:loud, out])
else
@precompiled << out
end
end
end
# Causes <tt>text</tt> to be evaluated, and Haml::Helpers#find_and_flatten
# to be run on it afterwards.
def push_flat_script(text)
if text.empty?
raise SyntaxError.new("Tag has no content.")
else
push_script(text, true)
end
end
def start_haml_comment
if @block_opened
@haml_comment = true
push_and_tabulate([:haml_comment])
end
end
# Closes the most recent item in <tt>@to_close_stack</tt>.
def close
tag, value = @to_close_stack.pop
case tag
when :script
close_block
when :comment
close_comment value
when :element
close_tag value
when :loud
close_loud value
when :filtered
close_filtered value
when :haml_comment
close_haml_comment
end
end
# Puts a line in <tt>@precompiled</tt> that will add the closing tag of
# the most recently opened tag.
def close_tag(tag)
@output_tabs -= 1
@template_tabs -= 1
@precompiled << "_hamlout.close_tag(#{tag.dump}, #{@output_tabs})\n"
end
# Closes a Ruby block.
def close_block
push_silent "end", false, true
@template_tabs -= 1
end
# Closes a comment.
def close_comment(has_conditional)
@output_tabs -= 1
@template_tabs -= 1
push_silent "_hamlout.close_comment(#{has_conditional}, #{@output_tabs})"
end
# Closes a loud Ruby block.
def close_loud(command)
push_silent 'end', false, true
@precompiled << command
@template_tabs -= 1
end
# Closes a filtered block.
def close_filtered(filter)
@flat_spaces = -1
if filter.is_a? String
if filter == 'redcloth' || filter == 'markdown' || filter == 'textile'
raise HamlError.new("You must have the RedCloth gem installed to use #{filter}")
else
raise HamlError.new("Filter \"#{filter}\" is not defined!")
end
else
filtered = filter.new(@filter_buffer).render
unless filter == Haml::Filters::Preserve
push_text(filtered.rstrip.gsub("\n", "\n#{' ' * @output_tabs}"))
else
push_silent("_hamlout.buffer << #{filtered.dump} << \"\\n\"\n")
end
end
@filter_buffer = nil
@template_tabs -= 1
end
def close_haml_comment
@haml_comment = false
@template_tabs -= 1
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-Z0-9]+)/) 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
def parse_literal_value(text)
text.match(LITERAL_VALUE_REGEX)
# $2 holds the value matched by a symbol, but is nil for a string match
# $5 holds the value matched by a string
$2 || $5
end
def parse_literal_hash(text)
unless text
return {}
end
attributes = {}
if inner = text.scan(/^\{(.*)\}$/)[0]
inner[0].split(',').each do |attrib|
key, value, more = attrib.split('=>')
# Make sure the key and value and only the key and value exist
# Otherwise, it's too complicated and we'll defer it to the actual Ruby parser
if more || (key = parse_literal_value(key)).nil? ||
(value = parse_literal_value(value)).nil?
return nil
end
attributes[key] = value
end
end
attributes
end
def build_attributes(attributes = {})
@quote_escape = @options[:attr_wrapper] == '"' ? "&quot;" : "&apos;"
@other_quote_char = @options[:attr_wrapper] == '"' ? "'" : '"'
result = attributes.collect do |a,v|
v = v.to_s
unless v.nil? || v.empty?
attr_wrapper = @options[:attr_wrapper]
if v.include? attr_wrapper
if v.include? @other_quote_char
# An imperfection in LITERAL_VALUE_REGEX prevents this
# from ever actually being reached,
# but in case it becomes possible,
# I'm leaving it in.
v = v.gsub(attr_wrapper, @quote_escape)
else
attr_wrapper = @other_quote_char
end
end
" #{a}=#{attr_wrapper}#{v}#{attr_wrapper}"
end
end
result.sort.join
end
def prerender_tag(name, atomic, attributes)
if atomic
str = " />"
else
str = ">"
end
"<#{name}#{build_attributes(attributes)}#{str}"
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)
matched = false
line.scan(TAG_REGEX) do |tag_name, attributes, attributes_hash, object_ref, action, value|
matched = true
value = value.to_s.strip
case action
when '/'
atomic = true
when '=', '~'
parse = true
if value[0] == ?=
value = value[1..-1].strip.dump.gsub('\\#', '#')
end
end
flattened = (action == '~')
value_exists = !value.empty?
literal_attributes = parse_literal_hash(attributes_hash)
attributes_hash = "{nil}" if attributes_hash.nil? || literal_attributes || @options[:suppress_eval]
object_ref = "nil" if object_ref.nil? || @options[:suppress_eval]
if attributes =~ /[\.#](\.|#|\z)/
raise SyntaxError.new("Illegal element: classes and ids must have values. Use %div instead.")
end
# Preparse the attributes hash
attributes = parse_class_and_id(attributes)
Buffer.merge_attrs(attributes, literal_attributes) if literal_attributes
if @block_opened
if atomic
raise SyntaxError.new("Illegal Nesting: Nesting within an atomic tag is illegal.")
elsif action == '=' || value_exists
raise SyntaxError.new("Illegal Nesting: Nesting within a tag that already has content is illegal.")
end
elsif atomic && value_exists
raise SyntaxError.new("Atomic tags can't have content.")
elsif parse && !value_exists
raise SyntaxError.new("Tag has no content.")
end
if !@block_opened && !value_exists && @options[:autoclose].include?(tag_name)
atomic = true
end
do_one_liner = value_exists && !parse && Buffer.one_liner?(value)
if object_ref == "nil" && attributes_hash == "{nil}" && !flattened && (do_one_liner || !value_exists)
# This means that we can render the tag directly to text and not process it in the buffer
open_tag = prerender_tag(tag_name, atomic, attributes)
if do_one_liner
open_tag += value
open_tag += "</#{tag_name}>"
end
open_tag += "\n"
push_silent "_hamlout.open_prerendered_tag(#{open_tag.dump}, #{@output_tabs})"
return if do_one_liner
else
push_silent "_hamlout.open_tag(#{tag_name.inspect}, #{@output_tabs}, #{atomic.inspect}, #{value_exists.inspect}, #{attributes.inspect}, #{object_ref}, #{attributes_hash[1...-1]})", true
end
unless atomic
push_and_tabulate([:element, tag_name])
@output_tabs += 1
if value_exists
if parse
push_script(value, flattened)
else
push_text(value)
end
close
elsif flattened
raise SyntaxError.new("Tag has no content.")
end
end
end
unless matched
raise SyntaxError.new("Invalid tag: \"#{line}\"")
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)
render_tag('%div' + line)
end
# Renders an XHTML comment.
def render_comment(line)
conditional, content = line.scan(COMMENT_REGEX)[0]
content.strip!
if @block_opened && !content.empty?
raise SyntaxError.new('Illegal Nesting: Nesting within a tag that already has content is illegal.')
end
try_one_line = !content.empty?
push_silent "_hamlout.open_comment(#{try_one_line}, #{conditional.inspect}, #{@output_tabs})"
@output_tabs += 1
push_and_tabulate([:comment, !conditional.nil?])
if try_one_line
push_text content
close
end
end
# Renders an XHTML doctype or XML shebang.
def render_doctype(line)
if @block_opened
raise SyntaxError.new("Illegal Nesting: Nesting within a header command is illegal.")
end
line = line[3..-1].lstrip.downcase
if line[0...3] == "xml"
encoding = line.split[1] || "utf-8"
wrapper = @options[:attr_wrapper]
doctype = "<?xml version=#{wrapper}1.0#{wrapper} encoding=#{wrapper}#{encoding}#{wrapper} ?>"
else
version, type = line.scan(DOCTYPE_REGEX)[0]
if version == "1.1"
doctype = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'
else
case type
when "strict"
doctype = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
when "frameset"
doctype = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">'
else
doctype = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
end
end
end
push_text doctype
end
# Starts a filtered block.
def start_filtered(filter)
unless @block_opened
raise SyntaxError.new('Filters must have nested text.')
end
push_and_tabulate([:filtered, filter])
@flat_spaces = @template_tabs * 2
@filter_buffer = String.new
end
def unescape_interpolation(str)
str.dump.gsub('\\#', '#').gsub(/\#\{[^\}]+\}/) do |substr|
substr.gsub('\\"', '"')
end
end
# Counts the tabulation of a line.
def count_soft_tabs(line)
spaces = line.index(/[^ ]/)
if line[spaces] == ?\t
return nil if line.strip.empty?
raise SyntaxError.new("Illegal Indentation: Only two space characters are allowed as tabulation.")
end
[spaces, spaces/2]
end
# Pushes value onto <tt>@to_close_stack</tt> and increases
# <tt>@template_tabs</tt>.
def push_and_tabulate(value)
@to_close_stack.push(value)
@template_tabs += 1
end
end
end

@ -0,0 +1,43 @@
module Haml
# The abstract type of exception raised by Haml code.
# Haml::SyntaxError includes this module,
# as do all exceptions raised by Ruby code within Haml.
#
# Haml::Error encapsulates information about the exception,
# such as the line of the Haml template it was raised on
# and the Haml file that was being parsed (if applicable).
# It also provides a handy way to rescue only exceptions raised
# because of a faulty template.
module Error
# The line of the Haml template on which the exception was thrown.
attr_reader :haml_line
# The name of the file that was being parsed when the exception was raised.
# This will be nil unless Haml is being used as an ActionView plugin.
attr_reader :haml_filename
# Adds a properly formatted entry to the exception's backtrace.
# +lineno+ should be the line on which the error occurred.
# +filename+ should be the file in which the error occurred,
# if applicable (defaults to "(haml)").
def add_backtrace_entry(lineno, filename = nil) # :nodoc:
@haml_line = lineno
@haml_filename = filename
self.backtrace ||= []
self.backtrace.unshift "#{filename || '(haml)'}:#{lineno}"
end
end
# SyntaxError is the type of exception raised when Haml encounters an
# ill-formatted document.
# It's not particularly interesting, except in that it includes Haml::Error.
class SyntaxError < StandardError
include Haml::Error
end
# HamlError is the type of exception raised when Haml encounters an error
# not of a syntactical nature, such as an undefined Filter.
class HamlError < StandardError
include Haml::Error
end
end

@ -0,0 +1,296 @@
require File.dirname(__FILE__) + '/../haml'
require 'optparse'
module Haml
# This module contains code for working with the
# haml, sass, and haml2html executables,
# such as command-line parsing stuff.
# It shouldn't need to be invoked by client code.
module Exec # :nodoc:
# A class that encapsulates the executable code
# for all three executables.
class Generic # :nodoc:
def initialize(args)
@args = args
@options = {}
end
def parse!
begin
@opts = OptionParser.new(&(method(:set_opts).to_proc))
@opts.parse!(@args)
process_result
@options
rescue Exception => e
raise e if e.is_a? SystemExit
line = e.backtrace[0].scan(/:(.*)/)[0]
puts "#{e.class} on line #{line}: #{e.message}"
if @options[:trace]
e.backtrace[1..-1].each { |t| puts " #{t}" }
else
puts " Use --trace to see traceback"
end
exit 1
end
exit 0
end
def to_s
@opts.to_s
end
private
def set_opts(opts)
opts.on('--stdin', :NONE, 'Read input from standard input instead of an input file') do
@options[:input] = $stdin
end
opts.on('--stdout', :NONE, 'Print output to standard output instead of an output file') do
@options[:output] = $stdout
end
opts.on('-s', '--stdio', 'Read input from standard input and print output to standard output') do
@options[:input] = $stdin
@options[:output] = $stdout
end
opts.on('--trace', :NONE, 'Show a full traceback on error') do
@options[:trace] = true
end
opts.on_tail("-?", "-h", "--help", "Show this message") do
puts opts
exit
end
opts.on_tail("-v", "--version", "Print version") do
puts("Haml " + File.read(File.dirname(__FILE__) + '/../../VERSION'))
exit
end
end
def process_result
input = @options[:input]
output = @options[:output]
if input
output ||= ARGV[0]
else
input ||= ARGV[0]
output ||= ARGV[1]
end
unless input && output
puts @opts
exit 1
end
if input.is_a?(String) && !File.exists?(input)
puts "File #{input} doesn't exist!"
exit 1
end
unless input.is_a? IO
input = File.open(input)
input_file = true
end
unless output.is_a? IO
output = File.open(output, "w")
output_file = true
end
@options[:input] = input
@options[:output] = output
end
end
# A class encapsulating the executable functionality
# specific to Haml and Sass.
class HamlSass < Generic # :nodoc:
private
def set_opts(opts)
opts.banner = <<END
Usage: #{@name.downcase} [options] (#{@name.downcase} file) (output file)
Description:
Uses the #{@name} engine to parse the specified template
and outputs the result to the specified file.
Options:
END
opts.on('--rails RAILS_DIR', "Install Haml from the Gem to a Rails project") do |dir|
original_dir = dir
dir = File.join(dir, 'vendor', 'plugins')
unless File.exists?(dir)
puts "Directory #{dir} doesn't exist"
exit
end
dir = File.join(dir, 'haml')
if File.exists?(dir)
puts "Directory #{dir} already exists."
exit
end
begin
Dir.mkdir(dir)
rescue SystemCallError
puts "Cannot create #{dir}"
exit
end
File.open(File.join(dir, 'init.rb'), 'w') do |file|
file.puts <<END
require 'rubygems'
require 'haml'
require 'haml/template'
require 'sass'
require 'sass/plugin'
ActionView::Base.register_template_handler('haml', Haml::Template)
Sass::Plugin.update_stylesheets
END
end
puts "Haml plugin added to #{original_dir}"
exit
end
super
end
def process_result
super
require File.dirname(__FILE__) + "/../#{@name.downcase}"
end
end
# A class encapsulating executable functionality
# specific to Sass.
class Sass < HamlSass # :nodoc:
def initialize(args)
super
@name = "Sass"
end
def process_result
super
input = @options[:input]
output = @options[:output]
template = input.read()
input.close() if input.is_a? File
result = ::Sass::Engine.new(template).render
output.write(result)
output.close() if output.is_a? File
end
end
# A class encapsulating executable functionality
# specific to Haml.
class Haml < HamlSass # :nodoc:
def initialize(args)
super
@name = "Haml"
end
def process_result
super
input = @options[:input]
output = @options[:output]
template = input.read()
input.close() if input.is_a? File
result = ::Haml::Engine.new(template).to_html
output.write(result)
output.close() if output.is_a? File
end
end
# A class encapsulating executable functionality
# specific to the html2haml executable.
class HTML2Haml < Generic # :nodoc:
def initialize(args)
super
@module_opts = {}
begin
require 'haml/html'
rescue LoadError => err
dep = err.message.scan(/^no such file to load -- (.*)/)[0]
puts "Required dependency #{dep} not found!"
exit 1
end
end
def set_opts(opts)
opts.banner = <<END
Usage: html2haml [options] (html file) (output file)
Description: Transforms an HTML file into corresponding Haml code.
Options:
END
opts.on('-r', '--rhtml', 'Parse RHTML tags.') do
@module_opts[:rhtml] = true
end
super
end
def process_result
super
input = @options[:input]
output = @options[:output]
output.write(::Haml::HTML.new(input, @module_opts).render)
end
end
# A class encapsulating executable functionality
# specific to the css2sass executable.
class CSS2Sass < Generic # :nodoc:
def initialize(args)
super
require 'sass/css'
end
def set_opts(opts)
opts.banner = <<END
Usage: css2sass [options] (css file) (output file)
Description: Transforms a CSS file into corresponding Sass code.
Options:
END
super
end
def process_result
super
input = @options[:input]
output = @options[:output]
output.write(::Sass::CSS.new(input).render)
end
end
end
end

@ -0,0 +1,89 @@
# This file contains redefinitions of and wrappers around various text
# filters so they can be used as Haml filters.
# :stopdoc:
require 'erb'
require 'sass/engine'
require 'stringio'
volatile_requires = ['rubygems', 'redcloth', 'bluecloth']
NOT_LOADED = [] unless defined?(NOT_LOADED)
volatile_requires.each do |file|
begin
require file
rescue LoadError
NOT_LOADED.push file
end
end
class ERB; alias_method :render, :result; end
unless NOT_LOADED.include? 'bluecloth'
class BlueCloth; alias_method :render, :to_html; end
end
module Haml
module Filters
class Plain
def initialize(text)
@text = text
end
def render
@text
end
end
class Ruby
def initialize(text)
@text = text
end
def render
old_stdout = $stdout
$stdout = StringIO.new
Object.new.instance_eval(@text)
old_stdout, $stdout = $stdout, old_stdout
old_stdout.pos = 0
old_stdout.read
end
end
class Preserve
def initialize(text)
@text = text
end
def render
Haml::Helpers.preserve(@text)
end
end
unless NOT_LOADED.include? 'bluecloth'
Markdown = BlueCloth unless defined?(Markdown)
end
unless NOT_LOADED.include? 'redcloth'
class ::RedCloth; alias_method :render, :to_html; end
# Uses RedCloth to provide only Textile (not Markdown) parsing
class Textile < RedCloth
def render
self.to_html(:textile)
end
end
unless defined?(Markdown)
# Uses RedCloth to provide only Markdown (not Textile) parsing
class Markdown < RedCloth
def render
self.to_html(:markdown)
end
end
end
end
end
end
# :startdoc:

@ -0,0 +1,328 @@
require 'haml/helpers/action_view_mods'
require 'haml/helpers/action_view_extensions'
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
self.extend self
@@action_view_defined = defined?(ActionView)
@@force_no_action_view = false
# Returns whether or not ActionView is installed on the system.
def self.action_view?
@@action_view_defined
end
# Isolates the whitespace-sensitive tags in the string and uses preserve
# to convert any endlines inside them into HTML entities for endlines.
def find_and_preserve(input)
input = input.to_s
input.scan(/<(textarea|code|pre)[^>]*>(.*?)<\/\1>/im) do |tag, contents|
input = input.gsub(contents, preserve(contents))
end
input
end
# Takes any string, finds all the endlines and converts them to
# HTML entities for endlines so they'll render correctly in
# whitespace-sensitive tags without screwing up the indentation.
def preserve(input)
input.gsub(/\n/, '&#x000A;').gsub(/\r/, '')
end
alias_method :flatten, :preserve
# Takes an Enumerable object and a block
# and iterates over the object,
# yielding each element to a Haml block
# and putting the result into <tt><li></tt> elements.
# This creates a list of the results of the block.
# For example:
#
# = list_of([['hello'], ['yall']]) do |i|
# = i[0]
#
# Produces:
#
# <li>hello</li>
# <li>yall</li>
#
# And
#
# = list_of({:title => 'All the stuff', :description => 'A book about all the stuff.'}) do |key, val|
# %h3= key.humanize
# %p= val
#
# Produces:
#
# <li>
# <h3>Title</h3>
# <p>All the stuff</p>
# </li>
# <li>
# <h3>Description</h3>
# <p>A book about all the stuff.</p>
# </li>
#
def list_of(array, &block) # :yields: item
to_return = array.collect do |i|
result = capture_haml(i, &block)
if result.count("\n") > 1
result.gsub!("\n", "\n ")
result = "\n #{result.strip}\n"
else
result.strip!
end
"<li>#{result}</li>"
end
to_return.join("\n")
end
# Returns a hash containing default assignments for the xmlns and xml:lang
# attributes of the <tt>html</tt> HTML element.
# It also takes an optional argument for the value of xml:lang and lang,
# which defaults to 'en-US'.
# For example,
#
# %html{html_attrs}
#
# becomes
#
# <html xmlns='http://www.w3.org/1999/xhtml' xml:lang='en-US' lang='en-US'>
#
def html_attrs(lang = 'en-US')
{:xmlns => "http://www.w3.org/1999/xhtml", 'xml:lang' => lang, :lang => lang}
end
# Increments the number of tabs the buffer automatically adds
# to the lines of the template.
# For example:
#
# %h1 foo
# - tab_up
# %p bar
# - tab_down
# %strong baz
#
# Produces:
#
# <h1>foo</h1>
# <p>bar</p>
# <strong>baz</strong>
#
def tab_up(i = 1)
buffer.tabulation += i
end
# Increments the number of tabs the buffer automatically adds
# to the lines of the template.
#
# See tab_up.
def tab_down(i = 1)
buffer.tabulation -= i
end
# Surrounds the given block of Haml code with the given characters,
# with no whitespace in between.
# For example:
#
# = surround '(', ')' do
# %a{:href => "food"} chicken
#
# Produces:
#
# (<a href='food'>chicken</a>)
#
# and
#
# = surround '*' do
# %strong angry
#
# Produces:
#
# *<strong>angry</strong>*
#
def surround(front, back = nil, &block)
back ||= front
output = capture_haml(&block)
"#{front}#{output.chomp}#{back}\n"
end
# Prepends the given character to the beginning of the Haml block,
# with no whitespace between.
# For example:
#
# = precede '*' do
# %span.small Not really
#
# Produces:
#
# *<span class='small'>Not really</span>
#
def precede(char, &block)
"#{char}#{capture_haml(&block).chomp}\n"
end
# Appends the given character to the end of the Haml block,
# with no whitespace between.
# For example:
#
# click
# = succeed '.' do
# %a{:href=>"thing"} here
#
# Produces:
#
# click
# <a href='thing'>here</a>.
#
def succeed(char, &block)
"#{capture_haml(&block).chomp}#{char}\n"
end
# Captures the result of the given block of Haml code,
# gets rid of the excess indentation,
# and returns it as a string.
# For example, after the following,
#
# .foo
# - foo = capture_haml(13) do |a|
# %p= a
#
# the local variable <tt>foo</tt> would be assigned to "<p>13</p>\n".
#
def capture_haml(*args, &block)
capture_haml_with_buffer(buffer.buffer, *args, &block)
end
# Outputs text directly to the Haml buffer, with the proper tabulation
def puts(text = "")
buffer.buffer << (' ' * buffer.tabulation) << text.to_s << "\n"
nil
end
#
# call-seq:
# open(name, attributes = {}) {...}
# open(name, text, attributes = {}) {...}
#
# Creates an HTML tag with the given name and optionally text and attributes.
# Can take a block that will be executed
# between when the opening and closing tags are output.
# If the block is a Haml block or outputs text using puts,
# the text will be properly indented.
#
# For example,
#
# open :table do
# open :tr do
# open :td, {:class => 'cell'} do
# open :strong, "strong!"
# puts "data"
# end
# open :td do
# puts "more_data"
# end
# end
# end
#
# outputs
#
# <table>
# <tr>
# <td class='cell'>
# <strong>
# strong!
# </strong>
# data
# </td>
# <td>
# more_data
# </td>
# </tr>
# </table>
#
def open(name, attributes = {}, alt_atts = {}, &block)
text = nil
if attributes.is_a? String
text = attributes
attributes = alt_atts
end
puts "<#{name}#{buffer.build_attributes(attributes)}>"
tab_up
# Print out either the text (using push_text) or call the block and add an endline
if text
puts(text)
elsif block
block.call
end
tab_down
puts "</#{name}>"
nil
end
private
# Gets a reference to the current Haml::Buffer object.
def buffer
@haml_stack[-1]
end
# Gives a proc the same local "_hamlout" and "_erbout" variables
# that the current template has.
def bind_proc(&proc)
_hamlout = buffer
_erbout = _hamlout.buffer
proc { |*args| proc.call(*args) }
end
# Performs the function of capture_haml, assuming <tt>local_buffer</tt>
# is where the output of block goes.
def capture_haml_with_buffer(local_buffer, *args, &block)
position = local_buffer.length
block.call *args
captured = local_buffer.slice!(position..-1)
min_tabs = nil
captured.each do |line|
tabs = line.index(/[^ ]/)
min_tabs ||= tabs
min_tabs = min_tabs > tabs ? tabs : min_tabs
end
result = captured.map do |line|
line[min_tabs..-1]
end
result.to_s
end
# Returns whether or not the current template is a Haml template.
#
# This function, unlike other Haml::Helpers functions,
# also works in other ActionView templates,
# where it will always return false.
def is_haml?
@haml_is_haml
end
include ActionViewExtensions if self.const_defined? "ActionViewExtensions"
end
end
module ActionView
class Base # :nodoc:
def is_haml?
false
end
end
end

@ -0,0 +1,45 @@
require 'haml/helpers/action_view_mods'
if defined?(ActionView)
module Haml
module Helpers
# This module contains various useful helper methods
# that either tie into ActionView or the rest of the ActionPack stack,
# or are only useful in that context.
# Thus, the methods defined here are only available
# if ActionView is installed.
module ActionViewExtensions
# Returns a value for the "class" attribute
# unique to this controller/action pair.
# This can be used to target styles specifically at this action or controller.
# For example, if the current action were EntryController#show,
#
# %div{:class => page_class} My Div
#
# would become
#
# <div class="entry show">My Div</div>
#
# Then, in a stylesheet
# (shown here as Sass),
# you could refer to this specific action:
#
# .entry.show
# :font-weight bold
#
# or to all actions in the entry controller:
#
# .entry
# :color #00f
#
def page_class
controller.controller_name + " " + controller.action_name
end
# :stopdoc:
alias_method :generate_content_class_names, :page_class
# :startdoc:
end
end
end
end

@ -0,0 +1,86 @@
if defined?(ActionView) and not defined?(Merb::Plugins)
module ActionView
class Base # :nodoc:
def render_with_haml(*args, &block)
was_haml = is_haml?
@haml_is_haml = false
res = render_without_haml(*args, &block)
@haml_is_haml = was_haml
res
end
alias_method :render_without_haml, :render
alias_method :render, :render_with_haml
end
# This overrides various helpers in ActionView
# to make them work more effectively with Haml.
module Helpers
# :stopdoc:
module CaptureHelper
def capture_erb_with_buffer_with_haml(*args, &block)
if is_haml?
capture_haml_with_buffer(*args, &block)
else
capture_erb_with_buffer_without_haml(*args, &block)
end
end
alias_method :capture_erb_with_buffer_without_haml, :capture_erb_with_buffer
alias_method :capture_erb_with_buffer, :capture_erb_with_buffer_with_haml
end
module TextHelper
def concat_with_haml(string, binding = nil)
if is_haml?
buffer.buffer.concat(string)
else
concat_without_haml(string, binding)
end
end
alias_method :concat_without_haml, :concat
alias_method :concat, :concat_with_haml
end
module FormTagHelper
def form_tag_with_haml(url_for_options = {}, options = {}, *parameters_for_url, &proc)
if is_haml?
if block_given?
oldproc = proc
proc = bind_proc do |*args|
concat "\n"
tab_up
oldproc.call(*args)
tab_down
end
end
res = form_tag_without_haml(url_for_options, options, *parameters_for_url, &proc) + "\n"
concat "\n" if block_given? && is_haml?
res
else
form_tag_without_haml(url_for_options, options, *parameters_for_url, &proc)
end
end
alias_method :form_tag_without_haml, :form_tag
alias_method :form_tag, :form_tag_with_haml
end
module FormHelper
def form_for_with_haml(object_name, *args, &proc)
if block_given? && is_haml?
oldproc = proc
proc = bind_proc do |*args|
tab_up
oldproc.call(*args)
tab_down
end
end
form_for_without_haml(object_name, *args, &proc)
concat "\n" if block_given? && is_haml?
end
alias_method :form_for_without_haml, :form_for
alias_method :form_for, :form_for_with_haml
end
# :startdoc:
end
end
end

@ -0,0 +1,30 @@
***************
*** 1,11 ****
- begin
- require 'rubygems'
- require 'active_support'
- require 'action_controller'
- require 'action_view'
- action_view_included = true
- rescue LoadError
- action_view_included = false
end
--- 1,16 ----
+
+ # This obviously requires that ActiveSupport be present prior to Haml
+ # being loaded.
+ action_view_included = false
+ if defined?(ActiveSupport)
+ begin
+ require 'rubygems'
+ require 'active_support'
+ require 'action_controller'
+ require 'action_view'
+ action_view_included = true
+ rescue LoadError
+ end
end

@ -0,0 +1,173 @@
require File.dirname(__FILE__) + '/../haml'
require 'haml/engine'
require 'rubygems'
require 'hpricot'
require 'cgi'
module Haml
# This class contains the functionality used in the +html2haml+ utility,
# namely converting HTML documents to Haml templates.
# It depends on Hpricot for HTML parsing (http://code.whytheluckystiff.net/hpricot/).
class HTML
# Creates a new instance of Haml::HTML that will compile the given template,
# which can either be a string containing HTML or an Hpricot node,
# to a Haml string when +render+ is called.
def initialize(template, options = {})
@@options = options
if template.is_a? Hpricot::Node
@template = template
else
if template.is_a? IO
template = template.read
end
if @@options[:rhtml]
match_to_html(template, /<%=(.*?)-?%>/m, 'loud')
match_to_html(template, /<%(.*?)-?%>/m, 'silent')
end
@template = Hpricot(template)
end
end
# Processes the document and returns the result as a string
# containing the Haml template.
def render
@template.to_haml(0)
end
alias_method :to_haml, :render
module ::Hpricot::Node
# Returns the Haml representation of the given node,
# at the given tabulation.
def to_haml(tabs = 0)
parse_text(self.to_s, tabs)
end
private
def tabulate(tabs)
' ' * tabs
end
def parse_text(text, tabs)
text.strip!
if text.empty?
String.new
else
lines = text.split("\n")
lines.map do |line|
line.strip!
"#{tabulate(tabs)}#{'\\' if Haml::Engine::SPECIAL_CHARACTERS.include?(line[0])}#{line}\n"
end.join
end
end
end
# :stopdoc:
def self.options
@@options
end
TEXT_REGEXP = /^(\s*).*$/
class ::Hpricot::Doc
def to_haml(tabs = 0)
output = ''
children.each { |child| output += child.to_haml(0) }
output
end
end
class ::Hpricot::XMLDecl
def to_haml(tabs = 0)
"#{tabulate(tabs)}!!! XML\n"
end
end
class ::Hpricot::DocType
def to_haml(tabs = 0)
attrs = public_id.scan(/DTD\s+([^\s]+)\s*([^\s]*)\s*([^\s]*)\s*\/\//)[0]
if attrs == nil
raise Exception.new("Invalid doctype")
end
type, version, strictness = attrs.map { |a| a.downcase }
if type == "html"
version = "1.0"
strictness = "transitional"
end
if version == "1.0" || version.empty?
version = nil
end
if strictness == 'transitional' || strictness.empty?
strictness = nil
end
version = " #{version}" if version
if strictness
strictness[0] = strictness[0] - 32
strictness = " #{strictness}"
end
"#{tabulate(tabs)}!!!#{version}#{strictness}\n"
end
end
class ::Hpricot::Comment
def to_haml(tabs = 0)
"#{tabulate(tabs)}/\n#{parse_text(self.content, tabs + 1)}"
end
end
class ::Hpricot::Elem
def to_haml(tabs = 0)
output = "#{tabulate(tabs)}"
if HTML.options[:rhtml] && name[0...5] == 'haml:'
return output + HTML.send("haml_tag_#{name[5..-1]}", self.innerHTML)
end
output += "%#{name}" unless name == 'div' && (attributes.include?('id') || attributes.include?('class'))
if attributes
output += "##{attributes['id']}" if attributes['id']
attributes['class'].split(' ').each { |c| output += ".#{c}" } if attributes['class']
attributes.delete("id")
attributes.delete("class")
output += attributes.inspect if attributes.length > 0
end
output += "/" if children.length == 0
output += "\n"
self.children.each do |child|
output += child.to_haml(tabs + 1)
end
output
end
end
def self.haml_tag_loud(text)
"= #{text.gsub(/\n\s*/, '; ').strip}\n"
end
def self.haml_tag_silent(text)
text.split("\n").map { |line| "- #{line.strip}\n" }.join
end
private
def match_to_html(string, regex, tag)
string.gsub!(regex) do
"<haml:#{tag}>#{CGI.escapeHTML($1)}</haml:#{tag}>"
end
end
# :startdoc:
end
end

@ -0,0 +1,99 @@
require 'haml/engine'
require 'rubygems'
require 'active_support'
require 'action_view'
module Haml
# This class interfaces with ActionView
# to make Haml usable as a Ruby on Rails plugin.
# It usually shouldn't need to be used by end users.
# Just in case, though, here's what you might do to render
# <tt>templates/index.haml</tt>:
#
# ActionView::Base.register_template_handler("haml", Haml::Template)
# base = ActionView::Base.new("templates")
# base.render("index")
#
# Or, if you want to really get into the nitty-gritty:
#
# base = ActionView::Base.new
# template = Haml::Template.new(base)
# template.render("templates/index.haml")
#
class Template
class << self
@@options = {}
# Gets various options for Haml. See README for details.
def options
@@options
end
# Sets various options for Haml. See README for details.
def options=(value)
@@options = value
end
end
# Creates a new Haml::Template object that uses <tt>view</tt>
# to render its templates.
def initialize(view)
@view = view
end
# Renders the file at the location <tt>template</tt>,
# with <tt>local_assigns</tt> available as local variables within the template.
# Returns the result as a string.
def render(template, local_assigns={})
@view.instance_eval do
evaluate_assigns
end
options = @@options.dup
locals = options[:locals] || {}
locals.merge! local_assigns
options[:locals] = locals
if @view.haml_inline
engine = Haml::Engine.new(template, options)
else
options[:filename] ||= template
engine = Haml::Engine.new(File.read(template), options)
end
yield_proc = @view.instance_eval do
proc { |*name| instance_variable_get("@content_for_#{name.first || 'layout'}") }
end
engine.to_html(@view) { |*args| yield_proc.call(*args) }
end
end
end
# This module refers to the ActionView module that's part of Ruby on Rails.
# Haml can be used as an alternate templating engine for it,
# and includes several modifications to make it more Haml-friendly.
# The documentation can be found
# here[http://rubyonrails.org/api/classes/ActionView/Base.html].
module ActionView
class Base # :nodoc:
attr :haml_inline
alias_method :read_template_file_old, :read_template_file
def read_template_file(template_path, extension)
if extension =~ /haml/i
template_path
else
read_template_file_old(template_path, extension)
end
end
alias_method :render_template_old, :render_template
def render_template(template_extension, template, file_path = nil, local_assigns = {})
@haml_inline = !template.nil?
render_template_old(template_extension, template, file_path, local_assigns)
end
end
end

@ -0,0 +1,18 @@
# This file contains various useful bits of code
# that are shared between Haml and Sass.
class Hash # :nodoc:
# Same as Hash#merge!,
# but recursively merges sub-hashes
def rec_merge!(other)
other.each do |key, value|
myval = self[key]
if value.is_a?(Hash) && myval.is_a?(Hash)
myval.rec_merge!(value)
else
self[key] = value
end
end
self
end
end

@ -0,0 +1,613 @@
dir = File.dirname(__FILE__)
$LOAD_PATH << dir unless $LOAD_PATH.include?(dir)
# = Sass (Syntactically Awesome StyleSheets)
#
# Sass is a meta-language on top of CSS
# that's used to describe the style of a document
# cleanly and structurally,
# with more power than flat CSS allows.
# Sass both provides a simpler, more elegant syntax for CSS
# and implements various features that are useful
# for creating manageable stylesheets.
#
# == Features
#
# * Whitespace active
# * Well-formatted output
# * Elegant input
# * Feature-rich
#
# == Using Sass
#
# Sass can be used in several ways:
# As a plugin for Ruby on Rails or Merb,
# or as a standalone parser.
# Sass is bundled with Haml,
# so if the Haml plugin or RubyGem is installed,
# Sass will already be installed as a plugin or gem, respectively.
# The first step for all of these is to install the Haml gem:
#
# gem install haml
#
# To enable it as a Rails plugin,
# then run
#
# haml --rails path/to/rails/app
#
# To enable Sass in Merb,
# add
#
# dependency "haml"
#
# to config/dependencies.rb.
#
# Sass templates in Rails and Merb don't quite function in the same way as views,
# because they don't contain dynamic content,
# and so only need to be compiled when the template file has been updated.
# By default (see options, below),
# ".sass" files are placed in public/stylesheets/sass.
# Then, whenever necessary, they're compiled into corresponding CSS files in public/stylesheets.
# For instance, public/stylesheets/sass/main.sass would be compiled to public/stylesheets/main.css.
#
# Using Sass in Ruby code is very simple.
# After installing the Haml gem,
# you can use it by running <tt>require "sass"</tt>
# and using Sass::Engine like so:
#
# engine = Sass::Engine.new("#main\n :background-color #0000ff")
# engine.render #=> "#main { background-color: #0000ff; }\n"
#
# == CSS Rules
#
# Rules in flat CSS have two elements:
# the selector
# (e.g. "#main", "div p", "li a:hover")
# and the attributes
# (e.g. "color: #00ff00;", "width: 5em;").
#
# Sass has both of these,
# as well as one additional element: nested rules.
#
# === Rules and Selectors
#
# However, some of the syntax is a little different.
# The syntax for selectors is the same,
# but instead of using brackets to delineate the attributes that belong to a particular rule,
# Sass uses two spaces of indentation.
# For example:
#
# #main p
# <attribute>
# <attribute>
# ...
#
# === Attributes
#
# There are two different ways to write CSS attrbibutes.
# The first is very similar to the how you're used to writing them:
# with a colon between the name and the value.
# However, Sass attributes don't have semicolons at the end;
# each attribute is on its own line, so they aren't necessary.
# For example:
#
# #main p
# color: #00ff00
# width: 97%
#
# is compiled to:
#
# #main p {
# color: #00ff00;
# width: 97% }
#
# The second syntax for attributes is slightly different.
# The colon is at the beginning of the attribute,
# rather than between the name and the value,
# so it's easier to tell what elements are attributes just by glancing at them.
# For example:
#
# #main p
# :color #00ff00
# :width 97%
#
# is compiled to:
#
# #main p {
# color: #00ff00;
# width: 97% }
#
# === Nested Rules
#
# Rules can also be nested within each other.
# This signifies that the inner rule's selector is a child of the outer selector.
# For example:
#
# #main p
# :color #00ff00
# :width 97%
#
# .redbox
# :background-color #ff0000
# :color #000000
#
# is compiled to:
#
# #main p {
# color: #00ff00;
# width: 97%; }
# #main p .redbox {
# background-color: #ff0000;
# color: #000000; }
#
# This makes insanely complicated CSS layouts with lots of nested selectors very simple:
#
# #main
# :width 97%
#
# p, div
# :font-size 2em
# a
# :font-weight bold
#
# pre
# :font-size 3em
#
# is compiled to:
#
# #main {
# width: 97%; }
# #main p, #main div {
# font-size: 2em; }
# #main p a, #main div a {
# font-weight: bold; }
# #main pre {
# font-size: 3em; }
#
# === Referencing Parent Rules
#
# In addition to the default behavior of inserting the parent selector
# as a CSS parent of the current selector
# (e.g. above, "#main" is the parent of "p"),
# you can have more fine-grained control over what's done with the parent selector
# by using the ampersand character "&" in your selectors.
#
# The ampersand is automatically replaced by the parent selector,
# instead of having it prepended.
# This allows you to cleanly create pseudo-attributes:
#
# a
# :font-weight bold
# :text-decoration none
# &:hover
# :text-decoration underline
# &:visited
# :font-weight normal
#
# Which would become:
#
# a {
# font-weight: bold;
# text-decoration: none; }
# a:hover {
# text-decoration: underline; }
# a:visited {
# font-weight: normal; }
#
# It also allows you to add selectors at the base of the hierarchy,
# which can be useuful for targeting certain styles to certain browsers:
#
# #main
# :width 90%
# #sidebar
# :float left
# :margin-left 20%
# .ie6 &
# :margin-left 40%
#
# Which would become:
#
# #main {
# width: 90%; }
# #main #sidebar {
# float: left;
# margin-left: 20%; }
# .ie6 #main #sidebar {
# margin-left: 40%; }
#
# === Attribute Namespaces
#
# CSS has quite a few attributes that are in "namespaces;"
# for instance, "font-family," "font-size," and "font-weight"
# are all in the "font" namespace.
# In CSS, if you want to set a bunch of attributes in the same namespace,
# you have to type it out each time.
# Sass offers a shortcut for this:
# just write the namespace one,
# then indent each of the sub-attributes within it.
# For example:
#
# .funky
# :font
# :family fantasy
# :size 30em
# :weight bold
#
# is compiled to:
#
# .funky {
# font-family: fantasy;
# font-size: 30em;
# font-weight: bold; }
#
# == Constants
#
# Sass has support for setting document-wide constants.
# They're set using an exclamation mark followed by the name,
# an equals sign, and the value.
# An attribute can then be set to the value of a constant
# by following it with another equals sign.
# For example:
#
# !main_color = #00ff00
#
# #main
# :color = !main_color
# :p
# :background-color = !main_color
# :color #000000
#
# is compiled to:
#
# #main {
# color: #00ff00; }
# #main p {
# background-color: #00ff00;
# color: #000000; }
#
# === Arithmetic
#
# You can even do basic arithmetic with constants.
# Sass recognizes numbers, colors,
# lengths (numbers with units),
# and strings (everything that's not one of the above),
# and various operators that work on various values.
# All the normal arithmetic operators
# (+, -, *, /, %, and parentheses for grouping)
# are defined as usual
# for numbers, colors, and lengths.
# The "+" operator is also defined for Strings
# as the concatenation operator.
# For example:
#
# !main_width = 10
# !unit1 = em
# !unit2 = px
# !bg_color = #a5f39e
#
# #main
# :background-color = !bg_color
# p
# :background-color = !bg_color + #202020
# :width = !main_width + !unit1
# img.thumb
# :width = (!main_width + 15) + !unit2
#
# is compiled to:
#
# #main {
# background-color: #a5f39e; }
# #main p {
# background-color: #c5ffbe;
# width: 10em; }
# #main img.thumb {
# width: 25em; }
#
# === Colors
#
# Colors may be written as three- or six-digit hex numbers prefixed
# by a pound sign (#), or as HTML4 color names. For example,
# "#ff0", "#ffff00" and "yellow" all refer to the same color.
#
# Not only can arithmetic be done between colors and other colors,
# but it can be done between colors and normal numbers.
# In this case, the operation is done piecewise one each of the
# Red, Green, and Blue components of the color.
# For example:
#
# !main_color = #a5f39e
#
# #main
# :background-color = !main_color
# p
# :background-color = !main_color + 32
#
# is compiled to:
#
# #main {
# background-color: #a5f39e; }
# #main p {
# background-color: #c5ffbe; }
#
# === Strings
#
# Strings are the type that's used by default
# when an element in a bit of constant arithmetic isn't recognized
# as another type of constant.
# However, they can also be created explicitly be wrapping a section of code with quotation marks.
# Inside the quotation marks,
# a backslash can be used to
# escape quotation marks that you want to appear in the CSS.
# For example:
#
# !content = "Hello, \"Hubert\" Bean."
#
# #main
# :content = "string(" + !content + ")"
#
# is compiled to:
#
# #main {
# content: string(Hello, "Hubert" Bean.) }
#
# === Default Concatenation
#
# All those plusses and quotes for concatenating strings
# can get pretty messy, though.
# Most of the time, if you want to concatenate stuff,
# you just want individual values with spaces in between them.
# Thus, in Sass, when two values are next to each other without an operator,
# they're simply joined with a space.
# For example:
#
# !font_family = "sans-serif"
# !main_font_size = 1em
#
# #main
# :font
# :family = !font_family
# :size = !main_font_size
# h6
# :font = italic "small-caps" bold (!main_font_size + 0.1em) !font_family
#
# is compiled to:
#
# #main {
# font-family: sans-serif;
# font-size: 1em; }
# #main h6 {
# font: italic small-caps bold 1.1em sans-serif; }
#
# == Directives
#
# Directives allow the author to directly issue instructions to the Sass compiler.
# They're prefixed with an at sign, "<tt>@</tt>",
# followed by the name of the directive,
# a space, and any arguments to it -
# just like CSS directives.
# For example:
#
# @import red.sass
#
# === Import
#
# Currently, the only directive is the "import" directive.
# It works in a very similar way to the CSS import directive,
# and sometimes compiles to a literal CSS "@import".
#
# Sass can import either other Sass files or plain CSS files.
# If it imports a Sass file,
# not only are the rules from that file included,
# but all constants in that file are made available in the current file.
#
# Sass looks for other Sass files in the working directory,
# and the Sass file directory under Rails or Merb.
# Additional search directories may be specified
# using the :load_paths option (see below).
#
# Sass can also import plain CSS files.
# In this case, it doesn't literally include the content of the files;
# rather, it uses the built-in CSS "@import" directive to tell the client program
# to import the files.
#
# The import directive can take either a full filename
# or a filename without an extension.
# If an extension isn't provided,
# Sass will try to find a Sass file with the given basename in the load paths,
# and, failing that, will assume a relevant CSS file will be available.
#
# For example,
#
# @import foo.sass
#
# would compile to
#
# .foo
# :color #f00
#
# whereas
#
# @import foo.css
#
# would compile to
#
# @import foo.css
#
# Finally,
#
# @import foo
#
# might compile to either,
# depending on whether a file called "foo.sass" existed.
#
# == Comments
#
# === Silent Comments
#
# It's simple to add "silent" comments,
# which don't output anything to the CSS document,
# to a Sass document.
# Simply use the familiar C-style notation for a one-line comment, "//",
# at the normal indentation level and all text following it won't be output.
# For example:
#
# // A very awesome rule.
# #awesome.rule
# // An equally awesome attribute.
# :awesomeness very
#
# becomes
#
# #awesome.rule {
# awesomeness: very; }
#
# === Loud Comments
#
# "Loud" comments are just as easy as silent ones.
# These comments output to the document as CSS comments,
# and thus use the same opening sequence: "/*".
# For example:
#
# /* A very awesome rule.
# #awesome.rule
# /* An equally awesome attribute.
# :awesomeness very
#
# becomes
#
# /* A very awesome rule. */
# #awesome.rule {
# /* An equally awesome attribute. */
# awesomeness: very; }
#
# == Output Style
#
# Although the default CSS style that Sass outputs is very nice,
# and reflects the structure of the document in a similar way that Sass does,
# sometimes it's good to have other formats available.
#
# Sass allows you to choose between three different output styles
# by setting the <tt>:style</tt> option.
# In Rails, this is done by setting <tt>Sass::Plugin.options[:style]</tt>;
# outside Rails, it's done by passing an options hash with </tt>:style</tt> set.
#
# === <tt>:nested</tt>
#
# Nested style is the default Sass style,
# because it reflects the structure of the document
# in much the same way Sass does.
# Each attribute has its own line,
# but the indentation isn't constant.
# Each rule is indented based on how deeply it's nested.
# For example:
#
# #main {
# color: #fff;
# background-color: #000; }
# #main p {
# width: 10em; }
#
# .huge {
# font-size: 10em;
# font-weight: bold;
# text-decoration: underline; }
#
# Nested style is very useful when looking at large CSS files
# for the same reason Sass is useful for making them:
# it allows you to very easily grasp the structure of the file
# without actually reading anything.
#
# === <tt>:expanded</tt>
#
# Expanded is the typical human-made CSS style,
# with each attribute and rule taking up one line.
# Attributes are indented within the rules,
# but the rules aren't indented in any special way.
# For example:
#
# #main {
# color: #fff;
# background-color: #000;
# }
# #main p {
# width: 10em;
# }
#
# .huge {
# font-size: 10em;
# font-weight: bold;
# text-decoration: underline;
# }
#
# === <tt>:compact</tt>
#
# Compact style, as the name would imply,
# takes up less space than Nested or Expanded.
# However, it's also harder to read.
# Each CSS rule takes up only one line,
# with every attribute defined on that line.
# Nested rules are placed next to each other with no newline,
# while groups of rules have newlines between them.
# For example:
#
# #main { color: #fff; background-color: #000; }
# #main p { width: 10em; }
#
# .huge { font-size: 10em; font-weight: bold; text-decoration: underline; }
#
# == Sass Options
#
# Options can be set by setting the hash <tt>Sass::Plugin.options</tt>
# from <tt>environment.rb</tt> in Rails,
# or by passing an options hash to Sass::Engine.
# Available options are:
#
# [<tt>:style</tt>] Sets the style of the CSS output.
# See the section on Output Style, above.
#
# [<tt>:always_update</tt>] Whether the CSS files should be updated every
# time a controller is accessed,
# as opposed to only when the template has been modified.
# Defaults to false.
# Only has meaning within Ruby on Rails or Merb.
#
# [<tt>:always_check</tt>] Whether a Sass template should be checked for updates every
# time a controller is accessed,
# as opposed to only when the Rails server starts.
# If a Sass template has been updated,
# it will be recompiled and will overwrite the corresponding CSS file.
# Defaults to false if Rails is running in production mode,
# true otherwise.
# Only has meaning within Ruby on Rails.
#
# [<tt>:full_exception</tt>] Whether an error in the Sass code
# should cause Sass to provide a detailed description.
# If set to true, the specific error will be displayed
# along with a line number and source snippet.
# Otherwise, a simple uninformative error message will be displayed.
# Defaults to false in production mode, true otherwise.
# Only has meaning within Ruby on Rails or Merb.
#
# [<tt>:template_location</tt>] The directory where Sass templates should be read from.
# Defaults to <tt>RAILS_ROOT + "/public/stylesheets/sass"</tt>
# or <tt>MERB_ROOT + "/public/stylesheets/sass"</tt>.
# Only has meaning within Ruby on Rails or Merb.
#
# [<tt>:css_location</tt>] The directory where CSS output should be written to.
# Defaults to <tt>RAILS_ROOT + "/public/stylesheets"</tt>
# or <tt>MERB_ROOT + "/public/stylesheets"</tt>.
# Only has meaning within Ruby on Rails or Merb.
#
# [<tt>:filename</tt>] The filename of the file being rendered.
# This is used solely for reporting errors,
# and is automatically set when using Rails or Merb.
#
# [<tt>:load_paths</tt>] An array of filesystem paths which should be searched
# for Sass templates imported with the "@import" directive.
# This defaults to the working directory and, in Rails or Merb,
# whatever <tt>:template_location</tt> is.
#
module Sass; end
require 'sass/engine'
require 'sass/plugin' if defined?(Merb::Plugins)

@ -0,0 +1,219 @@
require 'sass/constant/operation'
require 'sass/constant/literal'
module Sass
module Constant # :nodoc:
# The character that begins a constant.
CONSTANT_CHAR = ?!
# Whitespace characters
WHITESPACE = [?\ , ?\t, ?\n, ?\r]
# The character used to escape values
ESCAPE_CHAR = ?\\
# The character used to open and close strings
STRING_CHAR = ?"
# A mapping of syntactically-significant characters
# to parsed symbols
SYMBOLS = {
?( => :open,
?) => :close,
?+ => :plus,
?- => :minus,
?* => :times,
?/ => :div,
?% => :mod,
STRING_CHAR => :str,
ESCAPE_CHAR => :esc
}
# The regular expression used to parse constants
MATCH = /^#{Regexp.escape(CONSTANT_CHAR.chr)}([^\s#{(SYMBOLS.keys + [ ?= ]).map {|c| Regexp.escape("#{c.chr}") }}]+)\s*=\s*(.+)/
# First-order operations
FIRST_ORDER = [:times, :div, :mod]
# Second-order operations
SECOND_ORDER = [:plus, :minus]
class << self
def parse(value, constants, line)
begin
operationalize(parenthesize(tokenize(value)), constants).to_s
rescue Sass::SyntaxError => e
if e.message == "Constant arithmetic error"
e.instance_eval do
@message += ": #{value.dump}"
end
end
e.sass_line = line
raise e
end
end
private
def tokenize(value)
escaped = false
is_string = false
negative_okay = true
str = ''
to_return = []
reset_str = Proc.new do
to_return << str unless str.empty?
''
end
value.each_byte do |byte|
unless escaped
if byte == ESCAPE_CHAR
escaped = true
next
end
last = to_return[-1]
# Do we need to open or close a string literal?
if byte == STRING_CHAR
is_string = !is_string
# Adjacent strings should be concatenated
if is_string && last && (!last.is_a?(Symbol) || last == :close)
to_return << :concat
end
str = reset_str.call
next
end
unless is_string
# Are we looking at whitespace?
if WHITESPACE.include?(byte)
str = reset_str.call
next
end
symbol = SYMBOLS[byte]
# Adjacent values without an operator should be concatenated
if (symbol.nil? || symbol == :open) &&
last && (!last.is_a?(Symbol) || last == :close)
to_return << :concat
end
# Time for a unary minus!
if negative_okay && symbol == :minus
negative_okay = true
to_return << :neg
next
end
# Are we looking at an operator?
if symbol && (str.empty? || symbol != :mod)
str = reset_str.call
negative_okay = true
to_return << symbol
next
end
end
end
escaped = false
negative_okay = false
str << byte.chr
end
if is_string
raise Sass::SyntaxError.new("Unterminated string: #{value.dump}")
end
str = reset_str.call
to_return
end
def parenthesize(value)
parenthesize_helper(0, value, value.length)[0]
end
def parenthesize_helper(i, value, value_len)
to_return = []
beginning = i
token = value[i]
while i < value_len && token != :close
if token == :open
to_return.push(*value[beginning...i])
sub, i = parenthesize_helper(i + 1, value, value_len)
beginning = i
to_return << sub
elsif token == :neg
if value[i + 1].nil?
raise Sass::SyntaxError("Unterminated unary minus.")
elsif value[i + 1] == :open
to_return.push(*value[beginning...i])
sub, i = parenthesize_helper(i + 2, value, value_len)
beginning = i
to_return << [:neg, sub]
else
to_return.push(*value[beginning...i])
to_return << [:neg, value[i + 1]]
beginning = i = i + 2
end
else
i += 1
end
token = value[i]
end
to_return.push(*value[beginning...i])
return to_return, i + 1
end
#--
# TODO: Don't pass around original value;
# have Constant.parse automatically add it to exception.
#++
def operationalize(value, constants)
value = [value] unless value.is_a?(Array)
if value.length == 1
value = value[0]
if value.is_a? Array
operationalize(value, constants)
elsif value.is_a? Operation
value
else
Literal.parse(insert_constant(value, constants))
end
elsif value.length == 2
if value[0] == :neg
Operation.new(Sass::Constant::Number.new('0'), operationalize(value[1], constants), :minus)
else
raise SyntaxError.new("Constant arithmetic error")
end
elsif value.length == 3
Operation.new(operationalize(value[0], constants), operationalize(value[2], constants), value[1])
else
if SECOND_ORDER.include?(value[1]) && FIRST_ORDER.include?(value[3])
operationalize([value[0], value[1], operationalize(value[2..4], constants), *value[5..-1]], constants)
else
operationalize([operationalize(value[0..2], constants), *value[3..-1]], constants)
end
end
end
def insert_constant(value, constants)
to_return = value
if value[0] == CONSTANT_CHAR
to_return = constants[value[1..-1]]
unless to_return
raise SyntaxError.new("Undefined constant: \"#{value}\"")
end
end
to_return
end
end
end
end

@ -0,0 +1,101 @@
require 'sass/constant/literal'
module Sass::Constant # :nodoc:
class Color < Literal # :nodoc:
HTML4_COLORS = {
'black' => 0x000000,
'silver' => 0xc0c0c0,
'gray' => 0x808080,
'white' => 0xffffff,
'maroon' => 0x800000,
'red' => 0xff0000,
'purple' => 0x800080,
'fuchsia' => 0xff00ff,
'green' => 0x008000,
'lime' => 0x00ff00,
'olive' => 0x808000,
'yellow' => 0xffff00,
'navy' => 0x000080,
'blue' => 0x0000ff,
'teal' => 0x008080,
'aqua' => 0x00ffff
}
REGEXP = /\##{"([0-9a-fA-F]{1,2})" * 3}/
def parse(value)
if (value =~ REGEXP)
@value = value.scan(REGEXP)[0].map { |num| num.ljust(2, num).to_i(16) }
else
color = HTML4_COLORS[value.downcase]
@value = (0..2).map{ |n| color >> (n << 3) & 0xff }.reverse
end
end
def plus(other)
if other.is_a? Sass::Constant::String
Sass::Constant::String.from_value(self.to_s + other.to_s)
else
piecewise(other, :+)
end
end
def minus(other)
if other.is_a? Sass::Constant::String
raise NoMethodError.new(nil, :minus)
else
piecewise(other, :-)
end
end
def times(other)
if other.is_a? Sass::Constant::String
raise NoMethodError.new(nil, :times)
else
piecewise(other, :*)
end
end
def div(other)
if other.is_a? Sass::Constant::String
raise NoMethodError.new(nil, :div)
else
piecewise(other, :/)
end
end
def mod(other)
if other.is_a? Sass::Constant::String
raise NoMethodError.new(nil, :mod)
else
piecewise(other, :%)
end
end
def to_s
red, green, blue = @value.map { |num| num.to_s(16).rjust(2, '0') }
"##{red}#{green}#{blue}"
end
protected
def self.filter_value(value)
value.map { |num| num.to_i }
end
private
def piecewise(other, operation)
other_num = other.is_a? Number
other_val = other.value
rgb = []
for i in (0...3)
res = @value[i].send(operation, other_num ? other_val : other_val[i])
rgb[i] = [ [res, 255].min, 0 ].max
end
Color.from_value(rgb)
end
end
end

@ -0,0 +1,53 @@
# Let the subclasses see the superclass
module Sass::Constant; class Literal; end; end; # :nodoc:
require 'sass/constant/string'
require 'sass/constant/number'
require 'sass/constant/color'
class Sass::Constant::Literal # :nodoc:
# The regular expression matching numbers.
NUMBER = /^(-?\d*?\.?)(\d+)([^\d\s]*)$/
html_color_matcher = Sass::Constant::Color::HTML4_COLORS.keys.map { |c| "^#{c}$" }.join '|'
# The regular expression matching colors.
COLOR = /^\# (?: [\da-f]{3} | [\da-f]{6} ) | #{html_color_matcher}/ix
def self.parse(value)
case value
when NUMBER
Sass::Constant::Number.new(value)
when COLOR
Sass::Constant::Color.new(value)
else
Sass::Constant::String.new(value)
end
end
def initialize(value = nil)
self.parse(value) if value
end
def perform
self
end
def concat(other)
Sass::Constant::String.from_value("#{self.to_s} #{other.to_s}")
end
attr_reader :value
protected
def self.filter_value(value)
value
end
def self.from_value(value)
instance = self.new
instance.instance_variable_set('@value', self.filter_value(value))
instance
end
end

@ -0,0 +1,87 @@
require 'sass/constant/literal'
module Sass::Constant # :nodoc:
class Number < Literal # :nodoc:
attr_reader :unit
def parse(value)
first, second, unit = value.scan(Literal::NUMBER)[0]
@value = first.empty? ? second.to_i : "#{first}#{second}".to_f
@unit = unit unless unit.empty?
end
def plus(other)
if other.is_a? Number
operate(other, :+)
elsif other.is_a? Color
other.plus(self)
else
Sass::Constant::String.from_value(self.to_s + other.to_s)
end
end
def minus(other)
if other.is_a? Number
operate(other, :-)
else
raise NoMethodError.new(nil, :minus)
end
end
def times(other)
if other.is_a? Number
operate(other, :*)
elsif other.is_a? Color
other.times(self)
else
raise NoMethodError.new(nil, :times)
end
end
def div(other)
if other.is_a? Number
operate(other, :/)
else
raise NoMethodError.new(nil, :div)
end
end
def mod(other)
if other.is_a? Number
operate(other, :%)
else
raise NoMethodError.new(nil, :mod)
end
end
def to_s
value = @value
value = value.to_i if value % 1 == 0.0
"#{value}#{@unit}"
end
protected
def self.from_value(value, unit=nil)
instance = super(value)
instance.instance_variable_set('@unit', unit)
instance
end
def operate(other, operation)
unit = nil
if other.unit.nil?
unit = self.unit
elsif self.unit.nil?
unit = other.unit
elsif other.unit == self.unit
unit = self.unit
else
raise Sass::SyntaxError.new("Incompatible units: #{self.unit} and #{other.unit}")
end
Number.from_value(self.value.send(operation, other.value), unit)
end
end
end

@ -0,0 +1,30 @@
require 'sass/constant/string'
require 'sass/constant/number'
require 'sass/constant/color'
module Sass::Constant # :nodoc:
class Operation # :nodoc:
def initialize(operand1, operand2, operator)
@operand1 = operand1
@operand2 = operand2
@operator = operator
end
def to_s
self.perform.to_s
end
protected
def perform
literal1 = @operand1.perform
literal2 = @operand2.perform
begin
literal1.send(@operator, literal2)
rescue NoMethodError => e
raise e unless e.name.to_s == @operator.to_s
raise Sass::SyntaxError.new("Undefined operation: \"#{literal1} #{@operator} #{literal2}\"")
end
end
end
end

@ -0,0 +1,18 @@
require 'sass/constant/literal'
module Sass::Constant # :nodoc:
class String < Literal # :nodoc:
def parse(value)
@value = value
end
def plus(other)
Sass::Constant::String.from_value(self.to_s + other.to_s)
end
def to_s
@value
end
end
end

@ -0,0 +1,197 @@
require File.dirname(__FILE__) + '/../sass'
require 'sass/tree/node'
require 'strscan'
module Sass
# :stopdoc:
module Tree
class Node
def to_sass
result = ''
children.each do |child|
result << "#{child.to_sass(0)}\n"
end
result
end
end
class ValueNode
def to_sass(tabs)
"#{value}\n"
end
end
class RuleNode
def to_sass(tabs)
str = "#{' ' * tabs}#{rule}\n"
children.each do |child|
str << "#{child.to_sass(tabs + 1)}"
end
str
end
end
class AttrNode
def to_sass(tabs)
"#{' ' * tabs}:#{name} #{value}\n"
end
end
end
# :startdoc:
# This class contains the functionality used in the +css2sass+ utility,
# namely converting CSS documents to Sass templates.
class CSS
# :stopdoc:
# The Regexp matching a CSS rule
RULE_RE = /\s*([^\{]+)\s*\{/
# The Regexp matching a CSS attribute
ATTR_RE = /\s*[^::\{\}]+\s*:\s*[^:;\{\}]+\s*;/
# :startdoc:
# Creates a new instance of Sass::CSS that will compile the given document
# to a Sass string when +render+ is called.
def initialize(template)
if template.is_a? IO
template = template.read
end
@template = StringScanner.new(template)
end
# Processes the document and returns the result as a string
# containing the CSS template.
def render
begin
build_tree.to_sass
rescue Exception => err
line = @template.string[0...@template.pos].split("\n").size
err.backtrace.unshift "(css):#{line}"
raise err
end
end
private
def build_tree
root = Tree::Node.new(nil)
whitespace
directives(root)
rules(root)
sort_rules(root)
root
end
def directives(root)
while @template.scan(/@/)
name = @template.scan /[^\s;]+/
whitespace
value = @template.scan /[^;]+/
assert_match /;/
whitespace
if name == "import" && value =~ /^(url\()?"?([^\s\(\)\"]+)\.css"?\)?$/
value = $2
end
root << Tree::ValueNode.new("@#{name} #{value};", nil)
end
end
def rules(root)
rules = []
while @template.scan(/[^\{\s]+/)
rules << @template[0]
whitespace
if @template.scan(/\{/)
result = Tree::RuleNode.new(rules.join(' '), nil)
root << result
rules = []
whitespace
attributes(result)
end
end
end
def attributes(rule)
while @template.scan(/[^:\}\s]+/)
name = @template[0]
whitespace
assert_match /:/
value = ''
while @template.scan(/[^;\s]+/)
value << @template[0] << whitespace
end
assert_match /;/
rule << Tree::AttrNode.new(name, value, nil)
end
assert_match /\}/
end
def whitespace
space = @template.scan(/\s*/) || ''
# If we've hit a comment,
# go past it and look for more whitespace
if @template.scan(/\/\*/)
@template.scan_until(/\*\//)
return space + whitespace
end
return space
end
def assert_match(re)
if !@template.scan(re)
raise Exception.new("Invalid CSS!")
end
whitespace
end
def sort_rules(root)
root.children.sort! do |c1, c2|
if c1.is_a?(Tree::RuleNode) && c2.is_a?(Tree::RuleNode)
c1.rule <=> c2.rule
elsif !(c1.is_a?(Tree::RuleNode) || c2.is_a?(Tree::RuleNode)) || c2.is_a?(Tree::RuleNode)
-1
else
1
end
end
prev_rules = []
prev_rule_values = []
root.children.each do |child|
if child.is_a? Tree::RuleNode
joined_prev_values = prev_rule_values.join(' ')
until prev_rules.empty? || child.rule =~ /^#{Regexp.escape(joined_prev_values)}/
prev_rules.pop
prev_rule_values.pop
end
unless prev_rules.empty?
child.rule.slice!(0..(joined_prev_values.size))
prev_rules[-1] << child
root.children.delete child
end
prev_rules << child
prev_rule_values << child.rule
end
end
end
end
end

@ -0,0 +1,354 @@
require 'sass/tree/node'
require 'sass/tree/value_node'
require 'sass/tree/rule_node'
require 'sass/tree/comment_node'
require 'sass/tree/attr_node'
require 'sass/constant'
require 'sass/error'
require 'haml/util'
module Sass
# This is the class where all the parsing and processing of the Sass
# template is done. It can be directly used by the user by creating a
# new instance and calling <tt>render</tt> to render the template. For example:
#
# template = File.load('stylesheets/sassy.sass')
# sass_engine = Sass::Engine.new(template)
# output = sass_engine.render
# puts output
class Engine
# The character that begins a CSS attribute.
ATTRIBUTE_CHAR = ?:
# The character that designates that
# an attribute should be assigned to the result of constant arithmetic.
SCRIPT_CHAR = ?=
# The character that designates the beginning of a comment,
# either Sass or CSS.
COMMENT_CHAR = ?/
# The character that follows the general COMMENT_CHAR and designates a Sass comment,
# which is not output as a CSS comment.
SASS_COMMENT_CHAR = ?/
# The character that follows the general COMMENT_CHAR and designates a CSS comment,
# which is embedded in the CSS document.
CSS_COMMENT_CHAR = ?*
# The character used to denote a compiler directive.
DIRECTIVE_CHAR = ?@
# The regex that matches and extracts data from
# attributes of the form <tt>:name attr</tt>.
ATTRIBUTE = /^:([^\s=:]+)\s*(=?)(?:\s+|$)(.*)/
# The regex that matches attributes of the form <tt>name: attr</tt>.
ATTRIBUTE_ALTERNATE_MATCHER = /^[^\s:]+\s*[=:](\s|$)/
# The regex that matches and extracts data from
# attributes of the form <tt>name: attr</tt>.
ATTRIBUTE_ALTERNATE = /^([^\s=:]+)(\s*=|:)(?:\s+|$)(.*)/
# Creates a new instace of Sass::Engine that will compile the given
# template string when <tt>render</tt> is called.
# See README for available options.
#
#--
#
# TODO: Add current options to REFRENCE. Remember :filename!
#
# When adding options, remember to add information about them
# to README!
#++
#
def initialize(template, options={})
@options = {
:style => :nested,
:load_paths => ['.']
}.merge! options
@template = template.split(/\n\r|\n/)
@lines = []
@constants = {}
end
# Processes the template and returns the result as a string.
def render
begin
render_to_tree.to_s
rescue SyntaxError => err
unless err.sass_filename
err.add_backtrace_entry(@options[:filename])
end
raise err
end
end
alias_method :to_css, :render
protected
def constants
@constants
end
def render_to_tree
split_lines
root = Tree::Node.new(@options[:style])
index = 0
while @lines[index]
child, index = build_tree(index)
if child.is_a? Tree::Node
child.line = index
root << child
elsif child.is_a? Array
child.each do |c|
root << c
end
end
end
@line = nil
root
end
private
# Readies each line in the template for parsing,
# and computes the tabulation of the line.
def split_lines
@line = 0
old_tabs = 0
@template.each_with_index do |line, index|
@line += 1
tabs = count_tabs(line)
if line[0] == COMMENT_CHAR && line[1] == SASS_COMMENT_CHAR && tabs == 0
tabs = old_tabs
end
if tabs # if line isn't blank
if tabs - old_tabs > 1
raise SyntaxError.new("Illegal Indentation: Only two space characters are allowed as tabulation.", @line)
end
@lines << [line.strip, tabs]
old_tabs = tabs
else
@lines << ['//', old_tabs]
end
end
@line = nil
end
# Counts the tabulation of a line.
def count_tabs(line)
spaces = line.index(/[^ ]/)
if spaces
if spaces % 2 == 1 || line[spaces] == ?\t
# Make sure a line with just tabs isn't an error
return nil if line.strip.empty?
raise SyntaxError.new("Illegal Indentation: Only two space characters are allowed as tabulation.", @line)
end
spaces / 2
else
nil
end
end
def build_tree(index)
line, tabs = @lines[index]
index += 1
@line = index
node = parse_line(line)
has_children = has_children?(index, tabs)
# Node is a symbol if it's non-outputting, like a constant assignment
unless node.is_a? Tree::Node
if has_children
if node == :constant
raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath constants.", @line)
elsif node.is_a? Array
raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath import directives.", @line)
end
end
return node, index
end
if node.is_a? Tree::CommentNode
while has_children
line, index = raw_next_line(index)
node << line
has_children = has_children?(index, tabs)
end
else
while has_children
child, index = build_tree(index)
if child == :constant
raise SyntaxError.new("Constants may only be declared at the root of a document.", @line)
elsif child.is_a? Array
raise SyntaxError.new("Import directives may only be used at the root of a document.", @line)
elsif child.is_a? Tree::Node
child.line = @line
node << child
end
has_children = has_children?(index, tabs)
end
end
return node, index
end
def has_children?(index, tabs)
next_line = ['//', 0]
while !next_line.nil? && next_line[0] == '//' && next_line[1] = 0
next_line = @lines[index]
index += 1
end
next_line && next_line[1] > tabs
end
def raw_next_line(index)
[@lines[index][0], index + 1]
end
def parse_line(line)
case line[0]
when ATTRIBUTE_CHAR
parse_attribute(line, ATTRIBUTE)
when Constant::CONSTANT_CHAR
parse_constant(line)
when COMMENT_CHAR
parse_comment(line)
when DIRECTIVE_CHAR
parse_directive(line)
else
if line =~ ATTRIBUTE_ALTERNATE_MATCHER
parse_attribute(line, ATTRIBUTE_ALTERNATE)
else
Tree::RuleNode.new(line, @options[:style])
end
end
end
def parse_attribute(line, attribute_regx)
name, eq, value = line.scan(attribute_regx)[0]
if name.nil? || value.nil?
raise SyntaxError.new("Invalid attribute: \"#{line}\"", @line)
end
if eq.strip[0] == SCRIPT_CHAR
value = Sass::Constant.parse(value, @constants, @line).to_s
end
Tree::AttrNode.new(name, value, @options[:style])
end
def parse_constant(line)
name, value = line.scan(Sass::Constant::MATCH)[0]
unless name && value
raise SyntaxError.new("Invalid constant: \"#{line}\"", @line)
end
@constants[name] = Sass::Constant.parse(value, @constants, @line)
:constant
end
def parse_comment(line)
if line[1] == SASS_COMMENT_CHAR
:comment
elsif line[1] == CSS_COMMENT_CHAR
Tree::CommentNode.new(line, @options[:style])
else
Tree::RuleNode.new(line, @options[:style])
end
end
def parse_directive(line)
directive, value = line[1..-1].split(/\s+/, 2)
case directive
when "import"
import(value)
else
raise SyntaxError.new("Unknown compiler directive: #{"@#{directive} #{value}".dump}", @line)
end
end
def import(files)
nodes = []
files.split(/,\s*/).each do |filename|
engine = nil
filename = find_file_to_import(filename)
if filename =~ /\.css$/
nodes << Tree::ValueNode.new("@import url(#{filename});", @options[:style])
else
File.open(filename) do |file|
new_options = @options.dup
new_options[:filename] = filename
engine = Sass::Engine.new(file.read, @options)
end
engine.constants.merge! @constants
begin
root = engine.render_to_tree
rescue Sass::SyntaxError => err
err.add_backtrace_entry(filename)
raise err
end
root.children.each do |child|
child.filename = filename
nodes << child
end
@constants = engine.constants
end
end
nodes
end
def find_file_to_import(filename)
was_sass = false
original_filename = filename
new_filename = nil
if filename[-5..-1] == ".sass"
filename = filename[0...-5]
was_sass = true
elsif filename[-4..-1] == ".css"
return filename
end
@options[:load_paths].each do |path|
full_path = File.join(path, filename) + '.sass'
if File.readable?(full_path)
new_filename = full_path
break
end
end
if new_filename.nil?
if was_sass
raise SyntaxError.new("File to import not found or unreadable: #{original_filename}", @line)
else
return filename + '.css'
end
else
new_filename
end
end
end
end

@ -0,0 +1,35 @@
module Sass
# Sass::SyntaxError encapsulates information about the exception,
# such as the line of the Sass template it was raised on
# and the Sass file that was being parsed (if applicable).
# It also provides a handy way to rescue only exceptions raised
# because of a faulty template.
class SyntaxError < StandardError
# The line of the Sass template on which the exception was thrown.
attr_accessor :sass_line
# The name of the file that was being parsed when the exception was raised.
# This will be nil unless Sass is being used as an ActionView plugin.
attr_reader :sass_filename
# Creates a new SyntaxError.
# +lineno+ should be the line of the Sass template on which the error occurred.
def initialize(msg, lineno = nil)
@message = msg
@sass_line = lineno
end
# Adds a properly formatted entry to the exception's backtrace.
# +filename+ should be the file in which the error occurred,
# if applicable (defaults to "(sass)").
def add_backtrace_entry(filename) # :nodoc:
@sass_filename ||= filename
self.backtrace ||= []
self.backtrace.unshift "#{@sass_filename || '(sass)'}:#{@sass_line}"
end
def to_s # :nodoc:
@message
end
end
end

@ -0,0 +1,111 @@
require 'sass/engine'
module Sass
# This module contains methods to aid in using Sass
# as a stylesheet-rendering plugin for various systems.
# Currently Rails/ActionController and Merb are supported out of the box.
module Plugin
class << self
@@options = {
:template_location => './public/stylesheets/sass',
:css_location => './public/stylesheets',
:always_update => false,
:always_check => true,
:full_exception => true
}
# Gets various options for Sass. See README for details.
#--
# TODO: *DOCUMENT OPTIONS*
#++
def options
@@options
end
# Sets various options for Sass.
def options=(value)
@@options.merge!(value)
end
# Checks each stylesheet in <tt>options[:css_location]</tt>
# to see if it needs updating,
# and updates it using the corresponding template
# from <tt>options[:templates]</tt>
# if it does.
def update_stylesheets
Dir.glob(File.join(options[:template_location], "**", "*.sass")).entries.each do |file|
# Get the relative path to the file with no extension
name = file.sub(options[:template_location] + "/", "")[0...-5]
if options[:always_update] || stylesheet_needs_update?(name)
css = css_filename(name)
File.delete(css) if File.exists?(css)
filename = template_filename(name)
l_options = @@options.dup
l_options[:filename] = filename
l_options[:load_paths] = (l_options[:load_paths] || []) + [l_options[:template_location]]
engine = Engine.new(File.read(filename), l_options)
begin
result = engine.render
rescue Exception => e
if options[:full_exception]
e_string = "#{e.class}: #{e.message}"
if e.is_a? Sass::SyntaxError
e_string << "\non line #{e.sass_line}"
if e.sass_filename
e_string << " of #{e.sass_filename}"
if File.exists?(e.sass_filename)
e_string << "\n\n"
min = [e.sass_line - 5, 0].max
File.read(e.sass_filename).rstrip.split("\n")[
min .. e.sass_line + 5
].each_with_index do |line, i|
e_string << "#{min + i + 1}: #{line}\n"
end
end
end
end
result = "/*\n#{e_string}\n\nBacktrace:\n#{e.backtrace.join("\n")}\n*/"
else
result = "/* Internal stylesheet error */"
end
end
# Create any directories that might be necessary
dirs = [l_options[:css_location]]
name.split("/")[0...-1].each { |dir| dirs << "#{dirs[-1]}/#{dir}" }
dirs.each { |dir| Dir.mkdir(dir) unless File.exist?(dir) }
# Finally, write the file
File.open(css, 'w') do |file|
file.print(result)
end
end
end
end
private
def template_filename(name)
"#{@@options[:template_location]}/#{name}.sass"
end
def css_filename(name)
"#{@@options[:css_location]}/#{name}.css"
end
def stylesheet_needs_update?(name)
!File.exists?(css_filename(name)) || (File.mtime(template_filename(name)) - 2) > File.mtime(css_filename(name))
end
end
end
end
require 'sass/plugin/rails' if defined?(ActionController)
require 'sass/plugin/merb' if defined?(Merb::Plugins)

@ -0,0 +1,20 @@
unless defined?(Sass::MERB_LOADED)
Sass::MERB_LOADED = true
Sass::Plugin.options.merge!(:template_location => MERB_ROOT + '/public/stylesheets/sass',
:css_location => MERB_ROOT + '/public/stylesheets',
:always_check => MERB_ENV != "production",
:full_exception => MERB_ENV != "production")
config = Merb::Plugins.config[:sass] || Merb::Plugins.config["sass"] || {}
config.symbolize_keys!
Sass::Plugin.options.merge!(config)
class MerbHandler # :nodoc:
def process_with_sass(request, response)
Sass::Plugin.update_stylesheets if Sass::Plugin.options[:always_update] || Sass::Plugin.options[:always_check]
process_without_sass(request, response)
end
alias_method :process_without_sass, :process
alias_method :process, :process_with_sass
end
end

@ -0,0 +1,18 @@
unless defined?(Sass::RAILS_LOADED)
Sass::RAILS_LOADED = true
Sass::Plugin.options.merge!(:template_location => RAILS_ROOT + '/public/stylesheets/sass',
:css_location => RAILS_ROOT + '/public/stylesheets',
:always_check => RAILS_ENV != "production",
:full_exception => RAILS_ENV != "production")
module ActionController # :nodoc:
class Base # :nodoc:
alias_method :sass_old_process, :process
def process(*args)
Sass::Plugin.update_stylesheets if Sass::Plugin.options[:always_update] || Sass::Plugin.options[:always_check]
sass_old_process(*args)
end
end
end
end

@ -0,0 +1,52 @@
require 'sass/tree/node'
module Sass::Tree
class AttrNode < ValueNode
attr_accessor :name
def initialize(name, value, style)
@name = name
super(value, style)
end
def to_s(parent_name = nil)
if value[-1] == ?;
raise Sass::SyntaxError.new("Invalid attribute: #{declaration.dump} (This isn't CSS!)", @line)
end
real_name = name
real_name = "#{parent_name}-#{real_name}" if parent_name
if value.empty? && children.empty?
raise Sass::SyntaxError.new("Invalid attribute: #{declaration.dump}", @line)
end
join_string = @style == :compact ? ' ' : "\n"
to_return = ''
if !value.empty?
to_return << "#{real_name}: #{value};#{join_string}"
end
children.each do |kid|
if @style == :compact
to_return << "#{kid.to_s(real_name)} "
else
to_return << "#{kid.to_s(real_name)}\n"
end
end
to_return << "\n" unless children.empty? || @style == :compact
to_return[0...-1]
end
private
def declaration
":#{name} #{value}"
end
def invalid_child?(child)
if !child.is_a?(AttrNode)
"Illegal nesting: Only attributes may be nested beneath attributes."
end
end
end
end

@ -0,0 +1,14 @@
require 'sass/tree/node'
module Sass::Tree
class CommentNode < ValueNode
def initialize(value, style)
super(value[2..-1].strip, style)
end
def to_s(parent_name = nil)
join_string = @style == :compact ? ' ' : "\n * "
"/* #{value}#{join_string unless children.empty?}#{children.join join_string} */"
end
end
end

@ -0,0 +1,46 @@
module Sass
module Tree
class Node
attr_accessor :children
attr_accessor :line
attr_accessor :filename
def initialize(style)
@style = style
@children = []
end
def <<(child)
if msg = invalid_child?(child)
raise Sass::SyntaxError.new(msg, child.line)
end
@children << child
end
def to_s
result = String.new
children.each do |child|
if child.is_a? AttrNode
raise SyntaxError.new('Attributes aren\'t allowed at the root of a document.', child.line)
end
begin
result += "#{child.to_s(1)}\n"
rescue SyntaxError => e
raise e
end
end
result[0...-1]
end
private
# This method should be overridden by subclasses to return an error message
# if the given child node is invalid,
# and false or nil otherwise.
def invalid_child?(child)
false
end
end
end
end

@ -0,0 +1,59 @@
require 'sass/tree/node'
require 'sass/tree/attr_node'
module Sass::Tree
class RuleNode < ValueNode
# The character used to include the parent selector
PARENT = '&'
alias_method :rule, :value
alias_method :rule=, :value=
def to_s(tabs, super_rules = nil)
attributes = []
sub_rules = []
total_rule = if super_rules
super_rules.split(/,\s*/).collect! do |s|
self.rule.split(/,\s*/).collect do |r|
if r.include?(PARENT)
r.gsub(PARENT, s)
else
"#{s} #{r}"
end
end.join(", ")
end.join(", ")
elsif self.rule.include?(PARENT)
raise Sass::SyntaxError.new("Base-level rules cannot contain the parent-selector-referencing character '#{PARENT}'", line)
else
self.rule
end
children.each do |child|
if child.is_a? RuleNode
sub_rules << child
else
attributes << child
end
end
to_return = ''
unless attributes.empty?
if @style == :compact
to_return << "#{total_rule} { #{attributes.join(' ')} }\n"
else
spaces = (@style == :expanded ? 2 : tabs * 2)
old_spaces = ' ' * (spaces - 2)
spaces = ' ' * spaces
attributes = attributes.join("\n").gsub("\n", "\n#{spaces}").rstrip
end_attrs = (@style == :expanded ? "\n" : ' ')
to_return << "#{old_spaces}#{total_rule} {\n#{spaces}#{attributes}#{end_attrs}}\n"
end
end
tabs += 1 unless attributes.empty?
sub_rules.each { |sub| to_return << sub.to_s(tabs, total_rule) }
to_return
end
end
end

@ -0,0 +1,16 @@
require 'sass/tree/node'
module Sass::Tree
class ValueNode < Node
attr_accessor :value
def initialize(value, style)
@value = value
super(style)
end
def to_s(tabs = 0)
value
end
end
end

@ -0,0 +1,62 @@
require 'rubygems'
require 'active_support'
require 'action_controller'
require 'action_view'
require File.dirname(__FILE__) + '/../lib/haml'
require 'haml/template'
require 'sass/engine'
require 'benchmark'
require 'stringio'
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, and Sass on its own.
#
# Returns the results of the benchmarking as a string.
#
def benchmark(runs = 100)
template_name = 'standard'
haml_template = "haml/templates/#{template_name}"
rhtml_template = "haml/rhtml/#{template_name}"
sass_template = File.dirname(__FILE__) + "/sass/templates/complex.sass"
old_stdout = $stdout
$stdout = StringIO.new
times = Benchmark.bmbm do |b|
b.report("haml:") { runs.times { @base.render haml_template } }
b.report("erb:") { runs.times { @base.render rhtml_template } }
end
#puts times[0].inspect, times[1].inspect
ratio = sprintf("%g", times[0].to_a[5] / times[1].to_a[5])
puts "Haml/ERB: " + ratio
puts '', '-' * 50, 'Sass on its own', '-' * 50
Benchmark.bmbm do |b|
b.report("sass:") { runs.times { Sass::Engine.new(File.read(sass_template)).render } }
end
$stdout.pos = 0
to_return = $stdout.read
$stdout = old_stdout
to_return
end
end
end

@ -0,0 +1,261 @@
#!/usr/bin/env ruby
require 'rubygems'
require 'active_support'
require 'action_controller'
require 'action_view'
require 'test/unit'
require File.dirname(__FILE__) + '/../../lib/haml'
require 'haml/engine'
class EngineTest < Test::Unit::TestCase
def render(text, options = {})
Haml::Engine.new(text, options).to_html
end
def test_empty_render_should_remain_empty
assert_equal('', render(''))
end
# 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("<div class='atlantis' style='ugly'>\n</div>", render(".atlantis{:style => 'ugly'}").chomp)
rescue
assert_equal("<div style='ugly' class='atlantis'>\n</div>", render(".atlantis{:style => 'ugly'}").chomp)
end
def test_ruby_code_should_work_inside_attributes
author = 'hcatlin'
assert_equal("<p class='3'>foo</p>", render("%p{:class => 1+2} foo").chomp)
end
def test_nil_should_render_empty_tag
assert_equal("<div class='no_attributes'>\n</div>",
render(".no_attributes{:nil => nil}").chomp)
end
def test_strings_should_get_stripped_inside_tags
assert_equal("<div class='stripped'>This should have no spaces in front of it</div>",
render(".stripped This should have no spaces in front of it").chomp)
end
def test_one_liner_should_be_one_line
assert_equal("<p>Hello</p>", render('%p Hello').chomp)
end
def test_long_liner_should_not_print_on_one_line
assert_equal("<div>\n #{'x' * 51}\n</div>", render("%div #{'x' * 51}").chomp)
end
def test_multi_render
engine = Haml::Engine.new("%strong Hi there!")
assert_equal("<strong>Hi there!</strong>\n", engine.to_html)
assert_equal("<strong>Hi there!</strong>\n", engine.to_html)
assert_equal("<strong>Hi there!</strong>\n", engine.to_html)
end
def test_double_equals
assert_equal("<p>Hello World</p>\n", render('%p== Hello #{who}', :locals => {:who => 'World'}))
assert_equal("<p>\n Hello World\n</p>\n", render("%p\n == Hello \#{who}", :locals => {:who => 'World'}))
end
def test_nil_tag_value_should_render_as_empty
assert_equal("<p></p>\n", render("%p= nil"))
end
def test_tag_with_failed_if_should_render_as_empty
assert_equal("<p></p>\n", render("%p= 'Hello' if false"))
end
# Options tests
def test_stop_eval
assert_equal("", render("= 'Hello'", :suppress_eval => true))
assert_equal("", render("- puts 'foo'", :suppress_eval => true))
assert_equal("<div id='foo' yes='no' />\n", render("#foo{:yes => 'no'}/", :suppress_eval => true))
assert_equal("<div id='foo' />\n", render("#foo{:yes => 'no', :call => a_function() }/", :suppress_eval => true))
assert_equal("<div />\n", render("%div[1]/", :suppress_eval => true))
begin
assert_equal("", render(":ruby\n puts 'hello'", :suppress_eval => true))
rescue Haml::HamlError => err
caught = true
assert_equal('Filter "ruby" is not defined!', err.message)
end
assert(caught, "Rendering a ruby filter without evaluating didn't throw an error!")
end
def test_attr_wrapper
assert_equal("<p strange=*attrs*>\n</p>\n", render("%p{ :strange => 'attrs'}", :attr_wrapper => '*'))
assert_equal("<p escaped='quo\"te'>\n</p>\n", render("%p{ :escaped => 'quo\"te'}", :attr_wrapper => '"'))
assert_equal("<p escaped=\"q'uo&quot;te\">\n</p>\n", render("%p{ :escaped => 'q\\'uo\"te'}", :attr_wrapper => '"'))
assert_equal("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n", render("!!! XML", :attr_wrapper => '"'))
end
def test_attrs_parsed_correctly
assert_equal("<p boom=>biddly='bar => baz'>\n</p>\n", render("%p{'boom=>biddly' => 'bar => baz'}"))
assert_equal("<p foo,bar='baz, qux'>\n</p>\n", render("%p{'foo,bar' => 'baz, qux'}"))
assert_equal("<p escaped='quo\nte'>\n</p>\n", render("%p{ :escaped => \"quo\\nte\"}"))
assert_equal("<p escaped='quo4te'>\n</p>\n", render("%p{ :escaped => \"quo\#{2 + 2}te\"}"))
end
def test_locals
assert_equal("<p>Paragraph!</p>\n", render("%p= text", :locals => { :text => "Paragraph!" }))
end
def test_recompile_with_new_locals
template = "%p= (text == 'first time') ? text : new_text"
assert_equal("<p>first time</p>\n", render(template, :locals => { :text => "first time" }))
assert_equal("<p>second time</p>\n", render(template, :locals => { :text => "recompile", :new_text => "second time" }))
# Make sure the method called will return junk unless recompiled
method_name = Haml::Engine.send(:class_variable_get, '@@method_names')[template]
Haml::Engine::CompiledTemplates.module_eval "def #{method_name}(stuff); @haml_stack[-1].push_text 'NOT RECOMPILED', 0; end"
assert_equal("NOT RECOMPILED\n", render(template, :locals => { :text => "first time" }))
assert_equal("<p>first time</p>\n", render(template, :locals => { :text => "first time", :foo => 'bar' }))
end
def test_dynamc_attrs_shouldnt_register_as_literal_values
assert_equal("<p a='b2c'>\n</p>\n", render('%p{:a => "b#{1 + 1}c"}'))
assert_equal("<p a='b2c'>\n</p>\n", render("%p{:a => 'b' + (1 + 1).to_s + 'c'}"))
end
def test_rec_merge
hash1 = {1=>2, 3=>{5=>7, 8=>9}}
hash2 = {4=>5, 3=>{5=>2, 16=>12}}
hash3 = {1=>2, 4=>5, 3=>{5=>2, 8=>9, 16=>12}}
hash1.rec_merge!(hash2)
assert_equal(hash3, hash1)
end
def test_exception_type
begin
render("%p hi\n= undefined")
rescue Exception => e
assert(e.is_a?(Haml::Error))
assert_equal(2, e.haml_line)
assert_equal(nil, e.haml_filename)
assert_equal('(haml):2', e.backtrace[0])
else
# Test failed... should have raised an exception
assert(false)
end
end
def test_syntax_errors
errs = [ "!!!\n a", "a\n b", "a\n:foo\nb", "/ a\n b",
"% a", "%p a\n b", "a\n%p=\nb", "%p=\n a",
"a\n%p~\nb", "a\n~\nb", "a\n~\n b", "%p~\n b", "%p/\n a",
"%p\n \t%a b", "%a\n b\nc", "%a\n b\nc",
":notafilter\n This isn't\n a filter!",
".{} a", "\#{} a", ".= 'foo'", "%a/ b", "%p..class", "%p..#." ]
errs.each do |err|
begin
render(err)
rescue Exception => e
assert(e.is_a?(Haml::Error),
"#{err.dump} doesn't produce Haml::SyntaxError!")
else
assert(false,
"#{err.dump} doesn't produce an exception!")
end
end
end
def test_compile_error
begin
render("a\nb\n- fee do\nc")
rescue Exception => e
assert_equal(3, e.haml_line)
else
assert(false,
'"a\nb\n- fee do\nc" doesn\'t produce an exception!')
end
end
def test_no_bluecloth
old_markdown = false
if defined?(Haml::Filters::Markdown)
old_markdown = Haml::Filters::Markdown
end
Kernel.module_eval do
alias_method :haml_old_require, :gem_original_require
def gem_original_require(file)
raise LoadError if file == 'bluecloth'
haml_old_require(file)
end
end
if old_markdown
Haml::Filters.instance_eval do
remove_const 'Markdown'
end
end
# This is purposefully redundant, so it doesn't stop
# haml/filters from being required later on.
require 'haml/../haml/filters'
assert_equal("<h1>Foo</h1>\t<p>- a\n- b</p>\n",
Haml::Engine.new(":markdown\n Foo\n ===\n - a\n - b").to_html)
Haml::Filters.instance_eval do
remove_const 'Markdown'
end
Haml::Filters.const_set('Markdown', old_markdown) if old_markdown
Kernel.module_eval do
alias_method :gem_original_require, :haml_old_require
end
NOT_LOADED.delete 'bluecloth'
end
def test_no_redcloth
Kernel.module_eval do
alias_method :haml_old_require2, :gem_original_require
def gem_original_require(file)
raise LoadError if file == 'redcloth'
haml_old_require2(file)
end
end
# This is purposefully redundant, so it doesn't stop
# haml/filters from being required later on.
require 'haml/../haml/../haml/filters'
begin
Haml::Engine.new(":redcloth\n _foo_").to_html
rescue Haml::HamlError
else
assert(false, "No exception raised!")
end
Kernel.module_eval do
alias_method :gem_original_require2, :haml_old_require
end
NOT_LOADED.delete 'redcloth'
end
def test_local_assigns_dont_modify_class
assert_equal("bar\n", render("= foo", :locals => {:foo => 'bar'}))
assert_equal(nil, defined?(foo))
end
def test_object_ref_with_nil_id
user = Struct.new('User', :id).new
assert_equal("<p class='struct_user' id='struct_user_new'>New User</p>\n",
render("%p[user] New User", :locals => {:user => user}))
end
end

@ -0,0 +1,123 @@
#!/usr/bin/env ruby
require 'rubygems'
require 'active_support'
require 'action_controller'
require 'action_view'
require 'test/unit'
require File.dirname(__FILE__) + '/../../lib/haml'
require 'haml/template'
class HelperTest < Test::Unit::TestCase
include Haml::Helpers
def setup
ActionView::Base.register_template_handler("haml", Haml::Template)
@base = ActionView::Base.new
@base.controller = ActionController::Base.new
end
def render(text, options = {})
if options == :action_view
@base.render :inline => text, :type => :haml
else
scope = options.delete :scope_object
Haml::Engine.new(text, options).to_html(scope ? scope : Object.new)
end
end
def test_flatten
assert_equal(flatten("FooBar"), "FooBar")
assert_equal(flatten("Foo\rBar"), "FooBar")
assert_equal(flatten("Foo\nBar"), "Foo&#x000A;Bar")
assert_equal(flatten("Hello\nWorld!\nYOU ARE \rFLAT?\n\rOMGZ!"),
"Hello&#x000A;World!&#x000A;YOU ARE FLAT?&#x000A;OMGZ!")
end
def test_list_of_should_render_correctly
assert_equal("<li>1</li>\n<li>2</li>\n", render("= list_of([1, 2]) do |i|\n = i"))
assert_equal("<li>[1]</li>\n", render("= list_of([[1]]) do |i|\n = i.inspect"))
assert_equal("<li>\n <h1>Fee</h1>\n <p>A word!</p>\n</li>\n<li>\n <h1>Fi</h1>\n <p>A word!</p>\n</li>\n<li>\n <h1>Fo</h1>\n <p>A word!</p>\n</li>\n<li>\n <h1>Fum</h1>\n <p>A word!</p>\n</li>\n",
render("= list_of(['Fee', 'Fi', 'Fo', 'Fum']) do |title|\n %h1= title\n %p A word!"))
end
def test_buffer_access
assert(render("= buffer") =~ /#<Haml::Buffer:0x[a-z0-9]+>/)
assert_equal(render("= (buffer == _hamlout)"), "true\n")
end
def test_tabs
assert_equal(render("foo\n- tab_up\nbar\n- tab_down\nbaz"), "foo\n bar\nbaz\n")
end
def test_helpers_dont_leak
# Haml helpers shouldn't be accessible from ERB
render("foo")
proper_behavior = false
begin
ActionView::Base.new.render(:inline => "<%= flatten('Foo\\nBar') %>")
rescue NoMethodError
proper_behavior = true
end
assert(proper_behavior)
begin
ActionView::Base.new.render(:inline => "<%= concat('foo') %>")
rescue ArgumentError, NameError
proper_behavior = true
end
assert(proper_behavior)
end
def test_action_view_included
assert(Haml::Helpers.action_view?)
end
def test_form_tag
result = render("- form_tag 'foo' do\n %p bar\n %strong baz", :action_view)
should_be = "<form action=\"foo\" method=\"post\">\n <p>bar</p>\n <strong>baz</strong>\n</form>\n"
assert_equal(should_be, result)
end
def test_capture_haml
assert_equal("\"<p>13</p>\\n\"\n", render("- foo = capture_haml(13) do |a|\n %p= a\n= foo.dump"))
end
def test_is_haml
assert(!ActionView::Base.new.is_haml?)
assert_equal("true\n", render("= is_haml?"))
assert_equal("true\n", render("= is_haml?", :action_view))
assert_equal("false", @base.render(:inline => '<%= is_haml? %>'))
assert_equal("false\n", render("= render :inline => '<%= is_haml? %>'", :action_view))
end
def test_page_class
controller = Struct.new(:controller_name, :action_name).new('troller', 'tion')
scope = Struct.new(:controller).new(controller)
result = render("%div{:class => page_class} MyDiv", :scope_object => scope)
expected = "<div class='troller tion'>MyDiv</div>\n"
assert_equal expected, result
end
def test_indented_capture
assert_equal(" \n Foo\n ", @base.render(:inline => " <% res = capture do %>\n Foo\n <% end %><%= res %>"))
end
def test_capture_deals_properly_with_collections
Haml::Helpers.module_eval do
def trc(collection, &block)
collection.each do |record|
puts capture_haml(record, &block)
end
end
end
assert_equal("1\n\n2\n\n3\n\n", render("- trc([1, 2, 3]) do |i|\n = i.inspect"))
end
end

@ -0,0 +1,6 @@
class Article
attr_accessor :id, :title, :body
def initialize
@id, @title, @body = 1, 'Hello', 'World'
end
end

@ -0,0 +1,16 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
</head>
<body>
<div id='content'>
Lorem ipsum dolor sit amet
</div>
<div id='yieldy'>
Lorem ipsum dolor sit amet
</div>
<div id='nosym'>
Lorem ipsum dolor sit amet
</div>
</body>
</html>

@ -0,0 +1,8 @@
<p></p>
<h1>Me!</h1>
<div id='foo'>
<p id='bar'>All</p>
<br />
<p class='baz'>This</p>
Should render
</div>

@ -0,0 +1,57 @@
<style>
p {
border-style: dotted;
border-width: 10px;
border-color: #ff00ff; }
h1 {
font-weight: normal; }
</style>
TESTING HAHAHAHA!
<h1>Foo</h1>
<pre><code>This is preformatted!
Look at that!
Wowie-zowie!</code></pre>
<p><strong>boldilicious!</strong></p>
<h1>Yeah</h1>
<p><em>pretty much the same as above</em></p>
This
Is
Plain
Text
%strong right?
a
b
c
d
e
f
g
h
i
j
<ul>
<li>Foo</li>
<li>Bar</li>
<li>BAZ!</li>
</ul>
Text!
Hello, World!
How are you doing today?

@ -0,0 +1,84 @@
&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>
<p>foo</p>
<p>
reeeeeeeeeeeeeeeeeeeeeeeeeeeeeeally loooooooooooooooooong
</p>
<div class='woah'>
<div id='funky'>
<div>
<h1>Big!</h1>
<p>Small</p>
<!-- Invisible -->
</div>
<div class='dilly'>
<p>foo</p>
<h1>bar</h1>
</div>
</div>
(<strong>parentheses!</strong>)
</div>
*<span class='small'>Not really</span>
click
<a href='thing'>here</a>.
<p>baz</p>
<p>boom</p>
foo
<p>
<form action="hello/world" method="post">
</p>
<form action="heeheeaform" method="post">
<div><input name="commit" type="submit" value="save" /></div>
</form>
<form action="article_url" method="post">
Title:
<input id="article_title" name="article[title]" size="30" type="text" value="Hello" />
Body:
<input id="article_body" name="article[body]" size="30" type="text" value="World" />
</form>
<li><a href='http://www.google.com'>google</a></li>
<p>
foo
<div>
bar
</div>
boom
baz
boom, again
</p>
<table>
<tr>
<td class='cell'>
<strong>
strong!
</strong>
data
</td>
<td>
more_data
</td>
</tr>
</table>

@ -0,0 +1,10 @@
<div class='article' id='article_1'>
<h1>Hello</h1>
<div>World</div>
</div>
<div class='article' id='id_article_1'>id</div>
<div class='article class' id='article_1'>class</div>
<div class='article class' id='id_article_1'>id class</div>
<div class='article full' id='article_1'>boo</div>
<div class='article full' id='article_1'>moo</div>
<div class='article articleFull' id='article_1'>foo</div>

@ -0,0 +1,59 @@
<?xml version='1.0' encoding='utf-8' ?>
<?xml version='1.0' encoding='iso-8859-1' ?>
<?xml version='1.0' encoding='utf-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">
<strong apos="Foo's bar!">Boo!</strong>
Embedded? false!
Embedded? true!
Embedded? true!
<div class='render'><em>wow!</em></div>
stuff followed by whitespace
<strong>block with whitespace</strong>
<p>
Escape
- character
%p foo
yee\ha
</p>
<!-- Short comment -->
<!--
This is a really long comment look how long it is it should be on a line of its own don't you think?
-->
<!--
This is a block comment
cool, huh?
<strong>there can even be sub-tags!</strong>
Or script!
-->
<p>class attribute shouldn't appear!</p>
<!--[if lte IE6]> conditional comment! <![endif]-->
<!--[if gte IE7]>
<p>Block conditional comment</p>
<div>
<h1>Cool, eh?</h1>
</div>
<![endif]-->
<!--[if gte IE5.2]>
Woah a period.
<![endif]-->
testtest
<br />
<meta foo='bar' />
<img />
<hr />
<link />
<script>Inline content</script>
<br>
Nested content
</br>
<p class='article bar foo' id='article_1'>Blah</p>
<p class='article foo' id='article_1'>Blah</p>
<p class='article quux qux' id='article_1'>Blump</p>
Woah inner quotes
<p class='dynamic_quote' dyn='3' quotes="single '">
</p>
<p class='dynamic_atomic' dyn='3' />

@ -0,0 +1,12 @@
! Not a Doctype !
<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>
</ul>

@ -0,0 +1,24 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Stop. haml time</title>
<div id='content'>
<h1>This is a title!</h1>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit
</p>
<p class='foo'>Cigarettes!</p>
<h2>Man alive!</h2>
<ul class='things'>
<li>Slippers</li>
<li>Shoes</li>
<li>Bathrobe</li>
<li>Coffee</li>
</ul>
<pre>
This is some text that's in a pre block!
Let's see what happens when it's rendered! What about now, since we're on a new line?
</pre>
</div>
</head>
</html>

@ -0,0 +1,20 @@
<p>
@foo =
value one
</p>
<p>
@foo =
value two
</p>
<p>
@foo =
value two
</p>
<p>
@foo =
value three
</p>
<p>
@foo =
value three
</p>

@ -0,0 +1,74 @@
<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! "undefined method `silly' for String:Class" happened!
<p>
"false" is:
false
</p>
Even!
Odd!
Even!
Odd!
Even!
</div>
<div class='foo'>
<strong>foobar</strong>
</div>
<strong>0</strong>
<strong>1</strong>
<strong>2</strong>
<strong>3</strong>
<strong>4</strong>
<div class='test'>
<p>boom</p>
</div>

@ -0,0 +1,43 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html lang='en-US' xml:lang='en-US' xmlns='http://www.w3.org/1999/xhtml'>
<head>
<title>Hampton Catlin Is Totally Awesome</title>
<meta content='text/html; charset=utf-8' http-equiv='Content-Type' />
</head>
<body>
<!-- You're In my house now! -->
foo
<div class='header'>
Yes, ladies and gentileman. He is just that egotistical.
Fantastic! This should be multi-line output
The question is if this would translate! Ahah!
20
</div>
<div id='body'> Quotes should be loved! Just like people!</div>
Wow.|
<p>
Holy cow multiline tags! A pipe (|) even!
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>
hello
<div class='footer'>
<strong class='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.
So, I'm just making it *really* long. God, I hope this works
</strong>
</div>
</body>
</html>

@ -0,0 +1,28 @@
<div class='tags'>
<foo>1</foo>
<FOO>2</FOO>
<fooBAR>3</fooBAR>
<fooBar>4</fooBar>
<foo_bar>5</foo_bar>
<foo-bar>6</foo-bar>
<foo:bar>7</foo:bar>
<foo class='bar'>8</foo>
<fooBAr_baz:boom_bar>9</fooBAr_baz:boom_bar>
<foo13>10</foo13>
<foo2u>11</foo2u>
</div>
<div class='classes'>
<p class='foo bar' id='boom'>
</p>
<div class='fooBar'>a</div>
<div class='foo-bar'>b</div>
<div class='foo_bar'>c</div>
<div class='FOOBAR'>d</div>
<div class='foo16'>e</div>
<div class='123'>f</div>
<div class='foo2u'>g</div>
</div>
<div class='broken'>
<foo><{ :a => :b }</foo>
<div class='foo'>>{ :c => :d }</div>
</div>

@ -0,0 +1,7 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
</head>
<body>
</body>
</html>

@ -0,0 +1,94 @@
<div id='whitespace_test'>
<div class='text_area_test_area'>
<textarea>Oneline</textarea>
</div>
<textarea>BLAH
</textarea>
<div class='text_area_test_area'>
<textarea>Two&#x000A;lines</textarea>
</div>
<textarea>BLAH
</textarea>
<div class='text_area_test_area'>
<textarea>Oneline</textarea>
</div>
<textarea>BLAH&#x000A;</textarea>
<div class='text_area_test_area'>
<textarea>Two&#x000A;lines</textarea>
</div>
<textarea>BLAH&#x000A;</textarea>
<div id='flattened'>
<div class='text_area_test_area'>
<textarea>Two&#x000A;lines</textarea>
</div>
<textarea>BLAH&#x000A;</textarea>
</div>
</div>
<div class='hithere'>
Foo bar
<pre>foo bar</pre>
<pre>foo&#x000A;bar</pre>
<p><pre>foo&#x000A;bar</pre></p>
<p>
foo
bar
</p>
</div>
<div class='foo'>
13
<textarea>&#x000A;a&#x000A;</textarea><textarea>&#x000A;b&#x000A;</textarea><textarea>&#x000A;c&#x000A;</textarea>
</div>
<div id='whitespace_test'>
<div class='text_area_test_area'>
<textarea>Oneline</textarea>
</div>
<textarea>BLAH
</textarea>
<div class='text_area_test_area'>
<textarea>Two&#x000A;lines</textarea>
</div>
<textarea>BLAH
</textarea>
<div class='text_area_test_area'>
<textarea>Oneline</textarea>
</div>
<textarea>BLAH&#x000A;</textarea>
<div class='text_area_test_area'>
<textarea>Two&#x000A;lines</textarea>
</div>
<textarea>BLAH&#x000A;</textarea>
<div id='flattened'>
<div class='text_area_test_area'>
<textarea>Two&#x000A;lines</textarea>
</div>
<textarea>BLAH&#x000A;</textarea>
</div>
</div>
<div class='hithere'>
Foo bar
<pre>foo bar</pre>
<pre>foo&#x000A;bar</pre>
<p><pre>foo&#x000A;bar</pre></p>
<p>
foo
bar
</p>
<pre>
___&#x000A; ,o88888&#x000A; ,o8888888'&#x000A; ,:o:o:oooo. ,8O88Pd8888"&#x000A; ,.::.::o:ooooOoOoO. ,oO8O8Pd888'"&#x000A; ,.:.::o:ooOoOoOO8O8OOo.8OOPd8O8O"&#x000A; , ..:.::o:ooOoOOOO8OOOOo.FdO8O8"&#x000A; , ..:.::o:ooOoOO8O888O8O,COCOO"&#x000A; , . ..:.::o:ooOoOOOO8OOOOCOCO"&#x000A; . ..:.::o:ooOoOoOO8O8OCCCC"o&#x000A; . ..:.::o:ooooOoCoCCC"o:o&#x000A; . ..:.::o:o:,cooooCo"oo:o:&#x000A; ` . . ..:.:cocoooo"'o:o:::'&#x000A; .` . ..::ccccoc"'o:o:o:::'&#x000A; :.:. ,c:cccc"':.:.:.:.:.'&#x000A; ..:.:"'`::::c:"'..:.:.:.:.:.' http://www.chris.com/ASCII/&#x000A; ...:.'.:.::::"' . . . . .'&#x000A; .. . ....:."' ` . . . ''&#x000A; . . . ...."'&#x000A; .. . ."' -hrr-&#x000A; .&#x000A;&#x000A;&#x000A; It's a planet!&#x000A;%strong This shouldn't be bold!&#x000A;
</pre>
<strong>This should!</strong>
<textarea>
___ ___ ___ ___ &#x000A; /\__\ /\ \ /\__\ /\__\&#x000A; /:/ / /::\ \ /::| | /:/ /&#x000A; /:/__/ /:/\:\ \ /:|:| | /:/ / &#x000A; /::\ \ ___ /::\~\:\ \ /:/|:|__|__ /:/ / &#x000A; /:/\:\ /\__\ /:/\:\ \:\__\ /:/ |::::\__\ /:/__/ &#x000A; \/__\:\/:/ / \/__\:\/:/ / \/__/~~/:/ / \:\ \ &#x000A; \::/ / \::/ / /:/ / \:\ \ &#x000A; /:/ / /:/ / /:/ / \:\ \ &#x000A; /:/ / /:/ / /:/ / \:\__\&#x000A; \/__/ \/__/ \/__/ \/__/&#x000A;&#x000A; Many&#x000A; thanks&#x000A; to&#x000A; http://www.network-science.de/ascii/&#x000A;
<strong>indeed!</strong>
</textarea>
</div>
<div class='foo'>
13
</div>
<pre>
__ ______ __ ______&#x000A;.----.| |--.|__ |.----.| |--..--------.| __ |&#x000A;| __|| ||__ || __|| < | || __ |&#x000A;|____||__|__||______||____||__|__||__|__|__||______|&#x000A;
</pre>
<pre>
foo&#x000A;
bar
</pre>

@ -0,0 +1,55 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns='http://www.w3.org/1999/xhtml' xml:lang='en-US'>
<head>
<title>Hampton Catlin Is Totally Awesome</title>
<meta content='text/html; charset=utf-8' http-equiv='Content-Type' />
</head>
<body>
<!-- You're In my house now! -->
<% concat("Foo", Proc.new {}) %>
<div class='header'>
Yes, ladies and gentileman. He is just that egotistical.
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>
<% 120.times do |number| -%>
<%= number %>
<% end -%>
<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>
<%= [ 104, 101, 108, 108, 111 ].map do |byte|
byte.chr
end %>
<div class='footer'>
<strong class='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.\n" +
" So, I'm just making it *really* long. God, I hope this works" %>
</strong>
</div>
</body>
</html>

@ -0,0 +1,16 @@
require 'rubygems'
require 'active_support'
require 'action_controller'
require 'action_view'
require '../../lib/haml/template'
require 'fileutils'
haml_template_engine = Haml::Template.new(ActionView::Base.new)
haml_template_engine.render(File.dirname(__FILE__) + '/templates/standard.haml')
begin
eval(File.read("template_test.rb"))
rescue StandardError => e
puts e.backtrace
puts e.inspect
end

@ -0,0 +1,155 @@
#!/usr/bin/env ruby
require 'test/unit'
require 'rubygems'
require 'active_support'
require 'action_controller'
require 'action_view'
require File.dirname(__FILE__) + '/../../lib/haml'
require 'haml/template'
require File.dirname(__FILE__) + '/mocks/article'
class TestFilter
def initialize(text)
@text = text
end
def render
"TESTING HAHAHAHA!"
end
end
class TemplateTest < Test::Unit::TestCase
@@templates = %w{ very_basic standard helpers
whitespace_handling original_engine list helpful
silent_script tag_parsing just_stuff partials
filters }
def setup
ActionView::Base.register_template_handler("haml", Haml::Template)
Haml::Template.options = { :filters => { 'test'=>TestFilter } }
@base = ActionView::Base.new(File.dirname(__FILE__) + "/templates/", {'article' => Article.new, 'foo' => 'value one'})
end
def render(text)
Haml::Engine.new(text).to_html(@base)
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)
test = Proc.new do |rendered|
load_result(name).split("\n").zip(rendered.split("\n")).each_with_index do |pair, line|
message = "template: #{name}\nline: #{line}"
assert_equal(pair.first, pair.last, message)
end
end
test.call(@base.render(name))
# If eval's suppressed, the partial won't render anyway :p.
unless Haml::Template.options[:suppress_eval]
test.call(@base.render(:file => "partialize", :locals => { :name => name }))
end
end
def test_empty_render_should_remain_empty
assert_equal('', render(''))
end
def test_templates_should_render_correctly
@@templates.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("<p>something</p>", render("%p= @content_for_layout").chomp)
@base.instance_eval("@author = 'Hampton Catlin'")
assert_equal("<div class='author'>Hampton Catlin</div>", 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("<p class='hcatlin'>foo</p>", render("%p{:class => @author} foo").chomp)
end
def test_template_renders_should_eval
assert_equal("2\n", render("= 1+1"))
end
def test_rhtml_still_renders
# Make sure it renders normally
res = @base.render("../rhtml/standard")
assert !(res.nil? || res.empty?)
# Register Haml stuff in @base...
@base.render("standard")
# Does it still render?
res = @base.render("../rhtml/standard")
assert !(res.nil? || res.empty?)
end
def test_haml_options
Haml::Template.options = { :suppress_eval => true }
assert_equal({ :suppress_eval => true }, Haml::Template.options)
assert_renders_correctly("eval_suppressed")
Haml::Template.options = {}
end
def test_exceptions_should_work_correctly
begin
Haml::Template.new(@base).render(File.dirname(__FILE__) + '/templates/breakage.haml')
rescue Exception => e
assert_equal("./test/haml/templates/breakage.haml:4", e.backtrace[0])
else
assert false
end
begin
render("- raise 'oops!'")
rescue Exception => e
assert_equal("(haml):1", e.backtrace[0])
else
assert false
end
template = <<END
%p
%h1 Hello!
= "lots of lines"
= "even more!"
- raise 'oh no!'
%p
this is after the exception
%strong yes it is!
ho ho ho.
END
begin
render(template.chomp)
rescue Exception => e
assert_equal("(haml):5", e.backtrace[0])
else
assert false
end
end
end

@ -0,0 +1,7 @@
%p
@foo =
= @foo
- @foo = 'value three'
%p
@foo =
= @foo

@ -0,0 +1,3 @@
.text_area_test_area
~ "<textarea>" + value + "</textarea>"
= "<textarea>BLAH\n</textarea>"

@ -0,0 +1,8 @@
%p
%h1 Hello!
= "lots of lines"
- raise "Oh no!"
%p
this is after the exception
%strong yes it is!
ho ho ho.

@ -0,0 +1,10 @@
!!!
%html
%head
%body
#content
= @content_for_layout
#yieldy
= yield :layout
#nosym
= yield

@ -0,0 +1,10 @@
= "not me!"
= "nor me!"
- puts "not even me!"
%p= "NO!"
%h1 Me!
#foo
%p#bar All
%br/
%p.baz This
Should render

@ -0,0 +1,53 @@
%style
:sass
p
:border
:style dotted
:width 10px
:color #ff00ff
h1
:font-weight normal
:test
This
Should
Not
Print
:redcloth
Foo
===
This is preformatted!
Look at that!
Wowie-zowie!
*boldilicious!*
:textile
h1. Yeah
_pretty much the same as above_
:plain
This
Is
Plain
Text
%strong right?
:erb
<% 10.times do |c| %>
<%= (c+97).chr %>
<% end %>
:markdown
* Foo
* Bar
* BAZ!
= "Text!"
:ruby
puts "Hello, World!"
puts "How are you doing today?"

@ -0,0 +1,63 @@
= 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
%p foo
- tab_up
%p reeeeeeeeeeeeeeeeeeeeeeeeeeeeeeally loooooooooooooooooong
- tab_down
.woah
#funky
= capture_haml do
%div
%h1 Big!
%p Small
/ Invisible
= capture do
.dilly
%p foo
%h1 bar
= surround '(', ')' do
%strong parentheses!
= precede '*' do
%span.small Not really
click
= succeed '.' do
%a{:href=>"thing"} here
%p baz
- buffer.tabulation = 10
%p boom
- concat "foo\n"
- buffer.tabulation = 0
- def url_for(*stuff); stuff.join(' '); end
%p
= form_tag 'hello/world'
- form_tag 'heeheeaform' do
%div= submit_tag 'save'
- form_for :article, @article, :url => 'article_url' do |f|
Title:
= f.text_field :title
Body:
= f.text_field :body
= list_of({:google => 'http://www.google.com'}) do |name, link|
%a{ :href => link }= name
%p
- puts "foo"
%div
- puts "bar"
- puts "boom"
baz
- puts "boom, again"
- open :table do
- open :tr do
- open :td, {:class => 'cell'} do
- open :strong, "strong!"
- puts "data"
- open :td do
- puts "more_data"

@ -0,0 +1,11 @@
%div[@article]
%h1= @article.title
%div= @article.body
#id[@article] id
.class[@article] class
#id.class[@article] id class
%div{:class => "article full"}[@article]= "boo"
%div{'class' => "article full"}[@article]= "moo"
%div.articleFull[@article]= "foo"
%span[@not_a_real_variable_and_will_be_nil]
Boo

@ -0,0 +1,69 @@
!!! XML
!!! XML ISO-8859-1
!!! XML UtF-8 Foo bar
!!!
!!! 1.1
!!! 1.1 Strict
!!! Strict foo bar
!!! FRAMESET
%strong{:apos => "Foo's bar!"} Boo!
== Embedded? false!
== Embedded? #{true}!
- embedded = true
== Embedded? #{embedded}!
.render= render :inline => "%em= 'wow!'", :type => :haml
= "stuff followed by whitespace"
- if true
%strong block with whitespace
%p
\Escape
\- character
\%p foo
\yee\ha
/ Short comment
/ This is a really long comment look how long it is it should be on a line of its own don't you think?
/
This is a block comment
cool, huh?
%strong there can even be sub-tags!
= "Or script!"
-# Haml comment
-#
Nested Haml comment
- raise 'dead'
%p{ :class => "" } class attribute shouldn't appear!
/[if lte IE6] conditional comment!
/[if gte IE7]
%p Block conditional comment
%div
%h1 Cool, eh?
/[if gte IE5.2]
Woah a period.
= "test" |
"test" |
-# Hard tabs shouldn't throw errors.
- case :foo
- when :bar
%br Blah
- when :foo
%br
- case :foo
- when :bar
%meta{ :foo => 'blah'}
- when :foo
%meta{ :foo => 'bar'}
%img
%hr
%link
%script Inline content
%br
Nested content
%p.foo{:class => true ? 'bar' : 'baz'}[@article] Blah
%p.foo{:class => false ? 'bar' : ''}[@article] Blah
%p.qux{:class => 'quux'}[@article] Blump
== #{"Woah inner quotes"}
%p.dynamic_quote{:quotes => "single '", :dyn => 1 + 2}
%p.dynamic_atomic{:dyn => 1 + 2}/

@ -0,0 +1,12 @@
! Not a Doctype !
%ul
%li a
%li b
%li c
%li d
%li e
%li f
%li g
%li h
%li i

@ -0,0 +1,17 @@
!!!
%html
%head
%title Stop. haml time
#content
%h1 This is a title!
%p Lorem ipsum dolor sit amet, consectetur adipisicing elit
%p{:class => 'foo'} Cigarettes!
%h2 Man alive!
%ul.things
%li Slippers
%li Shoes
%li Bathrobe
%li Coffee
%pre
This is some text that's in a pre block!
Let's see what happens when it's rendered! What about now, since we're on a new line?

@ -0,0 +1 @@
= render :file => "#{name}.haml"

@ -0,0 +1,12 @@
%p
@foo =
= @foo
- @foo = 'value two'
%p
@foo =
= @foo
= render :file => "_partial.haml"
%p
@foo =
= @foo
- @foo = 'value one'

@ -0,0 +1,40 @@
%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
- String.silly
- rescue NameError => e
= "Oh no! \"#{e}\" happened!"
%p
"false" is:
- if false
= "true"
- else
= "false"
- if true
- 5.times do |i|
- if i % 2 == 1
Odd!
- else
Even!
- else
= "This can't happen!"
- 13 |
.foo
%strong foobar
- 5.times |
do |
|a| |
%strong= a
.test
- "foo |
bar |
baz" |
%p boom

@ -0,0 +1,43 @@
!!!
%html{html_attrs}
%head
%title Hampton Catlin Is Totally Awesome
%meta{"http-equiv" => "Content-Type", :content => "text/html; charset=utf-8"}
%body
/ You're In my house now!
- concat("foo\n")
.header
Yes, ladies and gentileman. He is just that egotistical.
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
#body= " Quotes should be loved! Just like people!"
- 120.times do |number|
- number
Wow.|
%p
= "Holy cow " + |
"multiline " + |
"tags! " + |
"A pipe (|) even!" |
= [1, 2, 3].collect { |n| "PipesIgnored|" }
= [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!
-# That was a line that shouldn't close everything.
%ul.really.cool
- ('a'..'f').each do |a|
%li= a
#combo.of_divs_with_underscore= @should_eval = "with this text"
= [ 104, 101, 108, 108, 111 ].map do |byte|
- byte.chr
.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"

@ -0,0 +1,24 @@
%div.tags
%foo 1
%FOO 2
%fooBAR 3
%fooBar 4
%foo_bar 5
%foo-bar 6
%foo:bar 7
%foo.bar 8
%fooBAr_baz:boom_bar 9
%foo13 10
%foo2u 11
%div.classes
%p.foo.bar#baz#boom
.fooBar a
.foo-bar b
.foo_bar c
.FOOBAR d
.foo16 e
.123 f
.foo2u g
%div.broken
%foo<{ :a => :b }
.foo>{ :c => :d }

@ -0,0 +1,87 @@
#whitespace_test
= render :file => "_text_area.haml", :locals => { :value => "Oneline" }
= render :file => "_text_area.haml", :locals => { :value => "Two\nlines" }
~ render :file => "_text_area.haml", :locals => { :value => "Oneline" }
~ render :file => "_text_area.haml", :locals => { :value => "Two\nlines" }
#flattened~ render :file => "_text_area.haml", :locals => { :value => "Two\nlines" }
.hithere
~ "Foo bar"
~ "<pre>foo bar</pre>"
~ "<pre>foo\nbar</pre>"
%p~ "<pre>foo\nbar</pre>"
%p~ "foo\nbar"
.foo
~ 13
~ ['a', 'b', 'c'].map do |a|
- "<textarea>\n#{a}\n</textarea>"
#whitespace_test
= render :file => "_text_area.haml", :locals => { :value => "Oneline" }
= render :file => "_text_area.haml", :locals => { :value => "Two\nlines" }
= find_and_preserve render(:file => "_text_area.haml", :locals => { :value => "Oneline" })
= find_and_preserve render(:file => "_text_area.haml", :locals => { :value => "Two\nlines" })
#flattened= find_and_preserve render(:file => "_text_area.haml", :locals => { :value => "Two\nlines" })
.hithere
= find_and_preserve("Foo bar")
= find_and_preserve("<pre>foo bar</pre>")
= find_and_preserve("<pre>foo\nbar</pre>")
%p= find_and_preserve("<pre>foo\nbar</pre>")
%p= find_and_preserve("foo\nbar")
%pre
:preserve
___
,o88888
,o8888888'
,:o:o:oooo. ,8O88Pd8888"
,.::.::o:ooooOoOoO. ,oO8O8Pd888'"
,.:.::o:ooOoOoOO8O8OOo.8OOPd8O8O"
, ..:.::o:ooOoOOOO8OOOOo.FdO8O8"
, ..:.::o:ooOoOO8O888O8O,COCOO"
, . ..:.::o:ooOoOOOO8OOOOCOCO"
. ..:.::o:ooOoOoOO8O8OCCCC"o
. ..:.::o:ooooOoCoCCC"o:o
. ..:.::o:o:,cooooCo"oo:o:
` . . ..:.:cocoooo"'o:o:::'
.` . ..::ccccoc"'o:o:o:::'
:.:. ,c:cccc"':.:.:.:.:.'
..:.:"'`::::c:"'..:.:.:.:.:.' http://www.chris.com/ASCII/
...:.'.:.::::"' . . . . .'
.. . ....:."' ` . . . ''
. . . ...."'
.. . ."' -hrr-
.
It's a planet!
%strong This shouldn't be bold!
%strong This should!
%textarea
:preserve
___ ___ ___ ___
/\__\ /\ \ /\__\ /\__\
/:/ / /::\ \ /::| | /:/ /
/:/__/ /:/\:\ \ /:|:| | /:/ /
/::\ \ ___ /::\~\:\ \ /:/|:|__|__ /:/ /
/:/\:\ /\__\ /:/\:\ \:\__\ /:/ |::::\__\ /:/__/
\/__\:\/:/ / \/__\:\/:/ / \/__/~~/:/ / \:\ \
\::/ / \::/ / /:/ / \:\ \
/:/ / /:/ / /:/ / \:\ \
/:/ / /:/ / /:/ / \:\__\
\/__/ \/__/ \/__/ \/__/
Many
thanks
to
http://www.network-science.de/ascii/
%strong indeed!
.foo
= find_and_preserve(13)
%pre
:preserve
__ ______ __ ______
.----.| |--.|__ |.----.| |--..--------.| __ |
| __|| ||__ || __|| < | || __ |
|____||__|__||______||____||__|__||__|__|__||______|
%pre
:preserve
foo
bar

@ -0,0 +1,65 @@
require 'rubygems'
require 'active_support'
require 'action_controller'
require 'action_view'
require File.dirname(__FILE__) + '/../lib/haml'
require 'haml/template'
require 'profiler'
require 'stringio'
module Haml
# Used by both Haml::Profiler and Sass::Profiler.
# Encapsulates profiling behavior.
module AbstractProfiler
def self.profile(times, &block)
# Runs the profiler, collects information
Profiler__::start_profile
times.times &block
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
result
end
end
# 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__), 'haml', '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')
AbstractProfiler.profile(runs) { @base.render template_name }
end
end
end
module Sass
class Profiler
def profile(runs = 100, template_name = 'complex')
Haml::AbstractProfiler.profile(runs) do
Sass::Engine.new("#{File.dirname(__FILE__)}/sass/templates/#{template_name}.sass").render
end
end
end
end

@ -0,0 +1,116 @@
#!/usr/bin/env ruby
require 'test/unit'
require File.dirname(__FILE__) + '/../../lib/sass'
require 'sass/engine'
class SassEngineTest < Test::Unit::TestCase
EXCEPTION_MAP = {
"!a = 1 + " => 'Constant arithmetic error: "1 +"',
"!a = 1 + 2 +" => 'Constant arithmetic error: "1 + 2 +"',
"!a = \"b" => 'Unterminated string: "\\"b"',
"!a = #aaa - a" => 'Undefined operation: "#aaaaaa minus a"',
"!a = #aaa / a" => 'Undefined operation: "#aaaaaa div a"',
"!a = #aaa * a" => 'Undefined operation: "#aaaaaa times a"',
"!a = #aaa % a" => 'Undefined operation: "#aaaaaa mod a"',
"!a = 1 - a" => 'Undefined operation: "1 minus a"',
"!a = 1 * a" => 'Undefined operation: "1 times a"',
"!a = 1 / a" => 'Undefined operation: "1 div a"',
"!a = 1 % a" => 'Undefined operation: "1 mod a"',
":" => 'Invalid attribute: ":"',
": a" => 'Invalid attribute: ": a"',
":= a" => 'Invalid attribute: ":= a"',
"a\n :b" => 'Invalid attribute: ":b "',
"a\n :b: c" => 'Invalid attribute: ":b: c"',
"a\n :b:c d" => 'Invalid attribute: ":b:c d"',
"a\n :b=c d" => 'Invalid attribute: ":b=c d"',
"a\n :b c;" => 'Invalid attribute: ":b c;" (This isn\'t CSS!)',
"a\n b : c" => 'Invalid attribute: "b : c"',
"a\n b=c: d" => 'Invalid attribute: "b=c: d"',
":a" => 'Attributes aren\'t allowed at the root of a document.',
"!" => 'Invalid constant: "!"',
"!a" => 'Invalid constant: "!a"',
"! a" => 'Invalid constant: "! a"',
"!a b" => 'Invalid constant: "!a b"',
"a\n\t:b c" => "Illegal Indentation: Only two space characters are allowed as tabulation.",
"a\n :b c" => "Illegal Indentation: Only two space characters are allowed as tabulation.",
"a\n :b c" => "Illegal Indentation: Only two space characters are allowed as tabulation.",
"a\n :b c\n !d = 3" => "Constants may only be declared at the root of a document.",
"!a = 1b + 2c" => "Incompatible units: b and c",
"& a\n :b c" => "Base-level rules cannot contain the parent-selector-referencing character '&'",
"a\n :b\n c" => "Illegal nesting: Only attributes may be nested beneath attributes.",
"!a = b\n :c d\n" => "Illegal nesting: Nothing may be nested beneath constants.",
"@import foo.sass" => "File to import not found or unreadable: foo.sass",
"@import templates/basic\n foo" => "Illegal nesting: Nothing may be nested beneath import directives.",
"foo\n @import templates/basic" => "Import directives may only be used at the root of a document.",
"@foo bar boom" => "Unknown compiler directive: \"@foo bar boom\"",
}
def test_basic_render
renders_correctly "basic", { :style => :compact }
end
def test_alternate_styles
renders_correctly "expanded", { :style => :expanded }
renders_correctly "compact", { :style => :compact }
renders_correctly "nested", { :style => :nested }
end
def test_exceptions
EXCEPTION_MAP.each do |key, value|
begin
Sass::Engine.new(key).render
rescue Sass::SyntaxError => err
assert_equal(value, err.message)
assert(err.sass_line, "Line: #{key}")
assert_match(/\(sass\):[0-9]+/, err.backtrace[0], "Line: #{key}")
else
assert(false, "Exception not raised for\n#{key}")
end
end
end
def test_exception_line
to_render = "rule\n :attr val\n// comment!\n\n :broken\n"
begin
Sass::Engine.new(to_render).render
rescue Sass::SyntaxError => err
assert_equal(5, err.sass_line)
else
assert(false, "Exception not raised for '#{to_render}'!")
end
end
def test_imported_exception
[1, 2].each do |i|
i = nil if i == 1
begin
Sass::Engine.new("@import bork#{i}", :load_paths => [File.dirname(__FILE__) + '/templates/']).render
rescue Sass::SyntaxError => err
assert_equal(2, err.sass_line)
assert_match(/bork#{i}\.sass$/, err.sass_filename)
else
assert(false, "Exception not raised for imported template: bork#{i}")
end
end
end
def test_empty_first_line
assert_equal("#a {\n b: c; }\n", Sass::Engine.new("#a\n\n b: c").render)
end
private
def renders_correctly(name, options={})
sass_file = load_file(name, "sass")
css_file = load_file(name, "css")
css_result = Sass::Engine.new(sass_file, options).render
assert_equal css_file, css_result
end
def load_file(name, type = "sass")
@result = ''
File.new(File.dirname(__FILE__) + "/#{type == 'sass' ? 'templates' : 'results'}/#{name}.#{type}").each_line { |l| @result += l }
@result
end
end

@ -0,0 +1,136 @@
#!/usr/bin/env ruby
MERB_ENV = RAILS_ENV = 'testing'
RAILS_ROOT = '.'
require 'test/unit'
require 'fileutils'
require File.dirname(__FILE__) + '/../../lib/sass'
require 'rubygems'
require 'action_controller'
require 'sass/plugin'
class SassPluginTest < Test::Unit::TestCase
@@templates = %w{
complex constants parent_ref import alt
subdir/subdir subdir/nested_subdir/nested_subdir
}
def setup
FileUtils.mkdir File.dirname(__FILE__) + '/tmp'
set_plugin_opts
Sass::Plugin.update_stylesheets
end
def teardown
FileUtils.rm_r File.dirname(__FILE__) + '/tmp'
end
def test_templates_should_render_correctly
@@templates.each { |name| assert_renders_correctly(name) }
end
def test_no_update
File.delete(tempfile_loc('basic'))
assert Sass::Plugin.stylesheet_needs_update?('basic')
Sass::Plugin.update_stylesheets
assert !Sass::Plugin.stylesheet_needs_update?('basic')
end
def test_full_exception_handling
File.delete(tempfile_loc('bork'))
Sass::Plugin.update_stylesheets
File.open(tempfile_loc('bork')) do |file|
assert_equal("/*\nSass::SyntaxError: Undefined constant: \"!bork\"\non line 2 of #{File.dirname(__FILE__) + '/templates/bork.sass'}\n\n1: bork\n2: :bork= !bork", file.read.split("\n")[0...6].join("\n"))
end
File.delete(tempfile_loc('bork'))
end
def test_nonfull_exception_handling
Sass::Plugin.options[:full_exception] = false
File.delete(tempfile_loc('bork'))
Sass::Plugin.update_stylesheets
assert_equal("/* Internal stylesheet error */", File.read(tempfile_loc('bork')))
File.delete(tempfile_loc('bork'))
Sass::Plugin.options[:full_exception] = true
end
def test_rails_update
File.delete(tempfile_loc('basic'))
assert Sass::Plugin.stylesheet_needs_update?('basic')
ActionController::Base.new.process
assert !Sass::Plugin.stylesheet_needs_update?('basic')
end
def test_merb_update
begin
require 'merb'
rescue LoadError
puts "\nmerb couldn't be loaded, skipping a test"
return
end
require 'sass/plugin/merb'
MerbHandler.send(:define_method, :process_without_sass) { |*args| }
set_plugin_opts
File.delete(tempfile_loc('basic'))
assert Sass::Plugin.stylesheet_needs_update?('basic')
MerbHandler.new('.').process nil, nil
assert !Sass::Plugin.stylesheet_needs_update?('basic')
end
private
def assert_renders_correctly(name)
File.read(result_loc(name)).split("\n").zip(File.read(tempfile_loc(name)).split("\n")).each_with_index do |pair, line|
message = "template: #{name}\nline: #{line + 1}"
assert_equal(pair.first, pair.last, message)
end
end
def tempfile_loc(name)
File.dirname(__FILE__) + "/tmp/#{name}.css"
end
def result_loc(name)
File.dirname(__FILE__) + "/results/#{name}.css"
end
def set_plugin_opts
Sass::Plugin.options = {
:template_location => File.dirname(__FILE__) + '/templates',
:css_location => File.dirname(__FILE__) + '/tmp',
:style => :compact,
:load_paths => [File.dirname(__FILE__) + '/results'],
:always_update => true,
}
end
end
module Sass::Plugin
class << self
public :stylesheet_needs_update?
end
end
class Sass::Engine
alias_method :old_render, :render
def render
raise "bork bork bork!" if @template[0] == "{bork now!}"
old_render
end
end
class ActionController::Base
def sass_old_process(*args); end
end

@ -0,0 +1,4 @@
h1 { float: left; width: 274px; height: 75px; margin: 0; background-repeat: no-repeat; background-image: none; }
h1 a:hover, h1 a:visited { color: green; }
h1 b:hover { color: red; background-color: green; }
h1 const { nosp: 3; sp: 3; }

@ -0,0 +1,9 @@
body { font: Arial; background: blue; }
#page { width: 700px; height: 100; }
#page #header { height: 300px; }
#page #header h1 { font-size: 50px; color: blue; }
#content.user.show #container.top #column.left { width: 100px; }
#content.user.show #container.top #column.right { width: 600px; }
#content.user.show #container.bottom { background: brown; }

@ -0,0 +1,5 @@
#main { width: 15em; color: #0000ff; }
#main p { border-style: dotted; border-width: 2px; }
#main .cool { width: 100px; }
#left { font-size: 2em; font-weight: bold; float: left; }

@ -0,0 +1,87 @@
body { margin: 0; font: 0.85em "Lucida Grande", "Trebuchet MS", Verdana, sans-serif; color: #fff; background: url(/images/global_bg.gif); }
#page { width: 900px; margin: 0 auto; background: #440008; border-top-width: 5px; border-top-style: solid; border-top-color: #ff8500; }
#header { height: 75px; padding: 0; }
#header h1 { float: left; width: 274px; height: 75px; margin: 0; background-image: url(/images/global_logo.gif); background-repeat: no-repeat; text-indent: -9999px; }
#header .status { float: right; padding-top: .5em; padding-left: .5em; padding-right: .5em; padding-bottom: 0; }
#header .status p { float: left; margin-top: 0; margin-right: 0.5em; margin-bottom: 0; margin-left: 0; }
#header .status ul { float: left; margin: 0; padding: 0; }
#header .status li { list-style-type: none; display: inline; margin: 0 5px; }
#header .status a:link, #header .status a:visited { color: #ff8500; text-decoration: none; }
#header .status a:hover { text-decoration: underline; }
#header .search { float: right; clear: right; margin: 12px 0 0 0; }
#header .search form { margin: 0; }
#header .search input { margin: 0 3px 0 0; padding: 2px; border: none; }
#menu { clear: both; text-align: right; height: 20px; border-bottom: 5px solid #006b95; background: #00a4e4; }
#menu .contests ul { margin: 0 5px 0 0; padding: 0; }
#menu .contests ul li { list-style-type: none; margin: 0 5px; padding: 5px 5px 0 5px; display: inline; font-size: 1.1em; color: #fff; background: #00a4e4; }
#menu .contests ul li / This rule isn't a comment! { red: green; }
#menu .contests a:link, #menu .contests a:visited { color: #fff; text-decoration: none; font-weight: bold; }
#menu .contests a:hover { text-decoration: underline; }
#content { clear: both; }
#content .container { clear: both; }
#content .container .column { float: left; }
#content .container .column .right { float: right; }
#content a:link, #content a:visited { color: #93d700; text-decoration: none; }
#content a:hover { text-decoration: underline; }
#content p, #content div { width: 40em; }
#content p li, #content p dt, #content p dd, #content div li, #content div dt, #content div dd { color: #ddffdd; background-color: #4792bb; }
#content .container.video .column.left { width: 200px; }
#content .container.video .column.left .box { margin-top: 10px; }
#content .container.video .column.left .box p { margin: 0 1em auto 1em; }
#content .container.video .column.left .box.participants img { float: left; margin: 0 1em auto 1em; border: 1px solid #6e000d; border-style: solid; }
#content .container.video .column.left .box.participants h2 { margin: 0 0 10px 0; padding: 0.5em; /* The background image is a gif! */ background: #6e000d url(/images/hdr_participant.gif) 2px 2px no-repeat; /* Okay check this out Multiline comments Wow dude I mean seriously, WOW */ text-indent: -9999px; border-top-width: 5px; border-top-style: solid; border-top-color: #a20013; border-right-width: 1px; border-right-style: dotted; }
#content .container.video .column.middle { width: 500px; }
#content .container.video .column.right { width: 200px; }
#content .container.video .column.right .box { margin-top: 0; }
#content .container.video .column.right .box p { margin: 0 1em auto 1em; }
#content .container.video .column p { margin-top: 0; }
#content.contests .container.information .column.right .box { margin: 1em 0; }
#content.contests .container.information .column.right .box.videos .thumbnail img { width: 200px; height: 150px; margin-bottom: 5px; }
#content.contests .container.information .column.right .box.videos a:link, #content.contests .container.information .column.right .box.videos a:visited { color: #93d700; text-decoration: none; }
#content.contests .container.information .column.right .box.videos a:hover { text-decoration: underline; }
#content.contests .container.information .column.right .box.votes a { display: block; width: 200px; height: 60px; margin: 15px 0; background: url(/images/btn_votenow.gif) no-repeat; text-indent: -9999px; outline: none; border: none; }
#content.contests .container.information .column.right .box.votes h2 { margin: 52px 0 10px 0; padding: 0.5em; background: #6e000d url(/images/hdr_videostats.gif) 2px 2px no-repeat; text-indent: -9999px; border-top: 5px solid #a20013; }
#content.contests .container.video .box.videos h2 { margin: 0; padding: 0.5em; background: #6e000d url(/images/hdr_newestclips.gif) 2px 2px no-repeat; text-indent: -9999px; border-top: 5px solid #a20013; }
#content.contests .container.video .box.videos table { width: 100; }
#content.contests .container.video .box.videos table td { padding: 1em; width: 25; vertical-align: top; }
#content.contests .container.video .box.videos table td p { margin: 0 0 5px 0; }
#content.contests .container.video .box.videos table td a:link, #content.contests .container.video .box.videos table td a:visited { color: #93d700; text-decoration: none; }
#content.contests .container.video .box.videos table td a:hover { text-decoration: underline; }
#content.contests .container.video .box.videos .thumbnail { float: left; }
#content.contests .container.video .box.videos .thumbnail img { width: 80px; height: 60px; margin: 0 10px 0 0; border: 1px solid #6e000d; }
#content .container.comments .column { margin-top: 15px; }
#content .container.comments .column.left { width: 600px; }
#content .container.comments .column.left .box ol { margin: 0; padding: 0; }
#content .container.comments .column.left .box li { list-style-type: none; padding: 10px; margin: 0 0 1em 0; background: #6e000d; border-top: 5px solid #a20013; }
#content .container.comments .column.left .box li div { margin-bottom: 1em; }
#content .container.comments .column.left .box li ul { text-align: right; }
#content .container.comments .column.left .box li ul li { display: inline; border: none; padding: 0; }
#content .container.comments .column.right { width: 290px; padding-left: 10px; }
#content .container.comments .column.right h2 { margin: 0; padding: 0.5em; background: #6e000d url(/images/hdr_addcomment.gif) 2px 2px no-repeat; text-indent: -9999px; border-top: 5px solid #a20013; }
#content .container.comments .column.right .box textarea { width: 290px; height: 100px; border: none; }
#footer { margin-top: 10px; padding: 1.2em 1.5em; background: #ff8500; }
#footer ul { margin: 0; padding: 0; list-style-type: none; }
#footer ul li { display: inline; margin: 0 0.5em; color: #440008; }
#footer ul.links { float: left; }
#footer ul.links a:link, #footer ul.links a:visited { color: #440008; text-decoration: none; }
#footer ul.links a:hover { text-decoration: underline; }
#footer ul.copyright { float: right; }
.clear { clear: both; }
.centered { text-align: center; }
img { border: none; }
button.short { width: 60px; height: 22px; padding: 0 0 2px 0; color: #fff; border: none; background: url(/images/btn_short.gif) no-repeat; }
table { border-collapse: collapse; }

@ -0,0 +1,12 @@
#main { content: Hello!; qstr: Quo"ted"!; hstr: Hyph-en!; width: 30em; background-color: #000; color: #ffffaa; short-color: #112233; named-color: #808000; con: foo bar 9 hi there boom; con2: noquo quo; }
#main #sidebar { background-color: #00ff98; num-normal: 10; num-dec: 10.2; num-dec0: 99; num-neg: -10; esc: 10+12; many: 6; order: 7; complex: #4c9db1hi16; }
#plus { num-num: 7; num-num-un: 25em; num-num-un2: 23em; num-num-neg: 9.87; num-str: 100px; num-col: #b7b7b7; num-perc: 31%; str-str: hi there; str-str2: hi there; str-col: 14em solid #112233; str-num: times: 13; col-num: #ff7b9d; col-col: #5173ff; }
#minus { num-num: 900; col-num: #f9f9f4; col-col: #000035; unary-num: -1; unary-const: 10; unary-paren: -11; }
#times { num-num: 7; num-col: #7496b8; col-num: #092345; col-col: #243648; }
#div { num-num: 3.33333333333333; num-num2: 3; col-num: #092345; col-col: #0b0d0f; }
#mod { num-num: 2; col-col: #0f0e05; col-num: #020001; }

@ -0,0 +1,18 @@
#main {
width: 15em;
color: #0000ff;
}
#main p {
border-style: dotted;
border-width: 2px;
}
#main .cool {
width: 100px;
}
#left {
font-size: 2em;
font-weight: bold;
float: left;
}

@ -0,0 +1,27 @@
imported { otherconst: hello; myconst: goodbye; }
body { font: Arial; background: blue; }
#page { width: 700px; height: 100; }
#page #header { height: 300px; }
#page #header h1 { font-size: 50px; color: blue; }
#content.user.show #container.top #column.left { width: 100px; }
#content.user.show #container.top #column.right { width: 600px; }
#content.user.show #container.bottom { background: brown; }
midrule { inthe: middle; }
body { font: Arial; background: blue; }
#page { width: 700px; height: 100; }
#page #header { height: 300px; }
#page #header h1 { font-size: 50px; color: blue; }
#content.user.show #container.top #column.left { width: 100px; }
#content.user.show #container.top #column.right { width: 600px; }
#content.user.show #container.bottom { background: brown; }
@import url(basic.css);
@import url(../results/complex.css);
nonimported { myconst: hello; otherconst: goodbye; }

@ -0,0 +1,21 @@
#main {
width: 15em;
color: #0000ff; }
#main p {
border-style: dotted;
border-width: 2px; }
#main .cool {
width: 100px; }
#left {
font-size: 2em;
font-weight: bold;
float: left; }
#right .header {
border-style: solid; }
#right .body {
border-style: dotted; }
#right .footer {
border-style: dashed; }

@ -0,0 +1,13 @@
a { color: #000; }
a:hover { color: #f00; }
p, div { width: 100em; }
p foo, div foo { width: 10em; }
p:hover, p bar, div:hover, div bar { height: 20em; }
#cool { border-style: solid; border-width: 2em; }
.ie7 #cool, .ie6 #cool { content: string(Totally not cool.); }
.firefox #cool { content: string(Quite cool.); }
.wow, .snazzy { font-family: fantasy; }
.wow:hover, .wow:visited, .snazzy:hover, .snazzy:visited { font-weight: bold; }

@ -0,0 +1 @@
#subdir { font-size: 20px; font-weight: bold; }

@ -0,0 +1,16 @@
h1
:float left
:width 274px
height: 75px
margin: 0
background:
repeat: no-repeat
:image none
a:hover, a:visited
color: green
b:hover
color: red
:background-color green
const
nosp= 1 + 2
sp = 1 + 2

@ -0,0 +1,23 @@
body
:font Arial
:background blue
#page
:width 700px
:height 100
#header
:height 300px
h1
:font-size 50px
:color blue
#content.user.show
#container.top
#column.left
:width 100px
#column.right
:width 600px
#container.bottom
:background brown

@ -0,0 +1,15 @@
#main
:width 15em
:color #0000ff
p
:border
:style dotted
:width 2px
.cool
:width 100px
#left
:font
:size 2em
:weight bold
:float left

@ -0,0 +1,309 @@
body
:margin 0
:font 0.85em "Lucida Grande", "Trebuchet MS", Verdana, sans-serif
:color #fff
:background url(/images/global_bg.gif)
#page
:width 900px
:margin 0 auto
:background #440008
:border-top
:width 5px
:style solid
:color #ff8500
#header
:height 75px
:padding 0
h1
:float left
:width 274px
:height 75px
:margin 0
:background
:image url(/images/global_logo.gif)
:repeat no-repeat
:text-indent -9999px
.status
:float right
:padding
:top .5em
:left .5em
:right .5em
:bottom 0
p
:float left
:margin
:top 0
:right 0.5em
:bottom 0
:left 0
ul
:float left
:margin 0
:padding 0
li
:list-style-type none
:display inline
:margin 0 5px
a:link, a:visited
:color #ff8500
:text-decoration none
a:hover
:text-decoration underline
.search
:float right
:clear right
:margin 12px 0 0 0
form
:margin 0
input
:margin 0 3px 0 0
:padding 2px
:border none
#menu
:clear both
:text-align right
:height 20px
:border-bottom 5px solid #006b95
:background #00a4e4
.contests
ul
:margin 0 5px 0 0
:padding 0
li
:list-style-type none
:margin 0 5px
:padding 5px 5px 0 5px
:display inline
// This comment is in the middle of this rule
:font-size 1.1em
// This comment is properly indented
:color #fff
:background #00a4e4
/ This rule isn't a comment!
:red green
a:link, a:visited
:color #fff
:text-decoration none
:font-weight bold
a:hover
:text-decoration underline
//General content information
#content
:clear both
.container
:clear both
.column
:float left
.left
.middle
.right
:float right
a:link, a:visited
:color #93d700
:text-decoration none
a:hover
:text-decoration underline
// A hard tab:
#content
p, div
:width 40em
li, dt, dd
:color #ddffdd
:background-color #4792bb
.container.video
.column.left
:width 200px
.box
:margin-top 10px
p
:margin 0 1em auto 1em
.box.participants
img
:float left
:margin 0 1em auto 1em
:border 1px solid #6e000d
:style solid
h2
:margin 0 0 10px 0
:padding 0.5em
/* The background image is a gif!
:background #6e000d url(/images/hdr_participant.gif) 2px 2px no-repeat
/* Okay check this out
Multiline comments
Wow dude
I mean seriously, WOW
:text-indent -9999px
// And also...
Multiline comments that don't output!
Snazzy, no?
:border
:top
:width 5px
:style solid
:color #a20013
:right
:width 1px
:style dotted
.column.middle
:width 500px
.column.right
:width 200px
.box
:margin-top 0
p
:margin 0 1em auto 1em
.column
p
:margin-top 0
#content.contests
.container.information
.column.right
.box
:margin 1em 0
.box.videos
.thumbnail img
:width 200px
:height 150px
:margin-bottom 5px
a:link, a:visited
:color #93d700
:text-decoration none
a:hover
:text-decoration underline
.box.votes
a
:display block
:width 200px
:height 60px
:margin 15px 0
:background url(/images/btn_votenow.gif) no-repeat
:text-indent -9999px
:outline none
:border none
h2
:margin 52px 0 10px 0
:padding 0.5em
:background #6e000d url(/images/hdr_videostats.gif) 2px 2px no-repeat
:text-indent -9999px
:border-top 5px solid #a20013
#content.contests
.container.video
.box.videos
h2
:margin 0
:padding 0.5em
:background #6e000d url(/images/hdr_newestclips.gif) 2px 2px no-repeat
:text-indent -9999px
:border-top 5px solid #a20013
table
:width 100
td
:padding 1em
:width 25
:vertical-align top
p
:margin 0 0 5px 0
a:link, a:visited
:color #93d700
:text-decoration none
a:hover
:text-decoration underline
.thumbnail
:float left
img
:width 80px
:height 60px
:margin 0 10px 0 0
:border 1px solid #6e000d
#content
.container.comments
.column
:margin-top 15px
.column.left
:width 600px
.box
ol
:margin 0
:padding 0
li
:list-style-type none
:padding 10px
:margin 0 0 1em 0
:background #6e000d
:border-top 5px solid #a20013
div
:margin-bottom 1em
ul
:text-align right
li
:display inline
:border none
:padding 0
.column.right
:width 290px
:padding-left 10px
h2
:margin 0
:padding 0.5em
:background #6e000d url(/images/hdr_addcomment.gif) 2px 2px no-repeat
:text-indent -9999px
:border-top 5px solid #a20013
.box
textarea
:width 290px
:height 100px
:border none
#footer
:margin-top 10px
:padding 1.2em 1.5em
:background #ff8500
ul
:margin 0
:padding 0
:list-style-type none
li
:display inline
:margin 0 0.5em
:color #440008
ul.links
:float left
a:link, a:visited
:color #440008
:text-decoration none
a:hover
:text-decoration underline
ul.copyright
:float right
.clear
:clear both
.centered
:text-align center
img
:border none
button.short
:width 60px
:height 22px
:padding 0 0 2px 0
:color #fff
:border none
:background url(/images/btn_short.gif) no-repeat
table
:border-collapse collapse

@ -0,0 +1,88 @@
!width = 10em + 20
!color = #00ff98
!main_text = #ffa
!num = 10
!dec = 10.2
!dec_0 = 99.0
!neg = -10
!esc= 10\+12
!str= "Hello!"
!qstr= "Quo\"ted\"!"
!hstr= "Hyph-en!"
!concat = (5 + 4) hi there
!percent= 11%
#main
:content = !str
:qstr = !qstr
:hstr = !hstr
:width = !width
:background-color #000
:color= !main_text
:short-color= #123
:named-color= olive
:con= foo bar (!concat boom)
:con2= noquo "quo"
#sidebar
:background-color= !color
:num
:normal= !num
:dec= !dec
:dec0= !dec_0
:neg= !neg
:esc= !esc
:many= 1 + 2 + 3
:order= 1 + 2 * 3
:complex= ((1 + 2) + 15)+#3a8b9f + (hi+(1 +1+ 2)* 4)
#plus
:num
:num= 5+2
:num-un= 10em + 15em
:num-un2= 10 + 13em
:num-neg= 10 + -.13
:str= 100 + px
:col= 13 + #aaa
:perc = !percent + 20%
:str
:str= hi + \ there
:str2= hi + " there"
:col= "14em solid " + #123
:num= times:\ + 13
:col
:num= #f02 + 123.5
:col= #12A + #405162
#minus
:num
:num= 912 - 12
:col
:num= #fffffa - 5.2
:col= #abcdef - #fedcba
:unary
:num= -1
:const= -!neg
:paren= -(5 + 6)
#times
:num
:num= 2 * 3.5
:col= 2 * #3a4b5c
:col
:num= #12468a * 0.5
:col= #121212 * #020304
#div
:num
:num= 10 / 3.0
:num2= 10 / 3
:col
:num= #12468a / 2
:col= #abcdef / #0f0f0f
#mod
:num
:num= 17 % 3
:col
:col= #5f6e7d % #10200a
:num= #aaabac % 3

Some files were not shown because too many files have changed in this diff Show More