Skip to content

@vangberg /README
Created

Embed URL

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Deploying a Sinatra app to Heroku
# Deploying a Sinatra app to Heroku
## Database
The location of the database Heroku provides can be found in the environment
variable DATABASE_URL. Check the configure-block of toodeloo.rb for an example
on how to use this.
## Rackup file
Heroku can serve all Rack applications. It looks for a rackup file named
'config.ru' in the root directory. Thus serving a Sinatra app is simple:
require 'toodeloo'
run Sinatra::Application
## Create app and deploy
The whole process of deploying this small Sinatra app was as follows:
$ git clone git://gist.github.com/68277.git toodeloo
$ cd toodeloo
$ heroku create toodeloo
$ git remote add heroku git@heroku.com:toodeloo.git
$ git push heroku master
That's it. You can see it in action at http://toodeloo.heroku.com

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 plugin 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 haml --rails path/to/rails/app and both Haml and Sass will be installed. Views with the .haml (or .html.haml for edge) extension will automatically use Haml. Sass is a little more complicated; .sass 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, .html.haml views will work without any further modification. To enable Sass, you also need to add a dependency. To do so, just add

dependency "merb-haml"

to config/dependencies.rb (or config/init.rb in a flat/very flat Merb application). 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 class and id 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 <div> tag, it becomes even easier. Because <div> 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, =, will output the result of the code. A hyphen, -, will run the code but not output the result. You can even use control statements like if and while:

%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; }

Taking the idea of constants a bit further are mixins. These let you group whole swathes of CSS attributes into a single directive and then include those anywhere you want:

=blue-border
  :border
    :color blue
    :width 2px
    :style dotted

.comment
  +blue-border
  :padding 2px
  :margin 10px 0

.reply
  +blue-border

becomes:

.comment {
  border-color: blue;
  border-width: 2px;
  border-style: dotted;
  padding: 2px;
  margin: 10px 0;
}

.reply {
  border-color: blue;
  border-width: 2px;
  border-style: dotted;
}

A comprehensive list of features is in the documentation for the Sass module.

Executables

The Haml gem includes several executables that are useful for dealing with Haml and Sass from the command line.

haml

The haml executable transforms a source Haml file into HTML. See haml --help for further information and options.

sass

The sass executable transforms a source Sass file into CSS. See sass --help for further information and options.

html2haml

The html2haml executable attempts to transform HTML, optionally with ERB markup, into Haml code. Since HTML is so variable, this transformation is not always perfect; it's a good idea to have a human check the output of this tool. See html2haml --help for further information and options.

css2sass

The css2sass executable attempts to transform CSS into Sass code. This transformation attempts to use Sass nesting where possible. See css2sass --help for further information and options.

Authors

Haml and Sass are designed by Hampton Catlin (hcatlin) and he is the author of the original implementation. However, Hampton doesn't even know his way around the code anymore and mostly just concentrates on the language issues. Hampton lives in Toronto, Ontario (though he's an American by birth) and is a partner at Unspace Interactive.

Nathan Weizenbaum is the primary maintainer and architect of the “modern” Ruby implementation of Haml. His hard work has kept the project alive by endlessly answering forum posts, fixing bugs, refactoring, finding speed improvements, writing documentation, implementing new features, and getting Hampton coffee (a fitting task for a boy-genius). Nathan lives in Seattle, Washington and while not being a student at University of Washington he consults for Unspace Interactive and Microsoft.

If you use this software, you must pay Hampton a compliment. And buy Nathan some jelly beans. Maybe pet a kitten. Yeah. Pet that kitty.

Some of the work on Haml was supported by Unspace Interactive.

Beyond that, the implementation is licensed under the MIT License. Ok, fine, I guess that means compliments aren't required.

%h2 This is a pretty complicated partial
.partial
%p It has several nested partials,
%ul
- 5.times do
%li
%strong Partial:
- @nesting = 5
= render :partial => 'haml/templates/av_partial_2'
<h2>This is a pretty complicated partial</h2>
<div class="partial">
<p>It has several nested partials,</p>
<ul>
<% 5.times do %>
<li>
<strong>Partial:</strong>
<% @nesting = 5 %>
<%= render :partial => 'haml/rhtml/av_partial_2' %>
<% end %>
</ul>
</div>
%h2 This is a pretty complicated partial
.partial
%p It has several nested partials,
%ul
- 5.times do
%li
%strong Partial:
- @nesting = 5
= render :partial => 'haml/templates/av_partial_2_ugly'
- @nesting -= 1
.partial{:level => @nesting}
%h3 This is a crazy deep-nested partial.
%p== Nesting level #{@nesting}
= render :partial => 'haml/templates/av_partial_2' if @nesting > 0
<% @nesting -= 1 %>
<div class="partial" level="<%= @nesting %>">
<h3>This is a crazy deep-nested partial.</h3>
<p>Nesting level <%= @nesting %></p>
<% if @nesting > 0 %>
<%= render :partial => 'haml/rhtml/av_partial_2' %>
<% end %>
</div>
- @nesting -= 1
.partial{:level => @nesting}
%h3 This is a crazy deep-nested partial.
%p== Nesting level #{@nesting}
= render :partial => 'haml/templates/av_partial_2_ugly' if @nesting > 0
Before
<%= yield -%>
After
.partial-layout
%h2 This is inside a partial layout
= yield
%p
@foo =
= @foo
- @foo = 'value three'
== Toplevel? #{haml_buffer.toplevel?}
%p
@foo =
= @foo
#foo
:background-color #baf
.text_area_test_area
~ "<textarea>" + value + "</textarea>"
= "<textarea>BLAH\n</textarea>"
!!!
%html{html_attrs}
%head
%title Hampton Catlin Is Totally Awesome
%meta{"http-equiv" => "Content-Type", :content => "text/html; charset=utf-8"}
%body
%h1
This is very much like the standard template,
except that it has some ActionView-specific stuff.
It's only used for benchmarking.
.crazy_partials= render :partial => 'haml/templates/av_partial_1'
/ You're In my house now!
.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"
<!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>
<h1>
This is very much like the standard template,
except that it has some ActionView-specific stuff.
It's only used for benchmarking.
</h1>
<div class="crazy_partials">
<%= render :partial => 'haml/rhtml/av_partial_1' %>
</div>
<!-- You're In my house now! -->
<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>
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
if defined?(ActionView) and not defined?(Merb::Plugins)
module ActionView
class Base # :nodoc:
def render_with_haml(*args, &block)
options = args.first
# If render :layout is used with a block,
# it concats rather than returning a string
# so we need it to keep thinking it's Haml
# until it hits the sub-render
if is_haml? && !(options.is_a?(Hash) && options[:layout] && block_given?)
return non_haml { render_without_haml(*args, &block) }
end
render_without_haml(*args, &block)
end
alias_method :render_without_haml, :render
alias_method :render, :render_with_haml
# Rails >2.1
if Haml::Util.has?(:instance_method, self, :output_buffer)
def output_buffer_with_haml
return haml_buffer.buffer if is_haml?
output_buffer_without_haml
end
alias_method :output_buffer_without_haml, :output_buffer
alias_method :output_buffer, :output_buffer_with_haml
def set_output_buffer_with_haml(new)
if is_haml?
haml_buffer.buffer = new
else
set_output_buffer_without_haml new
end
end
alias_method :set_output_buffer_without_haml, :output_buffer=
alias_method :output_buffer=, :set_output_buffer_with_haml
end
end
# This overrides various helpers in ActionView
# to make them work more effectively with Haml.
module Helpers
# :stopdoc:
# In Rails <=2.1, we've got to override considerable capturing infrastructure.
# In Rails >2.1, we can make do with only overriding #capture
# (which no longer behaves differently in helper contexts).
unless Haml::Util.has?(:instance_method, ActionView::Base, :output_buffer)
module CaptureHelper
def capture_with_haml(*args, &block)
# Rails' #capture helper will just return the value of the block
# if it's not actually in the template context,
# as detected by the existance of an _erbout variable.
# We've got to do the same thing for compatibility.
if is_haml? && block_is_haml?(block)
capture_haml(*args, &block)
else
capture_without_haml(*args, &block)
end
end
alias_method :capture_without_haml, :capture
alias_method :capture, :capture_with_haml
def capture_erb_with_buffer_with_haml(buffer, *args, &block)
if is_haml?
capture_haml(*args, &block)
else
capture_erb_with_buffer_without_haml(buffer, *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?
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
else
module CaptureHelper
def capture_with_haml(*args, &block)
if Haml::Helpers.block_is_haml?(block)
capture_haml(*args, &block)
else
capture_without_haml(*args, &block)
end
end
alias_method :capture_without_haml, :capture
alias_method :capture, :capture_with_haml
end
end
module TagHelper
def content_tag_with_haml(name, *args, &block)
return content_tag_without_haml(name, *args, &block) unless is_haml?
preserve = haml_buffer.options[:preserve].include?(name.to_s)
if block_given? && block_is_haml?(block) && preserve
return content_tag_without_haml(name, *args) {preserve(&block)}
end
returning content_tag_without_haml(name, *args, &block) do |content|
return Haml::Helpers.preserve(content) if preserve && content
end
end
alias_method :content_tag_without_haml, :content_tag
alias_method :content_tag, :content_tag_with_haml
end
class InstanceTag
# Includes TagHelper
def haml_buffer
@template_object.send :haml_buffer
end
def is_haml?
@template_object.send :is_haml?
end
alias_method :content_tag_without_haml, :content_tag
alias_method :content_tag, :content_tag_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 = haml_bind_proc do |*args|
concat "\n"
tab_up
oldproc.call(*args)
tab_down
concat haml_indent
end
concat haml_indent
end
res = form_tag_without_haml(url_for_options, options, *parameters_for_url, &proc) + "\n"
concat "\n" if block_given?
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 = haml_bind_proc do |*args|
tab_up
oldproc.call(*args)
tab_down
concat haml_indent
end
concat haml_indent
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
!!!
%html{html_attrs}
%head
%title Hampton Catlin Is Totally Awesome
%meta{"http-equiv" => "Content-Type", :content => "text/html; charset=utf-8"}
%body
%h1
This is very much like the standard template,
except that it has some ActionView-specific stuff.
It's only used for benchmarking.
.crazy_partials= render :partial => 'haml/templates/av_partial_1_ugly'
/ You're In my house now!
.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"
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; }
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
class Article
attr_accessor :id, :title, :body
def initialize
@id, @title, @body = 1, 'Hello', 'World'
end
end
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 ==(other)
self.class == other.class && name == other.name && super
end
def to_s(tabs, 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 = case @style
when :compact; ' '
when :compressed; ''
else "\n"
end
spaces = ' ' * (tabs - 1)
to_return = ''
if !value.empty?
to_return << "#{spaces}#{real_name}:#{@style == :compressed ? '' : ' '}#{value};#{join_string}"
end
children.each do |kid|
to_return << "#{kid.to_s(tabs, real_name)}" << join_string
end
(@style == :compressed && parent_name) ? to_return : to_return[0...-1]
end
private
def declaration
":#{name} #{value}"
end
def invalid_child?(child)
if !child.is_a?(AttrNode) && !child.is_a?(CommentNode)
"Illegal nesting: Only attributes may be nested beneath attributes."
end
end
end
end
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; }
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
#!/usr/bin/env ruby
times = (ARGV.first || 1000).to_i
if times == 0 # Invalid parameter
puts <<END
ruby #$0 [times=1000]
Benchmark Haml against various other templating languages and Sass
on its own.
END
exit 1
end
require File.dirname(__FILE__) + '/../lib/haml'
require File.dirname(__FILE__) + '/linked_rails'
%w[sass rubygems erb erubis markaby active_support action_controller
action_view action_pack haml/template rbench].each {|dep| require(dep)}
def view
unless Haml::Util.has?(:instance_method, ActionView::Base, :finder)
return ActionView::Base.new(File.dirname(__FILE__), {})
end
# Rails >=2.1.0
base = ActionView::Base.new
base.finder.append_view_path(File.dirname(__FILE__))
base
end
def render(view, file)
view.render :file => file
end
RBench.run(times) do
column :haml, :title => "Haml"
column :haml_ugly, :title => "Haml :ugly"
column :erb, :title => "ERB"
column :erubis, :title => "Erubis"
template_name = 'standard'
directory = File.dirname(__FILE__) + '/haml'
haml_template = File.read("#{directory}/templates/#{template_name}.haml")
erb_template = File.read("#{directory}/rhtml/#{template_name}.rhtml")
markaby_template = File.read("#{directory}/markaby/#{template_name}.mab")
report "Cached" do
obj = Object.new
Haml::Engine.new(haml_template).def_method(obj, :haml)
Haml::Engine.new(haml_template, :ugly => true).def_method(obj, :haml_ugly)
Erubis::Eruby.new(erb_template).def_method(obj, :erubis)
obj.instance_eval("def erb; #{ERB.new(erb_template, nil, '-').src}; end")
haml { obj.haml }
haml_ugly { obj.haml_ugly }
erb { obj.erb }
erubis { obj.erubis }
end
report "ActionView" do
@base = view
@base.unmemoize_all
Haml::Template.options[:ugly] = false
# To cache the template
render @base, 'haml/templates/standard'
render @base, 'haml/rhtml/standard'
haml { render @base, 'haml/templates/standard' }
erb { render @base, 'haml/rhtml/standard' }
Haml::Template.options[:ugly] = true
render @base, 'haml/templates/standard_ugly'
haml_ugly { render @base, 'haml/templates/standard_ugly' }
end
report "ActionView with deep partials" do
@base = view
@base.unmemoize_all
Haml::Template.options[:ugly] = false
# To cache the template
render @base, 'haml/templates/action_view'
render @base, 'haml/rhtml/action_view'
haml { render @base, 'haml/templates/action_view' }
erb { render @base, 'haml/rhtml/action_view' }
Haml::Template.options[:ugly] = true
render @base, 'haml/templates/action_view_ugly'
haml_ugly { render @base, 'haml/templates/action_view_ugly' }
end
end
RBench.run(times) do
sass_template = File.read("#{File.dirname(__FILE__)}/sass/templates/complex.sass")
report("Sass") { Sass::Engine.new(sass_template).render }
end
bork
:bork= !bork
bork
:bork: bork;
%p
%h1 Hello!
= "lots of lines"
- raise "Oh no!"
%p
this is after the exception
%strong yes it is!
ho ho ho.
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
# The string that holds the compiled XHTML. This is aliased as
# _erbout for compatibility with ERB-specific code.
attr_accessor :buffer
# The options hash passed in from Haml::Engine.
attr_accessor :options
# The Buffer for the enclosing Haml document.
# This is set for partials and similar sorts of nested templates.
# It's nil at the top level (see #toplevel?).
attr_accessor :upper
# See #active?
attr_writer :active
# True if the format is XHTML
def xhtml?
not html?
end
# True if the format is any flavor of HTML
def html?
html4? or html5?
end
# True if the format is HTML4
def html4?
@options[:format] == :html4
end
# True if the format is HTML5
def html5?
@options[:format] == :html5
end
# True if this buffer is a top-level template,
# as opposed to a nested partial.
def toplevel?
upper.nil?
end
# True if this buffer is currently being used to render a Haml template.
# However, this returns false if a subtemplate is being rendered,
# even if it's a subtemplate of this buffer's template.
def active?
@active
end
# 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(upper = nil, options = {})
@active = true
@upper = upper
@options = {
:attr_wrapper => "'",
:ugly => false,
:format => :xhtml
}.merge options
@buffer = ""
@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, dont_tab_up = false, tab_change = 0)
if @tabulation > 0 && !@options[:ugly]
# Have to push every line in by the extra user set tabulation.
# Don't push lines with just whitespace, though,
# because that screws up precompiled indentation.
text.gsub!(/^(?!\s+$)/m, tabs)
text.sub!(tabs, '') if dont_tab_up
end
@buffer << text
@real_tabs += tab_change
@dont_tab_up_next_line = false
end
# Properly formats the output of a script that was run in the
# instance_eval.
def push_script(result, preserve_script, in_tag = false, preserve_tag = false,
escape_html = false, nuke_inner_whitespace = false)
tabulation = @real_tabs
result = result.to_s.rstrip
result = result.lstrip if nuke_inner_whitespace
result = html_escape(result) if escape_html
if preserve_tag
result = Haml::Helpers.preserve(result)
elsif preserve_script
result = Haml::Helpers.find_and_preserve(result, options[:preserve])
end
has_newline = result.include?("\n")
if in_tag && !nuke_inner_whitespace && (@options[:ugly] || !has_newline || preserve_tag)
@buffer << result
@real_tabs -= 1
return
end
@buffer << "\n" if in_tag && !nuke_inner_whitespace
# Precompiled tabulation may be wrong
if @tabulation > 0 && !in_tag
result = tabs + result
end
if has_newline && !@options[:ugly]
result = result.gsub "\n", "\n" + tabs(tabulation)
# Add tabulation if it wasn't precompiled
result = tabs(tabulation) + result if in_tag && !nuke_inner_whitespace
end
@buffer << "#{result}"
@buffer << "\n" unless nuke_inner_whitespace
if in_tag && !nuke_inner_whitespace
# We never get here if @options[:ugly] is true
@buffer << tabs(tabulation-1)
@real_tabs -= 1
end
nil
end
# Takes the various information about the opening tag for an
# element, formats it, and adds it to the buffer.
def open_tag(name, self_closing, try_one_line, preserve_tag, escape_html, class_id,
nuke_outer_whitespace, nuke_inner_whitespace, obj_ref, content, *attributes_hashes)
tabulation = @real_tabs
attributes = class_id
attributes_hashes.each do |old|
self.class.merge_attrs(attributes, old.inject({}) {|h, (key, val)| h[key.to_s] = val; h})
end
self.class.merge_attrs(attributes, parse_object_ref(obj_ref)) if obj_ref
if self_closing && xhtml?
str = " />" + (nuke_outer_whitespace ? "" : "\n")
else
str = ">" + ((if self_closing && html?
nuke_outer_whitespace
else
try_one_line || preserve_tag || nuke_inner_whitespace
end) ? "" : "\n")
end
attributes = Precompiler.build_attributes(html?, @options[:attr_wrapper], attributes)
@buffer << "#{nuke_outer_whitespace || @options[:ugly] ? '' : tabs(tabulation)}<#{name}#{attributes}#{str}"
if content
@buffer << "#{content}</#{name}>" << (nuke_outer_whitespace ? "" : "\n")
return
end
@real_tabs += 1 unless self_closing || nuke_inner_whitespace
end
def self.merge_attrs(to, from)
if to['id'] && from['id']
to['id'] << '_' << from.delete('id')
elsif to['id'] || from['id']
from['id'] ||= to['id']
end
if to['class'] && from['class']
# Make sure we don't duplicate class names
from['class'] = (from['class'].split(' ') | to['class'].split(' ')).join(' ')
elsif to['class'] || from['class']
from['class'] ||= to['class']
end
to.merge!(from)
end
private
# Some of these methods are exposed as public class methods
# so they can be re-used in helpers.
@@tab_cache = {}
# Gets <tt>count</tt> tabs. Mostly for internal use.
def tabs(count = 0)
tabs = [count + @tabulation, 0].max
@@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.
# The second object, if present, is used as a prefix,
# just like you can do with dom_id() and dom_class() in Rails
def parse_object_ref(ref)
prefix = ref[1]
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'}"
if prefix
class_name = "#{ prefix }_#{ class_name}"
id = "#{ prefix }_#{ id }"
end
{'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
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
require 'sass/tree/node'
module Sass::Tree
class CommentNode < ValueNode
def initialize(value, style)
super(value[2..-1].strip, style)
end
def to_s(tabs = 0, parent_name = nil)
return if @style == :compressed
spaces = ' ' * (tabs - 1)
join_string = @style == :compact ? ' ' : "\n#{spaces} * "
str = "#{spaces}/* #{value}"
str << join_string unless children.empty?
str << "#{children.join join_string} */"
str
end
end
end
#main { width: 15em; color: #0000ff; }
#main p { border-style: dotted; /* Nested comment More nested stuff */ border-width: 2px; }
#main .cool { width: 100px; }
#left { font-size: 2em; font-weight: bold; float: left; }
#main
:width 15em
:color #0000ff
p
:border
:style dotted
/* Nested comment
More nested stuff
:width 2px
.cool
:width 100px
#left
:font
:size 2em
:weight bold
:float left
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); /* Crazy nested comment */ 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; }
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)
/* Crazy nested comment
: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
#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}
#main
:width 15em
:color #0000ff
p
:border
:style dotted
:width 2px
.cool
:width 100px
#left
:font
:size 2em
:weight bold
:float left
require 'toodeloo'
run Sinatra::Application
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,
CONSTANT_CHAR => :const,
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}") }.join}]+)\s*((?:\|\|)?=)\s*(.+)/
# First-order operations
FIRST_ORDER = [:times, :div, :mod]
# Second-order operations
SECOND_ORDER = [:plus, :minus]
class << self
include Haml::Util
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
beginning_of_token = true
str = ''
to_return = []
reset_str = Proc.new do
to_return << str unless str.empty?
''
end
each_char(value) 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 || symbol == :const) &&
last && (!last.is_a?(Symbol) || last == :close)
to_return << :concat
end
# String then open with no whitespace means funcall
if symbol == :open && !str.empty?
str = reset_str.call
to_return << :funcall
end
# Time for a unary minus!
if beginning_of_token && symbol == :minus
beginning_of_token = true
to_return << :neg
next
end
# Are we looking at an operator?
if symbol && (symbol != :mod || str.empty?)
str = reset_str.call
beginning_of_token = symbol != :close
to_return << symbol
next
end
end
end
escaped = false
beginning_of_token = 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, return_after_expr = false)
to_return = []
while (token = value.shift) && token != :close
case token
when :open
to_return << parenthesize(value)
when :neg
# This is never actually reached, but we'll leave it in just in case.
raise Sass::SyntaxError.new("Unterminated unary minus.") if value.first.nil?
to_return << [:neg, parenthesize(value, true)]
when :const
raise Sass::SyntaxError.new("Unterminated constant.") if value.first.nil?
raise Sass::SyntaxError.new("Invalid constant.") unless value.first.is_a?(::String)
to_return << [:const, value.first]
value.shift
else
to_return << token
end
return to_return.first if return_after_expr
end
return to_return
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)
case value.length
when 0
Sass::Constant::Nil.new
when 1
value = value[0]
if value.is_a? Array
operationalize(value, constants)
elsif value.is_a? Operation
value
else
Literal.parse(value)
end
when 2
if value[0] == :neg
Operation.new(Sass::Constant::Number.new('0'), operationalize(value[1], constants), :minus)
elsif value[0] == :const
Literal.parse(get_constant(value[1], constants))
else
raise SyntaxError.new("Constant arithmetic error")
end
when 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 get_constant(value, constants)
to_return = constants[value]
raise SyntaxError.new("Undefined constant: \"!#{value}\".") unless to_return
to_return
end
end
end
end
#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; unary-two: 12; unary-many: 12; unary-crazy: -15; }
#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; }
#const { escaped-quote: !foo; escaped-slash: !foo; default: Hello! !important; }
#regression { a: 4; }
!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)
:two= --12
:many= --------12
:crazy= -----(5 + ---!neg)
#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
#const
:escaped
:quote = "!foo"
:slash = \!foo
:default = !str !important
#regression
:a= (3 + 2) - 1
!!!
%html
%head
%body
#content
= @content_for_layout
#yieldy
= yield :layout
#nosym
= yield
<!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>
require File.dirname(__FILE__) + '/../sass'
require 'sass/tree/node'
require 'strscan'
module Sass
# :stopdoc:
module Tree
class Node
def to_sass(opts = {})
result = ''
children.each do |child|
result << "#{child.to_sass(0, opts)}\n"
end
result
end
end
class ValueNode
def to_sass(tabs, opts = {})
"#{value}\n"
end
end
class RuleNode
def to_sass(tabs, opts = {})
str = "\n#{' ' * tabs}#{rule}#{children.any? { |c| c.is_a? AttrNode } ? "\n" : ''}"
children.each do |child|
str << "#{child.to_sass(tabs + 1, opts)}"
end
str
end
end
class AttrNode
def to_sass(tabs, opts = {})
"#{' ' * tabs}#{opts[:alternate] ? '' : ':'}#{name}#{opts[:alternate] ? ':' : ''} #{value}\n"
end
end
class DirectiveNode
def to_sass(tabs, opts = {})
"#{' ' * tabs}#{value}#{children.map {|c| c.to_sass(tabs + 1, opts)}}\n"
end
end
end
# This class is based on the Ruby 1.9 ordered hashes.
# It keeps the semantics and most of the efficiency of normal hashes
# while also keeping track of the order in which elements were set.
class OrderedHash
Node = Struct.new(:key, :value, :next, :prev)
include Enumerable
def initialize
@hash = {}
end
def initialize_copy(other)
@hash = other.instance_variable_get('@hash').clone
end
def [](key)
@hash[key] && @hash[key].value
end
def []=(key, value)
node = Node.new(key, value)
if old = @hash[key]
if old.prev
old.prev.next = old.next
else # old is @first and @last
@first = @last = nil
end
end
if @first.nil?
@first = @last = node
else
node.prev = @last
@last.next = node
@last = node
end
@hash[key] = node
value
end
def each
return unless @first
yield [@first.key, @first.value]
node = @first
yield [node.key, node.value] while node = node.next
self
end
def values
self.map { |k, v| v }
end
end
# :startdoc:
# This class contains the functionality used in the +css2sass+ utility,
# namely converting CSS documents to Sass templates.
class CSS
# Creates a new instance of Sass::CSS that will compile the given document
# to a Sass string when +render+ is called.
def initialize(template, options = {})
if template.is_a? IO
template = template.read
end
@options = options
@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(@options).strip + "\n"
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
rules root
expand_commas root
parent_ref_rules root
remove_parent_refs root
flatten_rules root
fold_commas root
root
end
def rules(root)
while r = rule
root << r
whitespace
end
end
def rule
return unless rule = @template.scan(/[^\{\};]+/)
rule.strip!
directive = rule[0] == ?@
if directive
node = Tree::DirectiveNode.new(rule, nil)
return node if @template.scan(/;/)
assert_match /\{/
whitespace
rules(node)
return node
end
assert_match /\{/
node = Tree::RuleNode.new(rule, nil)
attributes(node)
return node
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)
line = @template.string[0..@template.pos].count "\n"
# Display basic regexps as plain old strings
expected = re.source == Regexp.escape(re.source) ? "\"#{re.source}\"" : re.inspect
raise Exception.new("Invalid CSS on line #{line}: expected #{expected}")
end
whitespace
end
# Transform
#
# foo, bar, baz
# color: blue
#
# into
#
# foo
# color: blue
# bar
# color: blue
# baz
# color: blue
#
# Yes, this expands the amount of code,
# but it's necessary to get nesting to work properly.
def expand_commas(root)
root.children.map! do |child|
next child unless Tree::RuleNode === child && child.rule.include?(',')
child.rule.split(',').map do |rule|
node = Tree::RuleNode.new(rule.strip, {})
node.children = child.children
node
end
end
root.children.flatten!
end
# Make rules use parent refs so that
#
# foo
# color: green
# foo.bar
# color: blue
#
# becomes
#
# foo
# color: green
# &.bar
# color: blue
#
# This has the side effect of nesting rules,
# so that
#
# foo
# color: green
# foo bar
# color: red
# foo baz
# color: blue
#
# becomes
#
# foo
# color: green
# & bar
# color: red
# & baz
# color: blue
#
def parent_ref_rules(root)
rules = OrderedHash.new
root.children.select { |c| Tree::RuleNode === c }.each do |child|
root.children.delete child
first, rest = child.rule.scan(/^(&?(?: .|[^ ])[^.#: \[]*)([.#: \[].*)?$/).first
rules[first] ||= Tree::RuleNode.new(first, nil)
if rest
child.rule = "&" + rest
rules[first] << child
else
rules[first].children += child.children
end
end
rules.values.each { |v| parent_ref_rules(v) }
root.children += rules.values
end
# Remove useless parent refs so that
#
# foo
# & bar
# color: blue
#
# becomes
#
# foo
# bar
# color: blue
#
def remove_parent_refs(root)
root.children.each do |child|
if child.is_a?(Tree::RuleNode)
child.rule.gsub! /^& +/, ''
remove_parent_refs child
end
end
end
# Flatten rules so that
#
# foo
# bar
# baz
# color: red
#
# becomes
#
# foo bar baz
# color: red
#
# and
#
# foo
# &.bar
# color: blue
#
# becomes
#
# foo.bar
# color: blue
#
def flatten_rules(root)
root.children.each { |child| flatten_rule(child) if child.is_a?(Tree::RuleNode) }
end
def flatten_rule(rule)
while rule.children.size == 1 && rule.children.first.is_a?(Tree::RuleNode)
child = rule.children.first
if child.rule[0] == ?&
rule.rule = child.rule.gsub /^&/, rule.rule
else
rule.rule = "#{rule.rule} #{child.rule}"
end
rule.children = child.children
end
flatten_rules(rule)
end
# Transform
#
# foo
# bar
# color: blue
# baz
# color: blue
#
# into
#
# foo
# bar, baz
# color: blue
#
def fold_commas(root)
prev_rule = nil
root.children.map! do |child|
next child unless child.is_a?(Tree::RuleNode)
if prev_rule && prev_rule.children == child.children
prev_rule.rule << ", #{child.rule}"
next nil
end
fold_commas(child)
prev_rule = child
child
end
root.children.compact!
end
end
end
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../lib/haml'
require 'haml/exec'
opts = Haml::Exec::CSS2Sass.new(ARGV)
opts.parse!
require 'test/unit'
require File.dirname(__FILE__) + '/../test_helper'
require 'sass/css'
class CSS2SassTest < Test::Unit::TestCase
def test_basic
css = <<CSS
h1 {
color: red;
}
CSS
assert_equal(<<SASS, css2sass(css))
h1
:color red
SASS
assert_equal(<<SASS, css2sass(css, :alternate => true))
h1
color: red
SASS
end
def test_nesting
assert_equal(<<SASS, css2sass(<<CSS))
li
:display none
a
:text-decoration none
span
:color yellow
&:hover
:text-decoration underline
SASS
li {
display: none;
}
li a {
text-decoration: none;
}
li a span {
color: yellow;
}
li a:hover {
text-decoration: underline;
}
CSS
end
def test_comments_multiline
css = <<CSS
/* comment */
elephant.rawr {
rampages: excessively;
}
/* actual multiline
comment */
span.turkey {
isdinner: true;
}
.turducken {
/* Sounds funny
doesn't it */
chimera: not_really;
}
#overhere {
bored: sorta; /* it's for a good
cause */
better_than: thread_pools;
}
#one_more {
finally: srsly;
} /* just a line here */
CSS
sass = <<SASS
elephant.rawr
:rampages excessively
span.turkey
:isdinner true
.turducken
:chimera not_really
#overhere
:bored sorta
:better_than thread_pools
#one_more
:finally srsly
SASS
assert_equal(css2sass(css), sass)
end
def test_fold_commas
assert_equal(<<SASS, css2sass(<<CSS))
li
.one, .two
:color red
SASS
li .one {
color: red;
}
li .two {
color: red;
}
CSS
assert_equal(<<SASS, css2sass(<<CSS))
.one
:color green
.two
:color green
:color red
.three
:color red
SASS
.one, .two {
color: green;
}
.two, .three {
color: red;
}
CSS
end
def test_bad_formatting
assert_equal(<<SASS, css2sass(<<CSS))
hello
:parent true
there
:parent false
who
:hoo false
why
:y true
when
:wen nao
down_here
:yeah true
SASS
hello {
parent: true;
}
hello there {
parent: false;
}
hello who {
hoo: false;
}
hello why {
y: true;
}
hello when {
wen: nao;
}
down_here { yeah: true; }
CSS
end
private
def css2sass(string, opts={})
Sass::CSS.new(string, opts).render
end
end
require 'sass/tree/node'
require 'sass/tree/value_node'
module Sass::Tree
class DirectiveNode < ValueNode
def to_s(tabs)
if children.empty?
value + ";"
else
result = if @style == :compressed
"#{value}{"
else
"#{' ' * (tabs - 1)}#{value} {" + (@style == :compact ? ' ' : "\n")
end
was_attr = false
first = true
children.each do |child|
if @style == :compact
if child.is_a?(AttrNode)
result << "#{child.to_s(first || was_attr ? 1 : tabs + 1)} "
else
if was_attr
result[-1] = "\n"
end
rendered = child.to_s(tabs + 1)
rendered.lstrip! if first
result << rendered
end
was_attr = child.is_a?(AttrNode)
first = false
elsif @style == :compressed
result << (was_attr ? ";#{child.to_s(1)}" : child.to_s(1))
was_attr = child.is_a?(AttrNode)
else
result << child.to_s(tabs + 1) + "\n"
end
end
result.rstrip + if @style == :compressed
"}"
else
(@style == :expanded ? "\n" : " ") + "}\n"
end
end
end
end
end
require 'haml/helpers'
require 'haml/buffer'
require 'haml/precompiler'
require 'haml/filters'
require 'haml/error'
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
include Precompiler
# Allow reading and writing of the options hash
attr :options, true
# This string contains the source code that is evaluated
# to produce the Haml document.
attr :precompiled, true
# True if the format is XHTML
def xhtml?
not html?
end
# True if the format is any flavor of HTML
def html?
html4? or html5?
end
# True if the format is HTML4
def html4?
@options[:format] == :html4
end
# True if the format is HTML5
def html5?
@options[:format] == :html5
end
# Creates a new instace of Haml::Engine that will compile the given
# template string when <tt>render</tt> is called.
# See the Haml module documentation for available options.
#
#--
# When adding options, remember to add information about them
# to lib/haml.rb!
#++
#
def initialize(template, options = {})
@options = {
:suppress_eval => false,
:attr_wrapper => "'",
# Don't forget to update the docs in lib/haml.rb if you update these
:autoclose => %w[meta img link br hr input area param col base],
:preserve => %w[textarea pre],
:filename => '(haml)',
:line => 1,
:ugly => false,
:format => :xhtml,
:escape_html => false
}
@options.merge! options
@index = 0
unless [:xhtml, :html4, :html5].include?(@options[:format])
raise Haml::Error, "Invalid format #{@options[:format].inspect}"
end
@template = template.rstrip + "\n-#\n-#"
@to_close_stack = []
@output_tabs = 0
@template_tabs = 0
@flat_spaces = -1
@flat = false
@newlines = 0
@precompiled = ''
@merged_text = ''
@tab_change = 0
if @template =~ /\A(\s*\n)*[ \t]+\S/
raise SyntaxError.new("Indenting at the beginning of the document is illegal.", ($1 || "").count("\n"))
end
if @options[:filters]
warn <<END
DEPRECATION WARNING:
The Haml :filters option is deprecated and will be removed in version 2.2.
Filters are now automatically registered.
END
end
precompile
rescue Haml::Error => e
e.backtrace.unshift "#{@options[:filename]}:#{(e.line ? e.line + 1 : @index) + @options[:line] - 1}" if @index
raise
end
# Processes the template and returns the result as a string.
#
# +scope+ is the context in which the template is evaluated.
# If it's a Binding or Proc object,
# Haml uses it as the second argument to Kernel#eval;
# otherwise, Haml just uses its #instance_eval context.
#
# Note that Haml modifies the evaluation context
# (either the scope object or the "self" object of the scope binding).
# It extends Haml::Helpers, and various instance variables are set
# (all prefixed with "haml").
# For example:
#
# s = "foobar"
# Haml::Engine.new("%p= upcase").render(s) #=> "<p>FOOBAR</p>"
#
# # s now extends Haml::Helpers
# s.responds_to?(:html_attrs) #=> true
#
# +locals+ is a hash of local variables to make available to the template.
# For example:
#
# Haml::Engine.new("%p= foo").render(Object.new, :foo => "Hello, world!") #=> "<p>Hello, world!</p>"
#
# If a block is passed to render,
# that block is run when +yield+ is called
# within the template.
#
# Due to some Ruby quirks,
# if scope is a Binding or Proc object and a block is given,
# the evaluation context may not be quite what the user expects.
# In particular, it's equivalent to passing <tt>eval("self", scope)</tt> as scope.
# This won't have an effect in most cases,
# but if you're relying on local variables defined in the context of scope,
# they won't work.
def render(scope = Object.new, locals = {}, &block)
buffer = Haml::Buffer.new(scope.instance_variable_get('@haml_buffer'), options_for_buffer)
if scope.is_a?(Binding) || scope.is_a?(Proc)
scope_object = eval("self", scope)
scope = scope_object.instance_eval{binding} if block_given?
else
scope_object = scope
scope = scope_object.instance_eval{binding}
end
set_locals(locals.merge(:_hamlout => buffer, :_erbout => buffer.buffer), scope, scope_object)
scope_object.instance_eval do
extend Haml::Helpers
@haml_buffer = buffer
end
eval(@precompiled, scope, @options[:filename], @options[:line])
# Get rid of the current buffer
scope_object.instance_eval do
@haml_buffer = buffer.upper
end
buffer.buffer
end
alias_method :to_html, :render
# Returns a proc that, when called,
# renders the template and returns the result as a string.
#
# +scope+ works the same as it does for render.
#
# The first argument of the returned proc is a hash of local variable names to values.
# However, due to an unfortunate Ruby quirk,
# the local variables which can be assigned must be pre-declared.
# This is done with the +local_names+ argument.
# For example:
#
# # This works
# Haml::Engine.new("%p= foo").render_proc(Object.new, :foo).call :foo => "Hello!"
# #=> "<p>Hello!</p>"
#
# # This doesn't
# Haml::Engine.new("%p= foo").render_proc.call :foo => "Hello!"
# #=> NameError: undefined local variable or method `foo'
#
# The proc doesn't take a block;
# any yields in the template will fail.
def render_proc(scope = Object.new, *local_names)
if scope.is_a?(Binding) || scope.is_a?(Proc)
scope_object = eval("self", scope)
else
scope_object = scope
scope = scope_object.instance_eval{binding}
end
eval("Proc.new { |*_haml_locals| _haml_locals = _haml_locals[0] || {};" +
precompiled_with_ambles(local_names) + "}\n", scope, @options[:filename], @options[:line])
end
# Defines a method on +object+
# with the given name
# that renders the template and returns the result as a string.
#
# If +object+ is a class or module,
# the method will instead by defined as an instance method.
# For example:
#
# t = Time.now
# Haml::Engine.new("%p\n Today's date is\n .date= self.to_s").def_method(t, :render)
# t.render #=> "<p>\n Today's date is\n <div class='date'>Fri Nov 23 18:28:29 -0800 2007</div>\n</p>\n"
#
# Haml::Engine.new(".upcased= upcase").def_method(String, :upcased_div)
# "foobar".upcased_div #=> "<div class='upcased'>FOOBAR</div>\n"
#
# The first argument of the defined method is a hash of local variable names to values.
# However, due to an unfortunate Ruby quirk,
# the local variables which can be assigned must be pre-declared.
# This is done with the +local_names+ argument.
# For example:
#
# # This works
# obj = Object.new
# Haml::Engine.new("%p= foo").def_method(obj, :render, :foo)
# obj.render(:foo => "Hello!") #=> "<p>Hello!</p>"
#
# # This doesn't
# obj = Object.new
# Haml::Engine.new("%p= foo").def_method(obj, :render)
# obj.render(:foo => "Hello!") #=> NameError: undefined local variable or method `foo'
#
# Note that Haml modifies the evaluation context
# (either the scope object or the "self" object of the scope binding).
# It extends Haml::Helpers, and various instance variables are set
# (all prefixed with "haml").
def def_method(object, name, *local_names)
method = object.is_a?(Module) ? :module_eval : :instance_eval
object.send(method, "def #{name}(_haml_locals = {}); #{precompiled_with_ambles(local_names)}; end",
@options[:filename], @options[:line])
end
private
def set_locals(locals, scope, scope_object)
scope_object.send(:instance_variable_set, '@_haml_locals', locals)
set_locals = locals.keys.map { |k| "#{k} = @_haml_locals[#{k.inspect}]" }.join("\n")
eval(set_locals, scope)
end
# Returns a hash of options that Haml::Buffer cares about.
# This should remain loadable from #inspect.
def options_for_buffer
{
:autoclose => @options[:autoclose],
:preserve => @options[:preserve],
:attr_wrapper => @options[:attr_wrapper],
:ugly => @options[:ugly],
:format => @options[:format]
}
end
end
end
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/tree/directive_node'
require 'sass/constant'
require 'sass/error'
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 = ?@
# Designates a non-parsed rule.
ESCAPE_CHAR = ?\\
# Designates block as mixin definition rather than CSS rules to output
MIXIN_DEFINITION_CHAR = ?=
# Includes named mixin declared using MIXIN_DEFINITION_CHAR
MIXIN_INCLUDE_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.rdoc for available options.
#
#--
#
# TODO: Add current options to REFRENCE. Remember :filename!
#
# When adding options, remember to add information about them
# to README.rdoc!
#++
#
def initialize(template, options={})
@options = {
:style => :nested,
:load_paths => ['.']
}.merge! options
@template = template.split(/\r\n|\r|\n/)
@lines = []
@constants = {"important" => "!important"}
@mixins = {}
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 mixins
@mixins
end
def render_to_tree
split_lines
root = Tree::Node.new(@options[:style])
index = 0
while @lines[index]
old_index = index
child, index = build_tree(index)
if child.is_a? Tree::Node
child.line = old_index + 1
root << child
elsif child.is_a? Array
child.each do |c|
root << c
end
end
end
@lines.clear
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 = nil
@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
raise SyntaxError.new("Indenting at the beginning of the document is illegal.", @line) if old_tabs.nil? && tabs > 0
if old_tabs && tabs - old_tabs > 1
raise SyntaxError.new("#{tabs * 2} spaces were used for indentation. Sass must be indented using two spaces.", @line)
end
@lines << [line.strip, tabs]
old_tabs = tabs
else
@lines << ['//', old_tabs || 0]
end
end
@line = nil
end
# Counts the tabulation of a line.
def count_tabs(line)
return nil if line.strip.empty?
return nil unless spaces = line.index(/[^ ]/)
if spaces % 2 == 1
raise SyntaxError.new(<<END.strip, @line)
#{spaces} space#{spaces == 1 ? ' was' : 's were'} used for indentation. Sass must be indented using two spaces.
END
elsif line[spaces] == ?\t
raise SyntaxError.new(<<END.strip, @line)
A tab character was used for indentation. Sass must be indented using two spaces.
Are you sure you have soft tabs enabled in your editor?
END
end
spaces / 2
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 + 1)
elsif node.is_a? Array
# arrays can either be full of import statements
# or attributes from mixin includes
# in either case they shouldn't have children.
# Need to peek into the array in order to give meaningful errors
directive_type = (node.first.is_a?(Tree::DirectiveNode) ? "import" : "mixin")
raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath #{directive_type} directives.", @line + 1)
end
end
index = @line if node == :mixin
return node, index
end
node.line = @line
if node.is_a? Tree::CommentNode
while has_children
line, index = raw_next_line(index)
node << line
has_children = has_children?(index, tabs)
end
return node, index
end
# Resolve multiline rules
if node.is_a?(Tree::RuleNode)
if node.continued?
child, index = build_tree(index) if @lines[old_index = index]
if @lines[old_index].nil? || has_children?(old_index, tabs) || !child.is_a?(Tree::RuleNode)
raise SyntaxError.new("Rules can't end in commas.", @line)
end
node.add_rules child
end
node.children = child.children if child
end
while has_children
child, index = build_tree(index)
validate_and_append_child(node, child)
has_children = has_children?(index, tabs)
end
return node, index
end
def validate_and_append_child(parent, child)
case child
when :constant
raise SyntaxError.new("Constants may only be declared at the root of a document.", @line)
when :mixin
raise SyntaxError.new("Mixins may only be defined at the root of a document.", @line)
when Array
child.each do |c|
if c.is_a?(Tree::DirectiveNode)
raise SyntaxError.new("Import directives may only be used at the root of a document.", @line)
end
parent << c
end
when Tree::Node
parent << child
end
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
if line[1] != ATTRIBUTE_CHAR
parse_attribute(line, ATTRIBUTE)
else
# Support CSS3-style pseudo-elements,
# which begin with ::
Tree::RuleNode.new(line, @options[:style])
end
when Constant::CONSTANT_CHAR
parse_constant(line)
when COMMENT_CHAR
parse_comment(line)
when DIRECTIVE_CHAR
parse_directive(line)
when ESCAPE_CHAR
Tree::RuleNode.new(line[1..-1], @options[:style])
when MIXIN_DEFINITION_CHAR
parse_mixin_definition(line)
when MIXIN_INCLUDE_CHAR
if line[1].nil? || line[1] == ?\s
Tree::RuleNode.new(line, @options[:style])
else
parse_mixin_include(line)
end
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)
if @options[:attribute_syntax] == :normal &&
attribute_regx == ATTRIBUTE_ALTERNATE
raise SyntaxError.new("Illegal attribute syntax: can't use alternate syntax when :attribute_syntax => :normal is set.")
elsif @options[:attribute_syntax] == :alternate &&
attribute_regx == ATTRIBUTE
raise SyntaxError.new("Illegal attribute syntax: can't use normal syntax when :attribute_syntax => :alternate is set.")
end
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, op, value = line.scan(Sass::Constant::MATCH)[0]
unless name && value
raise SyntaxError.new("Invalid constant: \"#{line}\".", @line)
end
constant = Sass::Constant.parse(value, @constants, @line)
if op == '||='
@constants[name] ||= constant
else
@constants[name] = constant
end
: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)
# If value begins with url( or ",
# it's a CSS @import rule and we don't want to touch it.
if directive == "import" && value !~ /^(url\(|")/
import(value)
else
Tree::DirectiveNode.new(line, @options[:style])
end
end
def parse_mixin_definition(line)
mixin_name = line[1..-1].strip
@mixins[mixin_name] = []
index = @line
line, tabs = @lines[index]
while !line.nil? && tabs > 0
child, index = build_tree(index)
validate_and_append_child(@mixins[mixin_name], child)
line, tabs = @lines[index]
end
:mixin
end
def parse_mixin_include(line)
mixin_name = line[1..-1]
unless @mixins.has_key?(mixin_name)
raise SyntaxError.new("Undefined mixin '#{mixin_name}'.", @line)
end
@mixins[mixin_name]
end
def import(files)
nodes = []
files.split(/,\s*/).each do |filename|
engine = nil
begin
filename = self.class.find_file_to_import(filename, @options[:load_paths])
rescue Exception => e
raise SyntaxError.new(e.message, @line)
end
if filename =~ /\.css$/
nodes << Tree::DirectiveNode.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
engine.mixins.merge! @mixins
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
@mixins = engine.mixins
end
end
nodes
end
def self.find_file_to_import(filename, load_paths)
was_sass = false
original_filename = filename