Skip to content

Instantly share code, notes, and snippets.

@vangberg
Created February 22, 2009 01:06
Show Gist options
  • Star 26 You must be signed in to star a gist
  • Fork 12 You must be signed in to fork a gist
  • Save vangberg/68277 to your computer and use it in GitHub Desktop.
Save vangberg/68277 to your computer and use it in GitHub Desktop.
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
if filename[-5..-1] == ".sass"
filename = filename[0...-5]
was_sass = true
elsif filename[-4..-1] == ".css"
return filename
end
new_filename = find_full_path("#{filename}.sass", load_paths)
return new_filename if new_filename
return filename + '.css' unless was_sass
raise SyntaxError.new("File to import not found or unreadable: #{original_filename}.", @line)
end
def self.find_full_path(filename, load_paths)
load_paths.each do |path|
["_#{filename}", filename].each do |name|
full_path = File.join(path, name)
if File.readable?(full_path)
return full_path
end
end
end
nil
end
end
end
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../test_helper'
class EngineTest < Test::Unit::TestCase
# A map of erroneous Haml documents to the error messages they should produce.
# The error messages may be arrays;
# if so, the second element should be the line number that should be reported for the error.
# If this isn't provided, the tests will assume the line number should be the last line of the document.
EXCEPTION_MAP = {
"!!!\n a" => "Illegal nesting: nesting within a header command is illegal.",
"a\n b" => "Illegal nesting: nesting within plain text is illegal.",
"/ a\n b" => "Illegal nesting: nesting within a tag that already has content is illegal.",
"% a" => 'Invalid tag: "% a".',
"%p a\n b" => "Illegal nesting: content can't be both given on the same line as %p and nested within it.",
"%p=" => "There's no Ruby code for = to evaluate.",
"%p~" => "There's no Ruby code for ~ to evaluate.",
"~" => "There's no Ruby code for ~ to evaluate.",
"=" => "There's no Ruby code for = to evaluate.",
"%p/\n a" => "Illegal nesting: nesting within a self-closing tag is illegal.",
"%p\n\ta" => <<END.strip,
A tab character was used for indentation. Haml must be indented using two spaces.
Are you sure you have soft tabs enabled in your editor?
END
"%p\n a" => "1 space was used for indentation. Haml must be indented using two spaces.",
"%p\n a" => "3 spaces were used for indentation. Haml must be indented using two spaces.",
"%p\n a" => "4 spaces were used for indentation. Haml must be indented using two spaces.",
":a\n b" => ['Filter "a" is not defined.', 1],
":a= b" => 'Invalid filter name ":a= b".',
"." => "Illegal element: classes and ids must have values.",
".#" => "Illegal element: classes and ids must have values.",
".{} a" => "Illegal element: classes and ids must have values.",
".= a" => "Illegal element: classes and ids must have values.",
"%p..a" => "Illegal element: classes and ids must have values.",
"%a/ b" => "Self-closing tags can't have content.",
" %p foo" => "Indenting at the beginning of the document is illegal.",
" %p foo" => "Indenting at the beginning of the document is illegal.",
"- end" => "You don't need to use \"- end\" in Haml. Use indentation instead:\n- if foo?\n %strong Foo!\n- else\n Not foo.",
" \n\t\n %p foo" => ["Indenting at the beginning of the document is illegal.", 3],
# Regression tests
"- raise 'foo'\n\n\n\nbar" => ["foo", 1],
"= 'foo'\n-raise 'foo'" => ["foo", 2],
"\n\n\n- raise 'foo'" => ["foo", 4],
"foo\n\n\n bar" => ["Illegal nesting: nesting within plain text is illegal.", 4],
"%p/\n\n bar" => ["Illegal nesting: nesting within a self-closing tag is illegal.", 3],
"%p foo\n\n bar" => ["Illegal nesting: content can't be both given on the same line as %p and nested within it.", 3],
"/ foo\n\n bar" => ["Illegal nesting: nesting within a tag that already has content is illegal.", 3],
"!!!\n\n bar" => ["Illegal nesting: nesting within a header command is illegal.", 3],
"foo\n\n\n\tbar" => [<<END.strip, 4],
A tab character was used for indentation. Haml must be indented using two spaces.
Are you sure you have soft tabs enabled in your editor?
END
"foo\n:ruby\n 1\n 2\n 3\n- raise 'foo'" => ["foo", 6],
}
User = Struct.new('User', :id)
def render(text, options = {}, &block)
scope = options.delete(:scope) || Object.new
locals = options.delete(:locals) || {}
engine(text, options).to_html(scope, locals, &block)
end
def engine(text, options = {})
unless options[:filename]
# use caller method name as fake filename. useful for debugging
i = -1
caller[i+=1] =~ /`(.+?)'/ until $1 and $1.index('test_') == 0
options[:filename] = "(#{$1})"
end
Haml::Engine.new(text, options)
end
def test_empty_render_should_remain_empty
assert_equal('', render(''))
end
def test_attributes_should_render_correctly
assert_equal("<div class='atlantis' style='ugly'></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'></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_one_liner_with_newline_shouldnt_be_one_line
assert_equal("<p>\n foo\n bar\n</p>", render('%p= "foo\nbar"').chomp)
end
def test_multi_render
engine = engine("%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_double_equals_in_the_middle_of_a_string
assert_equal("\"title 'Title'. \"\n",
render("== \"title '\#{\"Title\"}'. \""))
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
def test_static_attributes_with_empty_attr
assert_equal("<img alt='' src='/foo.png' />\n", render("%img{:src => '/foo.png', :alt => ''}"))
end
def test_dynamic_attributes_with_empty_attr
assert_equal("<img alt='' src='/foo.png' />\n", render("%img{:width => nil, :src => '/foo.png', :alt => String.new}"))
end
def test_attr_hashes_not_modified
hash = {:color => 'red'}
assert_equal(<<HTML, render(<<HAML, :locals => {:hash => hash}))
<div color='red'></div>
<div class='special' color='red'></div>
<div color='red'></div>
HTML
%div{hash}
.special{hash}
%div{hash}
HAML
assert_equal(hash, {:color => 'red'})
end
def test_end_of_file_multiline
assert_equal("<p>0</p>\n<p>1</p>\n<p>2</p>\n", render("- for i in (0...3)\n %p= |\n i |"))
end
def test_cr_newline
assert_equal("<p>foo</p>\n<p>bar</p>\n<p>baz</p>\n<p>boom</p>\n", render("%p foo\r%p bar\r\n%p baz\n\r%p boom"))
end
def test_textareas
assert_equal("<textarea>Foo&#x000A; bar&#x000A; baz</textarea>\n",
render('%textarea= "Foo\n bar\n baz"'))
assert_equal("<pre>Foo&#x000A; bar&#x000A; baz</pre>\n",
render('%pre= "Foo\n bar\n baz"'))
assert_equal("<textarea>#{'a' * 100}</textarea>\n",
render("%textarea #{'a' * 100}"))
assert_equal("<p>\n <textarea>Foo\n Bar\n Baz</textarea>\n</p>\n", render(<<SOURCE))
%p
%textarea
Foo
Bar
Baz
SOURCE
end
def test_boolean_attributes
assert_equal("<p bar baz='true' foo='bar'></p>\n",
render("%p{:foo => 'bar', :bar => true, :baz => 'true'}", :format => :html4))
assert_equal("<p bar='bar' baz='true' foo='bar'></p>\n",
render("%p{:foo => 'bar', :bar => true, :baz => 'true'}", :format => :xhtml))
assert_equal("<p baz='false' foo='bar'></p>\n",
render("%p{:foo => 'bar', :bar => false, :baz => 'false'}", :format => :html4))
assert_equal("<p baz='false' foo='bar'></p>\n",
render("%p{:foo => 'bar', :bar => false, :baz => 'false'}", :format => :xhtml))
end
def test_whitespace_nuke_with_both_newlines
# Regression test
assert_equal("<p>foo</p>\n", render('%p<= "\nfoo\n"'))
assert_equal(<<HTML, render(<<HAML))
<p>
<p>foo</p>
</p>
HTML
%p
%p<= "\\nfoo\\n"
HAML
end
def test_both_whitespace_nukes_work_together
assert_equal(<<RESULT, render(<<SOURCE))
<p><q>Foo
Bar</q></p>
RESULT
%p
%q><= "Foo\\nBar"
SOURCE
end
# Mostly a regression test
def test_both_case_indentation_work_with_deeply_nested_code
result = <<RESULT
<h2>
other
</h2>
RESULT
assert_equal(result, render(<<HAML))
- case 'other'
- when 'test'
%h2
hi
- when 'other'
%h2
other
HAML
assert_equal(result, render(<<HAML))
- case 'other'
- when 'test'
%h2
hi
- when 'other'
%h2
other
HAML
end
# HTML escaping tests
def test_ampersand_equals_should_escape
assert_equal("<p>\n foo &amp; bar\n</p>\n", render("%p\n &= 'foo & bar'", :escape_html => false))
end
def test_ampersand_equals_inline_should_escape
assert_equal("<p>foo &amp; bar</p>\n", render("%p&= 'foo & bar'", :escape_html => false))
end
def test_ampersand_equals_should_escape_before_preserve
assert_equal("<textarea>foo&#x000A;bar</textarea>\n", render('%textarea&= "foo\nbar"', :escape_html => false))
end
def test_bang_equals_should_not_escape
assert_equal("<p>\n foo & bar\n</p>\n", render("%p\n != 'foo & bar'", :escape_html => true))
end
def test_bang_equals_inline_should_not_escape
assert_equal("<p>foo & bar</p>\n", render("%p!= 'foo & bar'", :escape_html => true))
end
def test_static_attributes_should_be_escaped
assert_equal("<img class='atlantis' style='ugly&amp;stupid' />\n",
render("%img.atlantis{:style => 'ugly&stupid'}"))
assert_equal("<div class='atlantis' style='ugly&amp;stupid'>foo</div>\n",
render(".atlantis{:style => 'ugly&stupid'} foo"))
assert_equal("<p class='atlantis' style='ugly&amp;stupid'>foo</p>\n",
render("%p.atlantis{:style => 'ugly&stupid'}= 'foo'"))
assert_equal("<p class='atlantis' style='ugly&#x000A;stupid'></p>\n",
render("%p.atlantis{:style => \"ugly\\nstupid\"}"))
end
def test_dynamic_attributes_should_be_escaped
assert_equal("<img alt='' src='&amp;foo.png' />\n",
render("%img{:width => nil, :src => '&foo.png', :alt => String.new}"))
assert_equal("<p alt='' src='&amp;foo.png'>foo</p>\n",
render("%p{:width => nil, :src => '&foo.png', :alt => String.new} foo"))
assert_equal("<div alt='' src='&amp;foo.png'>foo</div>\n",
render("%div{:width => nil, :src => '&foo.png', :alt => String.new}= 'foo'"))
assert_equal("<img alt='' src='foo&#x000A;.png' />\n",
render("%img{:width => nil, :src => \"foo\\n.png\", :alt => String.new}"))
end
def test_string_interpolation_should_be_esaped
assert_equal("<p>4&amp;3</p>\n", render("%p== \#{2+2}&\#{2+1}", :escape_html => true))
assert_equal("<p>4&3</p>\n", render("%p== \#{2+2}&\#{2+1}", :escape_html => false))
end
def test_escaped_inline_string_interpolation
assert_equal("<p>4&amp;3</p>\n", render("%p&== \#{2+2}&\#{2+1}", :escape_html => true))
assert_equal("<p>4&amp;3</p>\n", render("%p&== \#{2+2}&\#{2+1}", :escape_html => false))
end
def test_unescaped_inline_string_interpolation
assert_equal("<p>4&3</p>\n", render("%p!== \#{2+2}&\#{2+1}", :escape_html => true))
assert_equal("<p>4&3</p>\n", render("%p!== \#{2+2}&\#{2+1}", :escape_html => false))
end
def test_escaped_string_interpolation
assert_equal("<p>\n 4&amp;3\n</p>\n", render("%p\n &== \#{2+2}&\#{2+1}", :escape_html => true))
assert_equal("<p>\n 4&amp;3\n</p>\n", render("%p\n &== \#{2+2}&\#{2+1}", :escape_html => false))
end
def test_unescaped_string_interpolation
assert_equal("<p>\n 4&3\n</p>\n", render("%p\n !== \#{2+2}&\#{2+1}", :escape_html => true))
assert_equal("<p>\n 4&3\n</p>\n", render("%p\n !== \#{2+2}&\#{2+1}", :escape_html => false))
end
def test_scripts_should_respect_escape_html_option
assert_equal("<p>\n foo &amp; bar\n</p>\n", render("%p\n = 'foo & bar'", :escape_html => true))
assert_equal("<p>\n foo & bar\n</p>\n", render("%p\n = 'foo & bar'", :escape_html => false))
end
def test_inline_scripts_should_respect_escape_html_option
assert_equal("<p>foo &amp; bar</p>\n", render("%p= 'foo & bar'", :escape_html => true))
assert_equal("<p>foo & bar</p>\n", render("%p= 'foo & bar'", :escape_html => false))
end
def test_script_ending_in_comment_should_render_when_html_is_escaped
assert_equal("foo&amp;bar\n", render("= 'foo&bar' #comment", :escape_html => true))
end
def test_script_with_if_shouldnt_output
assert_equal(<<HTML, render(<<HAML))
<p>foo</p>
<p></p>
HTML
%p= "foo"
%p= "bar" if false
HAML
end
# Options tests
def test_filename_and_line
begin
render("\n\n = abc", :filename => 'test', :line => 2)
rescue Exception => e
assert_kind_of Haml::SyntaxError, e
assert_match(/test:4/, e.backtrace.first)
end
begin
render("\n\n= 123\n\n= nil[]", :filename => 'test', :line => 2)
rescue Exception => e
assert_kind_of NoMethodError, e
assert_match(/test:6/, e.backtrace.first)
end
end
def test_stop_eval
assert_equal("", render("= 'Hello'", :suppress_eval => true))
assert_equal("", render("- haml_concat '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))
assert_equal("", render(":ruby\n Kernel.puts 'hello'", :suppress_eval => true))
end
def test_doctypes
assert_equal('<!DOCTYPE html>',
render('!!!', :format => :html5).strip)
assert_equal('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
render('!!! strict').strip)
assert_equal('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">',
render('!!! frameset').strip)
assert_equal('<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">',
render('!!! mobile').strip)
assert_equal('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">',
render('!!! basic').strip)
assert_equal('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
render('!!! transitional').strip)
assert_equal('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
render('!!!').strip)
assert_equal('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">',
render('!!! strict', :format => :html4).strip)
assert_equal('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">',
render('!!! frameset', :format => :html4).strip)
assert_equal('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">',
render('!!! transitional', :format => :html4).strip)
assert_equal('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">',
render('!!!', :format => :html4).strip)
end
def test_attr_wrapper
assert_equal("<p strange=*attrs*></p>\n", render("%p{ :strange => 'attrs'}", :attr_wrapper => '*'))
assert_equal("<p escaped='quo\"te'></p>\n", render("%p{ :escaped => 'quo\"te'}", :attr_wrapper => '"'))
assert_equal("<p escaped=\"quo'te\"></p>\n", render("%p{ :escaped => 'quo\\'te'}", :attr_wrapper => '"'))
assert_equal("<p escaped=\"q'uo&quot;te\"></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 =&gt; baz'></p>\n", render("%p{'boom=>biddly' => 'bar => baz'}"))
assert_equal("<p foo,bar='baz, qux'></p>\n", render("%p{'foo,bar' => 'baz, qux'}"))
assert_equal("<p escaped='quo&#x000A;te'></p>\n", render("%p{ :escaped => \"quo\\nte\"}"))
assert_equal("<p escaped='quo4te'></p>\n", render("%p{ :escaped => \"quo\#{2 + 2}te\"}"))
end
def test_correct_parsing_with_brackets
assert_equal("<p class='foo'>{tada} foo</p>\n", render("%p{:class => 'foo'} {tada} foo"))
assert_equal("<p class='foo'>deep {nested { things }}</p>\n", render("%p{:class => 'foo'} deep {nested { things }}"))
assert_equal("<p class='bar foo'>{a { d</p>\n", render("%p{{:class => 'foo'}, :class => 'bar'} {a { d"))
assert_equal("<p foo='bar'>a}</p>\n", render("%p{:foo => 'bar'} a}"))
foo = []
foo[0] = Struct.new('Foo', :id).new
assert_equal("<p class='struct_foo' id='struct_foo_new'>New User]</p>\n",
render("%p[foo[0]] New User]", :locals => {:foo => foo}))
assert_equal("<p class='prefix_struct_foo' id='prefix_struct_foo_new'>New User]</p>\n",
render("%p[foo[0], :prefix] New User]", :locals => {:foo => foo}))
foo[0].id = 1
assert_equal("<p class='struct_foo' id='struct_foo_1'>New User]</p>\n",
render("%p[foo[0]] New User]", :locals => {:foo => foo}))
assert_equal("<p class='prefix_struct_foo' id='prefix_struct_foo_1'>New User]</p>\n",
render("%p[foo[0], :prefix] New User]", :locals => {:foo => foo}))
end
def test_empty_attrs
assert_equal("<p attr=''>empty</p>\n", render("%p{ :attr => '' } empty"))
assert_equal("<p attr=''>empty</p>\n", render("%p{ :attr => x } empty", :locals => {:x => ''}))
end
def test_nil_attrs
assert_equal("<p>nil</p>\n", render("%p{ :attr => nil } nil"))
assert_equal("<p>nil</p>\n", render("%p{ :attr => x } nil", :locals => {:x => nil}))
end
def test_nil_id_with_syntactic_id
assert_equal("<p id='foo'>nil</p>\n", render("%p#foo{:id => nil} nil"))
assert_equal("<p id='foo_bar'>nil</p>\n", render("%p#foo{{:id => 'bar'}, :id => nil} nil"))
assert_equal("<p id='foo_bar'>nil</p>\n", render("%p#foo{{:id => nil}, :id => 'bar'} nil"))
end
def test_nil_class_with_syntactic_class
assert_equal("<p class='foo'>nil</p>\n", render("%p.foo{:class => nil} nil"))
assert_equal("<p class='bar foo'>nil</p>\n", render("%p.bar.foo{:class => nil} nil"))
assert_equal("<p class='bar foo'>nil</p>\n", render("%p.foo{{:class => 'bar'}, :class => nil} nil"))
assert_equal("<p class='bar foo'>nil</p>\n", render("%p.foo{{:class => nil}, :class => 'bar'} nil"))
end
def test_locals
assert_equal("<p>Paragraph!</p>\n", render("%p= text", :locals => { :text => "Paragraph!" }))
end
def test_dynamic_attrs_shouldnt_register_as_literal_values
assert_equal("<p a='b2c'></p>\n", render('%p{:a => "b#{1 + 1}c"}'))
assert_equal("<p a='b2c'></p>\n", render("%p{:a => 'b' + (1 + 1).to_s + 'c'}"))
end
def test_dynamic_attrs_with_self_closed_tag
assert_equal("<a b='2' />\nc\n", render("%a{'b' => 1 + 1}/\n= 'c'\n"))
end
def test_exceptions
EXCEPTION_MAP.each do |key, value|
begin
render(key, :filename => "(exception test for #{key.inspect})")
rescue Exception => err
value = [value] unless value.is_a?(Array)
expected_message, line_no = value
line_no ||= key.split("\n").length
line_reported = err.backtrace[0].gsub(/\(.+\):/, '').to_i
assert_equal(expected_message, err.message, "Line: #{key}")
assert_equal(line_no, line_reported, "Line: #{key}")
else
assert(false, "Exception not raised for\n#{key}")
end
end
end
def test_exception_line
render("a\nb\n!!!\n c\nd")
rescue Haml::SyntaxError => e
assert_equal("(test_exception_line):4", e.backtrace[0])
else
assert(false, '"a\nb\n!!!\n c\nd" doesn\'t produce an exception')
end
def test_exception
render("%p\n hi\n %a= undefined\n= 12")
rescue Exception => e
assert_match("(test_exception):3", e.backtrace[0])
else
# Test failed... should have raised an exception
assert(false)
end
def test_compile_error
render("a\nb\n- fee)\nc")
rescue Exception => e
assert_match(/\(test_compile_error\):3: syntax error/i, e.message)
else
assert(false,
'"a\nb\n- fee)\nc" doesn\'t produce an exception!')
end
def test_unbalanced_brackets
render('== #{1 + 5} foo #{6 + 7 bar #{8 + 9}')
rescue Haml::SyntaxError => e
assert_equal("Unbalanced brackets.", e.message)
end
def test_balanced_conditional_comments
assert_equal("<!--[if !(IE 6)|(IE 7)]> Bracket: ] <![endif]-->\n",
render("/[if !(IE 6)|(IE 7)] Bracket: ]"))
end
def test_empty_filter
assert_equal(<<END, render(':javascript'))
<script type='text/javascript'>
//<![CDATA[
//]]>
</script>
END
end
def test_ugly_filter
assert_equal(<<END, render(":sass\n #foo\n bar: baz", :ugly => true))
#foo {
bar: baz; }
END
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 = User.new
assert_equal("<p class='struct_user' id='struct_user_new'>New User</p>\n",
render("%p[user] New User", :locals => {:user => user}))
end
def test_object_ref_before_attrs
user = User.new 42
assert_equal("<p class='struct_user' id='struct_user_42' style='width: 100px;'>New User</p>\n",
render("%p[user]{:style => 'width: 100px;'} New User", :locals => {:user => user}))
end
def test_non_literal_attributes
assert_equal("<p a1='foo' a2='bar' a3='baz' />\n",
render("%p{a2, a1, :a3 => 'baz'}/",
:locals => {:a1 => {:a1 => 'foo'}, :a2 => {:a2 => 'bar'}}))
end
def test_render_should_accept_a_binding_as_scope
string = "This is a string!"
string.instance_variable_set("@var", "Instance variable")
b = string.instance_eval do
var = "Local variable"
binding
end
assert_equal("<p>THIS IS A STRING!</p>\n<p>Instance variable</p>\n<p>Local variable</p>\n",
render("%p= upcase\n%p= @var\n%p= var", :scope => b))
end
def test_yield_should_work_with_binding
assert_equal("12\nFOO\n", render("= yield\n= upcase", :scope => "foo".instance_eval{binding}) { 12 })
end
def test_yield_should_work_with_def_method
s = "foo"
engine("= yield\n= upcase").def_method(s, :render)
assert_equal("12\nFOO\n", s.render { 12 })
end
def test_def_method_with_module
engine("= yield\n= upcase").def_method(String, :render_haml)
assert_equal("12\nFOO\n", "foo".render_haml { 12 })
end
def test_def_method_locals
obj = Object.new
engine("%p= foo\n.bar{:baz => baz}= boom").def_method(obj, :render, :foo, :baz, :boom)
assert_equal("<p>1</p>\n<div baz='2' class='bar'>3</div>\n", obj.render(:foo => 1, :baz => 2, :boom => 3))
end
def test_render_proc_locals
proc = engine("%p= foo\n.bar{:baz => baz}= boom").render_proc(Object.new, :foo, :baz, :boom)
assert_equal("<p>1</p>\n<div baz='2' class='bar'>3</div>\n", proc[:foo => 1, :baz => 2, :boom => 3])
end
def test_render_proc_with_binding
assert_equal("FOO\n", engine("= upcase").render_proc("foo".instance_eval{binding}).call)
end
def test_ugly_true
assert_equal("<div id='outer'>\n<div id='inner'>\n<p>hello world</p>\n</div>\n</div>\n",
render("#outer\n #inner\n %p hello world", :ugly => true))
assert_equal("<p>#{'s' * 75}</p>\n",
render("%p #{'s' * 75}", :ugly => true))
assert_equal("<p>#{'s' * 75}</p>\n",
render("%p= 's' * 75", :ugly => true))
end
def test_auto_preserve_unless_ugly
assert_equal("<pre>foo&#x000A;bar</pre>\n", render('%pre="foo\nbar"'))
assert_equal("<pre>foo\nbar</pre>\n", render("%pre\n foo\n bar"))
assert_equal("<pre>foo\nbar</pre>\n", render('%pre="foo\nbar"', :ugly => true))
assert_equal("<pre>foo\nbar</pre>\n", render("%pre\n foo\n bar", :ugly => true))
end
def test_xhtml_output_option
assert_equal "<p>\n <br />\n</p>\n", render("%p\n %br", :format => :xhtml)
assert_equal "<a />\n", render("%a/", :format => :xhtml)
end
def test_arbitrary_output_option
assert_raise(Haml::Error, "Invalid output format :html1") { engine("%br", :format => :html1) }
end
# HTML 4.0
def test_html_has_no_self_closing_tags
assert_equal "<p>\n <br>\n</p>\n", render("%p\n %br", :format => :html4)
assert_equal "<br>\n", render("%br/", :format => :html4)
end
def test_html_renders_empty_node_with_closing_tag
assert_equal "<div class='foo'></div>\n", render(".foo", :format => :html4)
end
def test_html_doesnt_add_slash_to_self_closing_tags
assert_equal "<a>\n", render("%a/", :format => :html4)
assert_equal "<a foo='2'>\n", render("%a{:foo => 1 + 1}/", :format => :html4)
assert_equal "<meta>\n", render("%meta", :format => :html4)
assert_equal "<meta foo='2'>\n", render("%meta{:foo => 1 + 1}", :format => :html4)
end
def test_html_ignores_xml_prolog_declaration
assert_equal "", render('!!! XML', :format => :html4)
end
def test_html_has_different_doctype
assert_equal %{<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">\n},
render('!!!', :format => :html4)
end
# because anything before the doctype triggers quirks mode in IE
def test_xml_prolog_and_doctype_dont_result_in_a_leading_whitespace_in_html
assert_no_match(/^\s+/, render("!!! xml\n!!!", :format => :html4))
end
# HTML5
def test_html5_doctype
assert_equal %{<!DOCTYPE html>\n}, render('!!!', :format => :html5)
end
end
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../test_helper'
require 'sass/engine'
class SassEngineTest < Test::Unit::TestCase
# A map of erroneous Sass documents to the error messages they should produce.
# The error messages may be arrays;
# if so, the second element should be the line number that should be reported for the error.
# If this isn't provided, the tests will assume the line number should be the last line of the document.
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" => <<END.strip,
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
"a\n :b c" => "1 space was used for indentation. Sass must be indented using two spaces.",
"a\n :b c" => "4 spaces were used for indentation. Sass must be indented using two spaces.",
"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 '&'.", 1],
"a\n :b\n c" => "Illegal nesting: Only attributes may be nested beneath attributes.",
"a,\n :b c" => "Rules can\'t end in commas.",
"a," => "Rules can\'t end in commas.",
"a,\n!b = c" => "Rules can\'t end in commas.",
"!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 baz !" => "Unterminated constant.",
"!foo = !(foo)" => "Invalid constant.",
"=foo\n :color red\n.bar\n +bang" => "Undefined mixin 'bang'.",
".bar\n =foo\n :color red\n" => "Mixins may only be defined at the root of a document.",
"=foo\n :color red\n.bar\n +foo\n :color red" => "Illegal nesting: Nothing may be nested beneath mixin directives.",
" a\n b: c" => ["Indenting at the beginning of the document is illegal.", 1],
" \n \n\t\n a\n b: c" => ["Indenting at the beginning of the document is illegal.", 4],
# Regression tests
"a\n b:\n c\n d" => ["Illegal nesting: Only attributes may be nested beneath attributes.", 3],
"& foo\n bar: baz\n blat: bang" => ["Base-level rules cannot contain the parent-selector-referencing character '&'.", 1],
"a\n b: c\n& foo\n bar: baz\n blat: bang" => ["Base-level rules cannot contain the parent-selector-referencing character '&'.", 3],
}
def test_basic_render
renders_correctly "basic", { :style => :compact }
end
def test_multiple_calls_to_render
sass = Sass::Engine.new("a\n b: c")
assert_equal sass.render, sass.render
end
def test_alternate_styles
renders_correctly "expanded", { :style => :expanded }
renders_correctly "compact", { :style => :compact }
renders_correctly "nested", { :style => :nested }
renders_correctly "compressed", { :style => :compressed }
end
def test_exceptions
EXCEPTION_MAP.each do |key, value|
begin
Sass::Engine.new(key).render
rescue Sass::SyntaxError => err
value = [value] unless value.is_a?(Array)
assert_equal(value.first, err.message, "Line: #{key}")
assert_equal(value[1] || key.split("\n").length, 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_css_import
assert_equal("@import url(./fonts.css) screen;", render("@import url(./fonts.css) screen"))
assert_equal("@import \"./fonts.css\" screen;", render("@import \"./fonts.css\" screen"))
end
def test_sass_import
renders_correctly "import", { :style => :compact, :load_paths => [File.dirname(__FILE__) + "/templates"] }
end
def test_default_function
assert_equal("foo {\n bar: url(foo.png); }\n", render("foo\n bar = url(foo.png)\n"));
assert_equal("foo {\n bar: url(); }\n", render("foo\n bar = url()\n"));
end
def test_basic_multiline_selector
assert_equal("#foo #bar,\n#baz #boom {\n foo: bar; }\n",
render("#foo #bar,\n#baz #boom\n :foo bar"))
assert_equal("#foo #bar,\n#foo #baz {\n foo: bar; }\n",
render("#foo\n #bar,\n #baz\n :foo bar"))
assert_equal("#foo,\n#bar {\n foo: bar; }\n #foo #baz,\n #bar #baz {\n foo: bar; }\n",
render("#foo,\n#bar\n :foo bar\n #baz\n :foo bar"))
assert_equal("#foo #bar, #baz #boom { foo: bar; }\n",
render("#foo #bar,\n#baz #boom\n :foo bar", :style => :compact))
assert_equal("#foo #bar,#baz #boom{foo:bar}\n",
render("#foo #bar,\n#baz #boom\n :foo bar", :style => :compressed))
end
def test_complex_multiline_selector
renders_correctly "multiline"
end
def test_colon_only
begin
render("a\n b: c", :attribute_syntax => :normal)
rescue Sass::SyntaxError => e
assert_equal("Illegal attribute syntax: can't use alternate syntax when :attribute_syntax => :normal is set.",
e.message)
else
assert(false, "SyntaxError not raised for :attribute_syntax => :normal")
end
begin
render("a\n :b c", :attribute_syntax => :alternate)
rescue Sass::SyntaxError => e
assert_equal("Illegal attribute syntax: can't use normal syntax when :attribute_syntax => :alternate is set.",
e.message)
else
assert(false, "SyntaxError not raised for :attribute_syntax => :alternate")
end
end
def test_pseudo_elements
assert_equal(<<CSS, render(<<SASS))
::first-line {
size: 10em; }
CSS
::first-line
size: 10em
SASS
end
def test_directive
assert_equal("@a b;", render("@a b"))
assert_equal("@a {\n b: c; }\n", render("@a\n :b c"))
assert_equal("@a { b: c; }\n", render("@a\n :b c", :style => :compact))
assert_equal("@a {\n b: c;\n}\n", render("@a\n :b c", :style => :expanded))
assert_equal("@a{b:c}\n", render("@a\n :b c", :style => :compressed))
assert_equal("@a {\n b: c;\n d: e; }\n",
render("@a\n :b c\n :d e"))
assert_equal("@a { b: c; d: e; }\n",
render("@a\n :b c\n :d e", :style => :compact))
assert_equal("@a {\n b: c;\n d: e;\n}\n",
render("@a\n :b c\n :d e", :style => :expanded))
assert_equal("@a{b:c;d:e}\n",
render("@a\n :b c\n :d e", :style => :compressed))
assert_equal("@a {\n #b {\n c: d; } }\n",
render("@a\n #b\n :c d"))
assert_equal("@a { #b { c: d; } }\n",
render("@a\n #b\n :c d", :style => :compact))
assert_equal("@a {\n #b {\n c: d;\n }\n}\n",
render("@a\n #b\n :c d", :style => :expanded))
assert_equal("@a{#b{c:d}}\n",
render("@a\n #b\n :c d", :style => :compressed))
assert_equal("@a {\n #b {\n a: b; }\n #b #c {\n d: e; } }\n",
render("@a\n #b\n :a b\n #c\n :d e"))
assert_equal("@a { #b { a: b; }\n #b #c { d: e; } }\n",
render("@a\n #b\n :a b\n #c\n :d e", :style => :compact))
assert_equal("@a {\n #b {\n a: b;\n }\n #b #c {\n d: e;\n }\n}\n",
render("@a\n #b\n :a b\n #c\n :d e", :style => :expanded))
assert_equal("@a{#b{a:b}#b #c{d:e}}\n",
render("@a\n #b\n :a b\n #c\n :d e", :style => :compressed))
assert_equal("@a {\n #foo,\n #bar {\n b: c; } }\n",
render("@a\n #foo, \n #bar\n :b c"))
assert_equal("@a { #foo, #bar { b: c; } }\n",
render("@a\n #foo, \n #bar\n :b c", :style => :compact))
assert_equal("@a {\n #foo,\n #bar {\n b: c;\n }\n}\n",
render("@a\n #foo, \n #bar\n :b c", :style => :expanded))
assert_equal("@a{#foo,#bar{b:c}}\n",
render("@a\n #foo, \n #bar\n :b c", :style => :compressed))
to_render = <<END
@a
:b c
#d
:e f
:g h
END
rendered = <<END
@a { b: c;
#d { e: f; }
g: h; }
END
assert_equal(rendered, render(to_render, :style => :compact))
assert_equal("@a{b:c;#d{e:f}g:h}\n", render(to_render, :style => :compressed))
end
def test_empty_first_line
assert_equal("#a {\n b: c; }\n", render("#a\n\n b: c"))
end
def test_escaped_rule
assert_equal(":focus {\n a: b; }\n", render("\\:focus\n a: b"))
assert_equal("a {\n b: c; }\n a :focus {\n d: e; }\n", render("\\a\n b: c\n \\:focus\n d: e"))
end
def test_cr_newline
assert_equal("foo {\n a: b;\n c: d;\n e: f; }\n", render("foo\r a: b\r\n c: d\n\r e: f"))
end
def test_or_eq
assert_equal("foo {\n a: b; }\n", render("!foo = b\n!foo ||= c\nfoo\n a = !foo"))
assert_equal("foo {\n a: b; }\n", render("!foo ||= b\nfoo\n a = !foo"))
end
def test_mixins
renders_correctly "mixins", { :style => :expanded }
end
def test_mixins_dont_interfere_with_sibling_combinator
assert_equal("foo + bar {\n a: b; }\n", render("foo\n + bar\n a: b"))
assert_equal("foo + bar {\n a: b; }\nfoo + baz {\n c: d; }\n",
render("foo\n +\n bar\n a: b\n baz\n c: d"))
end
private
def render(sass, options = {})
Sass::Engine.new(sass, options).render
end
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
module Haml
# An exception raised by Haml code.
class Error < StandardError
# :stopdoc:
# By default, an error is taken to refer to the line of the template
# that was being processed when the exception was raised.
# However, if line is non-nil, it + 1 is used instead.
attr_reader :line
def initialize(message = nil, line = nil)
super(message)
@line = line
end
# :startdoc:
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 < Haml::Error; end
end
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
= "not me!"
= "nor me!"
- haml_concat "not even me!"
%p= "NO!"
%p~ "UH-UH!"
%h1 Me!
#foo
%p#bar All
%br/
%p.baz This
Should render
<p></p>
<p></p>
<h1>Me!</h1>
<div id='foo'>
<p id='bar'>All</p>
<br />
<p class='baz'>This</p>
Should render
</div>
require 'optparse'
require 'fileutils'
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))
@opts.parse!(@args)
process_result
@options
rescue Exception => e
raise e if @options[:trace] || e.is_a?(SystemExit)
$stderr.puts e.message
exit 1
end
exit 0
end
def to_s
@opts.to_s
end
protected
def get_line(exception)
# SyntaxErrors have weird line reporting
# when there's trailing whitespace,
# which there is for Haml documents.
return exception.message.scan(/:(\d+)/).first.first if exception.is_a?(::SyntaxError)
exception.backtrace[0].scan(/:(\d+)/).first.first
end
private
def set_opts(opts)
opts.on('-s', '--stdin', :NONE, 'Read input from standard input instead of an input file') do
@options[:input] = $stdin
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 #{::Haml.version[:string]}")
exit
end
end
def process_result
input, output = @options[:input], @options[:output]
input_file, output_file = if input
[nil, open_file(ARGV[0], 'w')]
else
[open_file(ARGV[0]), open_file(ARGV[1], 'w')]
end
input ||= input_file
output ||= output_file
input ||= $stdin
output ||= $stdout
@options[:input], @options[:output] = input, output
end
def open_file(filename, flag = 'r')
return if filename.nil?
File.open(filename, flag)
end
end
# A class encapsulating the executable functionality
# specific to Haml and Sass.
class HamlSass < Generic # :nodoc:
def initialize(args)
super
@options[:for_engine] = {}
end
private
def set_opts(opts)
opts.banner = <<END
Usage: #{@name.downcase} [options] [INPUT] [OUTPUT]
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 and Sass 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)
print "Directory #{dir} already exists, overwrite [y/N]? "
exit if gets !~ /y/i
FileUtils.rm_rf(dir)
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 "require 'rubygems'"
file << File.read(File.dirname(__FILE__) + "/../../init.rb")
end
puts "Haml plugin added to #{original_dir}"
exit
end
opts.on('-c', '--check', "Just check syntax, don't evaluate.") do
require 'stringio'
@options[:check_syntax] = true
@options[:output] = StringIO.new
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 set_opts(opts)
super
opts.on('-t', '--style NAME',
'Output style. Can be nested (default), compact, compressed, or expanded.') do |name|
@options[:for_engine][:style] = name.to_sym
end
end
def process_result
super
input = @options[:input]
output = @options[:output]
template = input.read()
input.close() if input.is_a? File
begin
# We don't need to do any special handling of @options[:check_syntax] here,
# because the Sass syntax checking happens alongside evaluation
# and evaluation doesn't actually evaluate any code anyway.
result = ::Sass::Engine.new(template, @options[:for_engine]).render
rescue ::Sass::SyntaxError => e
raise e if @options[:trace]
raise "Syntax error on line #{get_line e}: #{e.message}"
end
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"
@options[:requires] = []
@options[:load_paths] = []
end
def set_opts(opts)
super
opts.on('-t', '--style NAME',
'Output style. Can be indented (default) or ugly.') do |name|
@options[:for_engine][:ugly] = true if name.to_sym == :ugly
end
opts.on('-f', '--format NAME',
'Output format. Can be xhtml (default), html4, or html5.') do |name|
@options[:for_engine][:format] = name.to_sym
end
opts.on('-e', '--escape-html',
'Escape HTML characters (like ampersands and angle brackets) by default.') do
@options[:for_engine][:escape_html] = true
end
opts.on('-r', '--require FILE', "Same as 'ruby -r'.") do |file|
@options[:requires] << file
end
opts.on('-I', '--load-path PATH', "Same as 'ruby -I'.") do |path|
@options[:load_paths] << path
end
opts.on('--debug', "Print out the precompiled Ruby source.") do
@options[:debug] = true
end
end
def process_result
super
input = @options[:input]
output = @options[:output]
template = input.read()
input.close() if input.is_a? File
begin
engine = ::Haml::Engine.new(template, @options[:for_engine])
if @options[:check_syntax]
puts "Syntax OK"
return
end
@options[:load_paths].each {|p| $LOAD_PATH << p}
@options[:requires].each {|f| require f}
if @options[:debug]
puts engine.precompiled
puts '=' * 100
end
result = engine.to_html
rescue Exception => e
raise e if @options[:trace]
case e
when ::Haml::SyntaxError; raise "Syntax error on line #{get_line e}: #{e.message}"
when ::Haml::Error; raise "Haml error on line #{get_line e}: #{e.message}"
else raise "Exception on line #{get_line e}: #{e.message}\n Use --trace for backtrace."
end
end
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] [INPUT] [OUTPUT]
Description: Transforms an HTML file into corresponding Haml code.
Options:
END
opts.on('-r', '--rhtml', 'Parse RHTML tags.') do
@module_opts[:rhtml] = true
end
opts.on('--no-rhtml', "Don't parse RHTML tags.") do
@options[:no_rhtml] = true
end
opts.on('-x', '--xhtml', 'Parse the input using the more strict XHTML parser.') do
@module_opts[:xhtml] = true
end
super
end
def process_result
super
input = @options[:input]
output = @options[:output]
@module_opts[:rhtml] ||= input.respond_to?(:path) && input.path =~ /\.(rhtml|erb)$/
@module_opts[:rhtml] &&= @options[:no_rhtml] != false
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
@module_opts = {}
require 'sass/css'
end
def set_opts(opts)
opts.banner = <<END
Usage: css2sass [options] [INPUT] [OUTPUT]
Description: Transforms a CSS file into corresponding Sass code.
Options:
END
opts.on('-a', '--alternate', 'Output using alternative Sass syntax (margin: 1px)') do
@module_opts[:alternate] = true
end
super
end
def process_result
super
input = @options[:input]
output = @options[:output]
output.write(::Sass::CSS.new(input, @module_opts).render)
end
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
= Frequently Asked Questions
== Haml
=== How do I put a punctuation mark after an element, like "<tt>I like <strong>cake</strong>!</tt>"?
Expressing the structure of a document
and expressing inline formatting are two very different problems.
Haml is mostly designed for structure,
so the best way to deal with formatting is to leave it to other languages
that are designed for it.
You could use Textile:
%p
:textile
I like *cake*!
or Markdown:
%p
:markdown
I like **cake**!
or plain old XHTML:
%p I like <strong>cake</strong>!
If you're inserting something that's generated by a helper, like a link,
then it's even easier:
%p== I like #{link_to 'chocolate', 'http://franschocolates.com'}!
=== How do I stop Haml from indenting the contents of my +pre+ and +textarea+ tags?
Because Haml automatically indents the HTML source code,
the contents of whitespace-sensitive tags like +pre+ and +textarea+
can get screwed up.
The solution is to replace the newlines inside these tags
with HTML newline entities (<tt>&#x000A;</tt>),
which Haml does using the Haml::Helpers#preserve and Haml::Helpers#find_and_preserve helpers.
Normally, Haml will do this for you automatically
when you're using a tag that needs it
(this can be customized using the <tt>:preserve</tt> option;
see the Options section of the {Haml reference}(../classes/Haml.html)).
For example,
%p
%textarea= "Foo\nBar"
will be compiled to
<p>
<textarea>Foo&#x000A;Bar</textarea>
</p>
However, if a helper is generating the tag,
Haml can't detect that and so you'll have to call +find_and_preserve+ yourself.
You can also use <tt>~</tt>, which is the same as <tt>=</tt>
except that it automatically runs +find_and_preserve+ on its input.
For example:
%p= find_and_preserve "<textarea>Foo\nBar</textarea>"
is the same as
%p~ "<textarea>Foo\nBar</textarea>"
and renders
<p><textarea>Foo&#x000A;Bar</textarea></p>
=== How do I make my long lines of Ruby code look nicer in my Haml document?
Put them in a helper or your model.
Haml purposefully makes it annoying to put lots of Ruby code into your templates,
because lots of code doesn't belong in the view.
If you take that huge +link_to_remote+ call
and move it to a +update_sidebar_link+ helper,
it'll make your view both easier to read and more semantic.
If you absolutely must put lots of code in your template,
Haml offers a somewhat awkward multiline-continuation tool.
Put a <tt>|</tt> (pipe character) at the end of each line you want to be merged into one
(including the last line!).
For example:
%p= @this.is(way.too.much). |
code("and I should"). |
really_move.it.into( |
:a => @helper) |
=== I have Haml installed. Why is Rails (only looking for <tt>.html.erb</tt> files | rendering Haml files as plain text | rendering Haml files as blank pages)?
There are several reasons these things might be happening.
First of all, make sure vendor/plugins/haml really exists
and has an init.rb file in there.
Then try restarting Mongrel or WEBrick or whatever you might be using.
Finally, if none of these work,
chances are you've got some localization plugin like Globalize installed.
Such plugins often don't play nicely with Haml.
Luckily, there's usually an easy fix.
For Globalize, just edit globalize/lib/globalize/rails/action_view.rb
and change
@@re_extension = /\.(rjs|rhtml|rxml)$/
to
@@re_extension = /\.(rjs|rhtml|rxml|erb|builder|haml)$/
For other plugins, a little searching will probably turn up a way to fix them as well.
== Sass
=== Can I use a variable from my controller in my Sass file?
No. Sass files aren't views.
They're compiled once into static CSS files,
then left along until they're changed and need to be compiled again.
Not only don't you want to be running a full request cycle
every time someone requests a stylesheet,
but it's not a great idea to put much logic in there anyway
due to how browsers handle them.
If you really need some sort of dynamic CSS,
the best thing to do is put only the snippet you need to dynamically set
in the +head+ of your HTML document.
== You still haven't answered my question!
Sorry! Try looking at the Haml or Sass references,
in the doucmentation for the haml and Sass modules, respectively.
If you can't find an answer there,
feel free to ask in #haml on irc.freenode.net
or send an email to the {mailing list}[http://groups.google.com/group/haml?hl=en].
%style
- width = 5 + 17
:sass
p
:border
:style dotted
:width #{width}px
:color #ff00ff
h1
:font-weight normal
:test
This
Should
Not
Print
%p
:javascript
function newline(str) {
return "\n" + str;
}
:plain
This
Is
Plain
Text
%strong right?
\#{not interpolated}
\\#{1 + 2}
\\\#{also not}
\\
- last = "noitalo"
%p
%pre
:preserve
This pre is pretty deeply
nested.
Does #{"interp" + last.reverse} work?
:preserve
This one is, too.
Nested, that is.
- num = 10
%ul
:erb
<% num.times do |c| %>
<li><%= (c+97).chr %></li>
<% end %>
<% res = 178 %>
.res= res
= "Text!"
- var = "Hello"
:ruby
printf "%s, World!\n", var
print "How are you doing today?\n"
:escaped
<div class="foo">
<p>I think &mdash; or do I?</p>
</div>
module Haml
# The module containing the default filters,
# as well as the base module,
# Haml::Filters::Base.
module Filters
# Returns a hash of defined filters.
def self.defined
@defined ||= {}
end
# The base module for Haml filters.
# User-defined filters should be modules including this module.
#
# A user-defined filter should override either Base#render or Base #compile.
# Base#render is the most common.
# It takes a string, the filter source,
# and returns another string,
# the result of the filter.
# For example:
#
# module Haml::Filters::Sass
# include Haml::Filters::Base
#
# def render(text)
# ::Sass::Engine.new(text).render
# end
# end
#
# For details on overriding #compile, see its documentation.
#
module Base
def self.included(base) # :nodoc:
Filters.defined[base.name.split("::").last.downcase] = base
base.extend(base)
base.instance_variable_set "@lazy_requires", nil
end
# Takes a string, the source text that should be passed to the filter,
# and returns the string resulting from running the filter on <tt>text</tt>.
#
# This should be overridden in most individual filter modules
# to render text with the given filter.
# If compile is overridden, however, render doesn't need to be.
def render(text)
raise Error.new("#{self.inspect}#render not defined!")
end
# Same as render, but takes the Haml options hash as well.
# It's only safe to rely on options made available in Haml::Engine#options_for_buffer.
def render_with_options(text, options)
render(text)
end
def internal_compile(*args) # :nodoc:
resolve_lazy_requires
compile(*args)
end
# compile should be overridden when a filter needs to have access
# to the Haml evaluation context.
# Rather than applying a filter to a string at compile-time,
# compile uses the Haml::Precompiler instance to compile the string to Ruby code
# that will be executed in the context of the active Haml template.
#
# Warning: the Haml::Precompiler interface is neither well-documented
# nor guaranteed to be stable.
# If you want to make use of it,
# you'll probably need to look at the source code
# and should test your filter when upgrading to new Haml versions.
def compile(precompiler, text)
resolve_lazy_requires
filter = self
precompiler.instance_eval do
if contains_interpolation?(text)
return if options[:suppress_eval]
push_script(<<RUBY, false)
find_and_preserve(#{filter.inspect}.render_with_options(#{unescape_interpolation(text)}, _hamlout.options))
RUBY
return
end
rendered = Haml::Helpers::find_and_preserve(filter.render_with_options(text, precompiler.options), precompiler.options[:preserve])
if !options[:ugly]
push_text(rendered.rstrip.gsub("\n", "\n#{' ' * @output_tabs}"))
else
push_text(rendered.rstrip)
end
end
end
# This becomes a class method of modules that include Base.
# It allows the module to specify one or more Ruby files
# that Haml should try to require when compiling the filter.
#
# The first file specified is tried first,
# then the second, etc.
# If none are found, the compilation throws an exception.
#
# For example:
#
# module Haml::Filters::Markdown
# lazy_require 'rdiscount', 'peg_markdown', 'maruku', 'bluecloth'
#
# ...
# end
#
def lazy_require(*reqs)
@lazy_requires = reqs
end
private
def resolve_lazy_requires
return unless @lazy_requires
@lazy_requires[0...-1].each do |req|
begin
@required = req
require @required
return
rescue LoadError; end # RCov doesn't see this, but it is run
end
begin
@required = @lazy_requires[-1]
require @required
rescue LoadError => e
classname = self.name.match(/\w+$/)[0]
if @lazy_requires.size == 1
raise Error.new("Can't run #{classname} filter; required file '#{@lazy_requires.first}' not found")
else
raise Error.new("Can't run #{classname} filter; required #{@lazy_requires.map { |r| "'#{r}'" }.join(' or ')}, but none were found")
end
end
end
end
end
end
# :stopdoc:
begin
require 'rubygems'
rescue LoadError; end
module Haml
module Filters
module Plain
include Base
def render(text); text; end
end
module Javascript
include Base
def render_with_options(text, options)
<<END
<script type=#{options[:attr_wrapper]}text/javascript#{options[:attr_wrapper]}>
//<![CDATA[
#{text.rstrip.gsub("\n", "\n ")}
//]]>
</script>
END
end
end
module Cdata
include Base
def render(text)
"<![CDATA[#{("\n" + text).rstrip.gsub("\n", "\n ")}\n]]>"
end
end
module Escaped
include Base
def render(text)
Haml::Helpers.html_escape text
end
end
module Ruby
include Base
lazy_require 'stringio'
def compile(precompiler, text)
return if precompiler.options[:suppress_eval]
precompiler.instance_eval do
push_silent <<-FIRST.gsub("\n", ';') + text + <<-LAST.gsub("\n", ';')
_haml_old_stdout = $stdout
$stdout = StringIO.new(_hamlout.buffer, 'a')
FIRST
_haml_old_stdout, $stdout = $stdout, _haml_old_stdout
_haml_old_stdout.close
LAST
end
end
end
module Preserve
include Base
def render(text)
Haml::Helpers.preserve text
end
end
module Sass
include Base
lazy_require 'sass/plugin'
def render(text)
::Sass::Engine.new(text, ::Sass::Plugin.engine_options).render
end
end
module ERB
include Base
lazy_require 'erb'
def compile(precompiler, text)
return if precompiler.options[:suppress_eval]
src = ::ERB.new(text).src.sub(/^#coding:.*?\n/, '').
sub(/^_erbout = '';/, "").gsub("\n", ';')
precompiler.send(:push_silent, src)
end
end
module Textile
include Base
lazy_require 'redcloth'
def render(text)
::RedCloth.new(text).to_html(:textile)
end
end
RedCloth = Textile
Filters.defined['redcloth'] = RedCloth
# Uses BlueCloth or RedCloth to provide only Markdown (not Textile) parsing
module Markdown
include Base
lazy_require 'rdiscount', 'peg_markdown', 'maruku', 'bluecloth'
def render(text)
engine = case @required
when 'rdiscount'
::RDiscount
when 'peg_markdown'
::PEGMarkdown
when 'maruku'
::Maruku
when 'bluecloth'
::BlueCloth
end
engine.new(text).to_html
end
end
module Maruku
include Base
lazy_require 'maruku'
def render(text)
::Maruku.new(text).to_html
end
end
end
end
# :startdoc:
<style>
p { border-style: dotted; border-width: 22px; border-color: #ff00ff; }
h1 { font-weight: normal; }
</style>
TESTING HAHAHAHA!
<p>
<script type='text/javascript'>
//<![CDATA[
function newline(str) {
return "\n" + str;
}
//]]>
</script>
</p>
This
Is
Plain
Text
%strong right?
#{not interpolated}
\3
\#{also not}
\\
<p>
<pre>This pre is pretty deeply&#x000A; nested.&#x000A; Does interpolation work?
This one is, too.&#x000A;Nested, that is.&#x000A;</pre>
</p>
<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>
</ul>
<div class='res'>178</div>
Text!
Hello, World!
How are you doing today?
&lt;div class=&quot;foo&quot;&gt;
&lt;p&gt;I think &amp;mdash; or do I?&lt;/p&gt;
&lt;/div&gt;
#!/usr/bin/env ruby
# The command line Haml parser.
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
require 'haml'
require 'haml/exec'
opts = Haml::Exec::Haml.new(ARGV)
opts.parse!
;;; haml-mode.el -- Major mode for editing Haml files
;;; Written by Nathan Weizenbaum
;;; Because Haml's indentation schema is similar
;;; to that of YAML and Python, many indentation-related
;;; functions are similar to those in yaml-mode and python-mode.
;;; To install, save this somewhere and add the following to your .emacs file:
;;;
;;; (add-to-list 'load-path "/path/to/haml-mode.el")
;;; (require 'haml-mode nil 't)
;;; (add-to-list 'auto-mode-alist '("\\.haml$" . haml-mode))
;;;
;;; Code:
(eval-when-compile (require 'cl))
;; User definable variables
(defgroup haml nil
"Support for the Haml template language."
:group 'languages
:prefix "haml-")
(defcustom haml-mode-hook nil
"Hook run when entering Haml mode."
:type 'hook
:group 'haml)
(defcustom haml-indent-offset 2
"Amount of offset per level of indentation."
:type 'integer
:group 'haml)
(defcustom haml-backspace-backdents-nesting t
"Non-nil to have `haml-electric-backspace' re-indent all code
nested beneath the backspaced line be re-indented along with the
line itself."
:type 'boolean
:group 'haml)
(defface haml-tab-face
'((((class color)) (:background "hotpink"))
(t (:reverse-video t)))
"Face to use for highlighting tabs in Haml files."
:group 'faces
:group 'haml)
(defvar haml-indent-function 'haml-indent-p
"This function should look at the current line and return true
if the next line could be nested within this line.")
(defvar haml-block-openers
`("^ *\\([%\\.#][^ \t]*\\)\\(\\[.*\\]\\)?\\({.*}\\)?\\(\\[.*\\]\\)?[ \t]*$"
"^ *[-=].*do[ \t]*\\(|.*|[ \t]*\\)?$"
,(concat "^ *-[ \t]*\\("
(regexp-opt '("else" "elsif" "rescue" "ensure" "when"))
"\\)")
"^ */\\(\\[.*\\]\\)?[ \t]*$"
"^ *-#"
"^ *:")
"A list of regexps that match lines of Haml that could have
text nested beneath them.")
;; Font lock
(defconst haml-font-lock-keywords
'(("^ *\\(\t\\)" 1 'haml-tab-face)
("^!!!.*" 0 font-lock-constant-face)
("\\('[^']*'\\)" 1 font-lock-string-face append)
("\\(\"[^\"]*\"\\)" 1 font-lock-string-face append)
("&?:\\w+" 0 font-lock-constant-face append)
("@[a-z0-9_]+" 0 font-lock-variable-name-face append)
("| *$" 0 font-lock-string-face)
("^[ \t]*\\(/.*\\)$" 1 font-lock-comment-face append)
("^ *\\(#[a-z0-9_]+\/?\\)" 1 font-lock-keyword-face)
("^ *\\(\\.[a-z0-9_]+\/?\\)" 1 font-lock-type-face)
("^ *\\(%[a-z0-9_]+\/?\\)" 1 font-lock-function-name-face)
("^ *\\(#[a-z0-9_]+\/?\\)" (1 font-lock-keyword-face)
("\\.[a-z0-9_]+" nil nil (0 font-lock-type-face)))
("^ *\\(\\.[a-z0-9_]+\/?\\)" (1 font-lock-type-face)
("\\.[a-z0-9_]+" nil nil (0 font-lock-type-face)))
("^ *\\(\\.[a-z0-9_]+\/?\\)" (1 font-lock-type-face)
("\\#[a-z0-9_]+" nil nil (0 font-lock-keyword-face)))
("^ *\\(%[a-z0-9_]+\/?\\)" (1 font-lock-function-name-face)
("\\.[a-z0-9_]+" nil nil (0 font-lock-type-face)))
("^ *\\(%[a-z0-9_]+\/?\\)" (1 font-lock-function-name-face)
("\\#[a-z0-9_]+" nil nil (0 font-lock-keyword-face)))
("^ *\\([~=-] .*\\)" 1 font-lock-preprocessor-face prepend)
("^ *[\\.#%a-z0-9_]+\\([~=-] .*\\)" 1 font-lock-preprocessor-face prepend)
("^ *[\\.#%a-z0-9_]+\\({[^}]+}\\)" 1 font-lock-preprocessor-face prepend)
("^ *[\\.#%a-z0-9_]+\\(\\[[^]]+\\]\\)" 1 font-lock-preprocessor-face prepend)))
;; Mode setup
(defvar haml-mode-syntax-table
(let ((table (make-syntax-table)))
(modify-syntax-entry ?: "." table)
(modify-syntax-entry ?_ "w" table)
table)
"Syntax table in use in haml-mode buffers.")
(defvar haml-mode-map
(let ((map (make-sparse-keymap)))
(define-key map [backspace] 'haml-electric-backspace)
(define-key map "\C-?" 'haml-electric-backspace)
(define-key map "\C-\M-f" 'haml-forward-sexp)
(define-key map "\C-\M-b" 'haml-backward-sexp)
(define-key map "\C-\M-u" 'haml-up-list)
(define-key map "\C-\M-d" 'haml-down-list)
(define-key map "\C-C\C-k" 'haml-kill-line-and-indent)
map))
;;;###autoload
(define-derived-mode haml-mode fundamental-mode "Haml"
"Major mode for editing Haml files.
\\{haml-mode-map}"
(set-syntax-table haml-mode-syntax-table)
(set (make-local-variable 'indent-line-function) 'haml-indent-line)
(set (make-local-variable 'indent-region-function) 'haml-indent-region)
(set (make-local-variable 'forward-sexp-function) 'haml-forward-sexp)
(setq indent-tabs-mode nil)
(setq font-lock-defaults '((haml-font-lock-keywords) nil t)))
;; Navigation
(defun haml-forward-through-whitespace (&optional backward)
"Move the point forward at least one line, until it reaches
either the end of the buffer or a line with no whitespace.
If `backward' is non-nil, move the point backward instead."
(let ((arg (if backward -1 1))
(endp (if backward 'bobp 'eobp)))
(loop do (forward-line arg)
while (and (not (funcall endp))
(looking-at "^[ \t]*$")))))
(defun haml-at-indent-p ()
"Returns whether or not the point is at the first
non-whitespace character in a line or whitespace preceding that
character."
(let ((opoint (point)))
(save-excursion
(back-to-indentation)
(>= (point) opoint))))
(defun haml-forward-sexp (&optional arg)
"Move forward across one nested expression.
With `arg', do it that many times. Negative arg -N means move
backward across N balanced expressions.
A sexp in Haml is defined as a line of Haml code as well as any
lines nested beneath it."
(interactive "p")
(or arg (setq arg 1))
(if (and (< arg 0) (not (haml-at-indent-p)))
(back-to-indentation)
(while (/= arg 0)
(let ((indent (current-indentation)))
(loop do (haml-forward-through-whitespace (< arg 0))
while (and (not (eobp))
(not (bobp))
(> (current-indentation) indent)))
(back-to-indentation)
(setq arg (+ arg (if (> arg 0) -1 1)))))))
(defun haml-backward-sexp (&optional arg)
"Move backward across one nested expression.
With ARG, do it that many times. Negative arg -N means move
forward across N balanced expressions.
A sexp in Haml is defined as a line of Haml code as well as any
lines nested beneath it."
(interactive "p")
(haml-forward-sexp (if arg (- arg) -1)))
(defun haml-up-list (&optional arg)
"Move out of one level of nesting.
With ARG, do this that many times."
(interactive "p")
(or arg (setq arg 1))
(while (> arg 0)
(let ((indent (current-indentation)))
(loop do (haml-forward-through-whitespace t)
while (and (not (bobp))
(>= (current-indentation) indent)))
(setq arg (- arg 1))))
(back-to-indentation))
(defun haml-down-list (&optional arg)
"Move down one level of nesting.
With ARG, do this that many times."
(interactive "p")
(or arg (setq arg 1))
(while (> arg 0)
(let ((indent (current-indentation)))
(haml-forward-through-whitespace)
(when (<= (current-indentation) indent)
(haml-forward-through-whitespace t)
(back-to-indentation)
(error "Nothing is nested beneath this line"))
(setq arg (- arg 1))))
(back-to-indentation))
(defun haml-mark-sexp-but-not-next-line ()
"Marks the next Haml sexp, but puts the mark at the end of the
last line of the sexp rather than the first non-whitespace
character of the next line."
(mark-sexp)
(set-mark
(save-excursion
(goto-char (mark))
(forward-line -1)
(end-of-line)
(point))))
;; Indentation and electric keys
(defun haml-indent-p ()
"Returns true if the current line can have lines nested beneath it."
(loop for opener in haml-block-openers
if (looking-at opener) return t
finally return nil))
(defun haml-compute-indentation ()
"Calculate the maximum sensible indentation for the current line."
(save-excursion
(beginning-of-line)
(if (bobp) 0
(haml-forward-through-whitespace t)
(+ (current-indentation)
(if (funcall haml-indent-function) haml-indent-offset
0)))))
(defun haml-indent-region (start end)
"Indent each nonblank line in the region.
This is done by indenting the first line based on
`haml-compute-indentation' and preserving the relative
indentation of the rest of the region.
If this command is used multiple times in a row, it will cycle
between possible indentations."
(save-excursion
(goto-char end)
(setq end (point-marker))
(goto-char start)
(let (this-line-column current-column
(next-line-column
(if (and (equal last-command this-command) (/= (current-indentation) 0))
(* (/ (- (current-indentation) 1) haml-indent-offset) haml-indent-offset)
(haml-compute-indentation))))
(while (< (point) end)
(setq this-line-column next-line-column
current-column (current-indentation))
;; Delete whitespace chars at beginning of line
(delete-horizontal-space)
(unless (eolp)
(setq next-line-column (save-excursion
(loop do (forward-line 1)
while (and (not (eobp)) (looking-at "^[ \t]*$")))
(+ this-line-column
(- (current-indentation) current-column))))
;; Don't indent an empty line
(unless (eolp) (indent-to this-line-column)))
(forward-line 1)))
(move-marker end nil)))
(defun haml-indent-line ()
"Indent the current line.
The first time this command is used, the line will be indented to the
maximum sensible indentation. Each immediately subsequent usage will
back-dent the line by `haml-indent-offset' spaces. On reaching column
0, it will cycle back to the maximum sensible indentation."
(interactive "*")
(let ((ci (current-indentation))
(cc (current-column))
(need (haml-compute-indentation)))
(save-excursion
(beginning-of-line)
(delete-horizontal-space)
(if (and (equal last-command this-command) (/= ci 0))
(indent-to (* (/ (- ci 1) haml-indent-offset) haml-indent-offset))
(indent-to need)))
(if (< (current-column) (current-indentation))
(forward-to-indentation 0))))
(defun haml-reindent-region-by (n)
"Add N spaces to the beginning of each line in the region.
If N is negative, will remove the spaces instead. Assumes all
lines in the region have indentation >= that of the first line."
(let ((ci (current-indentation)))
(save-excursion
(replace-regexp (concat "^" (make-string ci ? ))
(make-string (max 0 (+ ci n)) ? )
nil (point) (mark)))))
(defun haml-electric-backspace (arg)
"Delete characters or back-dent the current line.
If invoked following only whitespace on a line, will back-dent
the line and all nested lines to the immediately previous
multiple of `haml-indent-offset' spaces.
Set `haml-backspace-backdents-nesting' to nil to just back-dent
the current line."
(interactive "*p")
(if (or (/= (current-indentation) (current-column))
(bolp)
(looking-at "^[ \t]+$"))
(backward-delete-char arg)
(let ((ci (current-column)))
(beginning-of-line)
(if haml-backspace-backdents-nesting
(haml-mark-sexp-but-not-next-line)
(set-mark (save-excursion (end-of-line) (point))))
(haml-reindent-region-by (* (- arg) haml-indent-offset))
(back-to-indentation)
(pop-mark))))
(defun haml-kill-line-and-indent ()
"Kill the current line, and re-indent all lines nested beneath it."
(interactive)
(beginning-of-line)
(haml-mark-sexp-but-not-next-line)
(kill-line 1)
(haml-reindent-region-by (* -1 haml-indent-offset)))
;; Setup/Activation
(provide 'haml-mode)
dir = File.dirname(__FILE__)
$LOAD_PATH.unshift dir unless $LOAD_PATH.include?(dir)
require 'haml/version'
# = 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 three ways:
# as a plugin for Ruby on Rails,
# as a standalone Ruby module,
# and as a command-line tool.
# 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
#
# Once it's installed, all view files with the ".html.haml" extension
# will be compiled using Haml.
#
# To run Haml from the command line, just use
#
# haml input.haml output.html
#
# Use <tt>haml --help</tt> for full documentation.
#
# 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>
#
# ===== Attribute Methods
#
# A Ruby method call that returns a hash
# can be substituted for the hash contents.
# For example, Haml::Helpers defines the following method:
#
# def html_attrs(lang = 'en-US')
# {:xmlns => "http://www.w3.org/1999/xhtml", 'xml:lang' => lang, :lang => lang}
# end
#
# This can then be used in Haml, like so:
#
# %html{html_attrs('fr-fr')}
#
# This is compiled to:
#
# <html lang='fr-fr' xml:lang='fr-fr' xmlns='http://www.w3.org/1999/xhtml'>
# </html>
#
# You can use as many such attribute methods as you want
# by separating them with commas,
# like a Ruby argument list.
# All the hashes will me merged together, from left to right.
# For example, if you defined
#
# def hash1
# {:bread => 'white', :filling => 'peanut butter and jelly'}
# end
#
# def hash2
# {:bread => 'whole wheat'}
# end
#
# then
#
# %sandwich{hash1, hash2, :delicious => true}/
#
# would compile to:
#
# <sandwich bread='whole wheat' delicious='true' filling='peanut butter and jelly' />
#
# Note that the Haml attributes list has the same syntax as a Ruby method call.
# This means that any attribute methods must come before the hash literal.
#
# ===== Boolean Attributes
#
# Some attributes, such as "checked" for <tt>input</tt> tags or "selected" for <tt>option</tt> tags,
# are "boolean" in the sense that their values don't matter -
# it only matters whether or not they're present.
# In HTML (but not XHTML), these attributes can be written as
#
# <input selected>
#
# To do this in Haml, just assign a Ruby true value to the attribute:
#
# %input{:selected => true}
#
# In XHTML, the only valid value for these attributes is the name of the attribute.
# Thus this will render in XHTML as
#
# <input selected='selected'>
#
# To set these attributes to false, simply assign them to a Ruby false value.
# In both XHTML and HTML
#
# %input{:selected => false}
#
# will just render as
#
# <input>
#
# ==== []
#
# 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.
# Additionally, the second argument (if present) will be used as a prefix for
# both the id and class attributes.
# For example:
#
# # file: app/controllers/users_controller.rb
#
# def show
# @user = CrazyUser.find(15)
# end
#
# -# file: app/views/users/show.haml
#
# %div[@user, :greeting]
# %bar[290]/
# Hello!
#
# is compiled to:
#
# <div class='greeting_crazy_user' id='greeting_crazy_user_15'>
# <bar class='fixnum' id='fixnum_581' />
# Hello!
# </div>
#
# ==== /
#
# 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>
#
# ==== > and <
#
# <tt>></tt> and <tt><</tt> give you more control over the whitespace near a tag.
# <tt>></tt> will remove all whitespace surrounding a tag,
# while <tt><</tt> will remove all whitespace immediately within a tag.
# You can think of them as alligators eating the whitespace:
# <tt>></tt> faces out of the tag and eats the whitespace on the outside,
# and <tt><</tt> faces into the tag and eats the whitespace on the inside.
# They're placed at the end of a tag definition,
# after class, id, and attribute declarations
# but before <tt>/</tt> or <tt>=</tt>.
# For example:
#
# %blockquote<
# %div
# Foo!
#
# is compiled to:
#
# <blockquote><div>
# Foo!
# </div></blockquote>
#
# And:
#
# %img
# %img>
# %img
#
# is compiled to:
#
# <img /><img /><img />
#
# And:
#
# %p<= "Foo\nBar"
#
# is compiled to:
#
# <p>Foo
# Bar</p>
#
# And finally:
#
# %img
# %pre><
# foo
# bar
# %img
#
# is compiled to:
#
# <img /><pre>foo
# bar</pre><img />
#
# ==== =
#
# <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>
#
# ==== ~
#
# ~ works just like =, except that it runs Haml::Helpers#find_and_preserve on its input.
# For example,
#
# ~ "Foo\n<pre>Bar\nBaz</pre>"
#
# is the same as:
#
# = find_and_preserve("Foo\n<pre>Bar\nBaz</pre>")
#
# and is compiled to:
#
# Foo
# <pre>Bar&#x000A;Baz</pre>
#
# See also Whitespace Preservation, below.
#
# === 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">
#
# while
#
# !!! Basic
#
# is compiled to:
#
# <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">
#
# and
#
# !!! Mobile
#
# is compiled to:
#
# <!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.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>
# <p>This is short</p>
# </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>
#
# Filters can have Ruby code interpolated, like with ==.
# For example,
#
# - flavor = "raspberry"
# #content
# :textile
# I *really* prefer _#{h flavor}_ jam.
#
# is compiled to
#
# <div id='content'>
# <p>I <strong>really</strong> prefer <em>raspberry</em> jam.</p>
# </div>
#
# 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.
#
# [javascript] Surrounds the filtered text with <script> and CDATA tags.
# Useful for including inline Javascript.
#
# [escaped] Works the same as plain, but HTML-escapes the text
# before placing it in the document.
#
# [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.
# The Ruby code is evaluated in the same context as the Haml template.
#
# [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.
# See also Whitespace Preservation, below.
#
# [erb] Parses the filtered text with ERB, like an RHTML template.
# Not available if the <tt>suppress_eval</tt> option is set to true.
# Embedded Ruby code is evaluated in the same context as the Haml template.
#
# [sass] Parses the filtered text with Sass to produce CSS output.
#
# [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 RDiscount, RPeg-Markdown, Maruku, or BlueCloth are installed.
#
# [maruku] Parses the filtered text with Maruku, which has some non-standard extensions to Markdown.
#
# You can also define your own filters (see Haml::Filters).
#
# === 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>
#
# If the <tt>:escape_html</tt> option is set, <tt>=</tt> will sanitize any
# HTML-sensitive characters generated by the script. For example:
#
# = '<script>alert("I\'m evil!");</script>'
#
# would be compiled to
#
# &lt;script&gt;alert(&quot;I'm evil!&quot;);&lt;/script&gt;
#
# ==== -
#
# 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>
#
# ==== ==
#
# Two equals characters interpolates Ruby code into plain text,
# similarly to Ruby string interpolation.
# For example,
#
# %p== This is #{h quality} cake!
#
# is the same as
#
# %p= "This is #{h quality} cake!"
#
# and might compile to
#
# <p>This is scrumptious cake!</p>
#
# Backslashes can be used to escape "#{" strings,
# but they don't act as escapes anywhere else in the string.
# For example:
#
# %p
# == \\ Look at \\#{h word} lack of backslash: \#{foo}
#
# might compile to
#
# <p>
# \\ Look at \yon lack of backslash: #{foo}
# </p>
#
# ==== &=
#
# An ampersand followed by one or two equals characters
# evaluates Ruby code just like the equals without the ampersand,
# but sanitizes any HTML-sensitive characters in the result of the code.
# For example:
#
# &= "I like cheese & crackers"
#
# compiles to
#
# I like cheese &amp; crackers
#
# If the <tt>:escape_html</tt> option is set,
# &= behaves identically to =.
#
# ==== !=
#
# An exclamation mark followed by one or two equals characters
# evaluates Ruby code just like the equals would,
# but never sanitizes the HTML.
#
# By default, the single equals doesn't sanitize HTML either.
# However, if the <tt>:escape_html</tt> option is set, = will sanitize the HTML, but != still won't.
# For example, if <tt>:escape_html</tt> is set:
#
# = "I feel <strong>!"
# != "I feel <strong>!"
#
# compiles to
#
# I feel &lt;strong&gt;!
# I feel <strong>!
#
# ===== 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>
#
# You can also nest text beneath a silent comment.
# None of this text will be rendered.
# For example:
#
# %p foo
# -#
# This won't be displayed
# Nor will this
# %p bar
#
# is compiled to:
#
# <p>foo</p>
# <p>bar</p>
#
# == Other Useful Things
#
# === Whitespace Preservation
#
# Sometimes you don't want Haml to indent all your text.
# For example, tags like +pre+ and +textarea+ are whitespace-sensitive;
# indenting the text makes them render wrong.
#
# Haml deals with this by "preserving" newlines before they're put into the document --
# converting them to the XHTML whitespace escape code, <tt>&#x000A;</tt>.
# Then Haml won't try to re-format the indentation.
#
# Literal +textarea+ and +pre+ tags automatically preserve their content.
# Dynamically can't be caught automatically,
# and so should be passed through Haml::Helpers#find_and_preserve or the <tt>~</tt> command,
# which has the same effect (see above).
#
# Blocks of literal text can be preserved using the :preserve filter (see above).
#
# === 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 <tt>Haml::Template.options</tt> hash
# in <tt>environment.rb</tt> in Rails...
#
# Haml::Template.options[:format] = :html5
#
# ...or by setting the <tt>Merb::Plugin.config[:haml]</tt> hash in <tt>init.rb</tt> in Merb...
#
# Merb::Plugin.config[:haml][:format] = :html5
#
# ...or by passing an options hash to Haml::Engine.new.
# Available options are:
#
# [<tt>:format</tt>] Determines the output format. The default is :xhtml.
# Other options are :html4 and :html5, which are
# identical to :xhtml except there are no self-closing tags,
# XML prolog is ignored and correct DOCTYPEs are generated.
#
# [<tt>:escape_html</tt>] Sets whether or not to escape HTML-sensitive characters in script.
# If this is true, = behaves like &=;
# otherwise, it behaves like !=.
# Note that if this is set, != should be used for yielding to subtemplates
# and rendering partials.
# Defaults to false.
#
# [<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>:line</tt>] The line offset of the Haml template being parsed.
# This is useful for inline templates,
# similar to the last argument to Kernel#eval.
#
# [<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', 'br', 'hr', 'input', 'area', 'param', 'col', 'base']</tt>.
#
# [<tt>:preserve</tt>] A list of tag names that should automatically have their newlines preserved
# using the Haml::Helpers#preserve helper.
# This means that any content given on the same line as the tag will be preserved.
# For example:
#
# %textarea= "Foo\nBar"
#
# compiles to:
#
# <textarea>Foo&&#x000A;Bar</textarea>
#
# Defaults to <tt>['textarea', 'pre']</tt>.
#
# See also Whitespace Preservation, above.
#
module Haml
extend Haml::Version
# A string representing the version of Haml.
# A more fine-grained representation is available from Haml.version.
VERSION = version[:string] unless defined?(Haml::VERSION)
# This method is called by init.rb,
# which is run by Rails on startup.
# We use it rather than putting stuff straight into init.rb
# so we can change the initialization behavior
# without modifying the file itself.
def self.init_rails(binding)
# No &method here for Rails 2.1 compatibility
%w[haml/template sass sass/plugin].each {|f| require f}
end
end
require 'haml/util'
require 'haml/engine'
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../test_helper'
require 'haml/template'
class ActionView::Base
def nested_tag
content_tag(:span) {content_tag(:div) {"something"}}
end
end
class HelperTest < Test::Unit::TestCase
Post = Struct.new('Post', :body)
def setup
@base = ActionView::Base.new
@base.controller = ActionController::Base.new
@base.instance_variable_set('@post', Post.new("Foo bar\nbaz"))
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("FooBar", Haml::Helpers.flatten("FooBar"))
assert_equal("FooBar", Haml::Helpers.flatten("Foo\rBar"))
assert_equal("Foo&#x000A;Bar", Haml::Helpers.flatten("Foo\nBar"))
assert_equal("Hello&#x000A;World!&#x000A;YOU ARE FLAT?&#x000A;OMGZ!",
Haml::Helpers.flatten("Hello\nWorld!\nYOU ARE \rFLAT?\n\rOMGZ!"))
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("foo\n bar\nbaz\n", render("foo\n- tab_up\nbar\n- tab_down\nbaz"))
assert_equal(" <p>tabbed</p>\n", render("- buffer.tabulation=5\n%p tabbed"))
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
# This is usually provided by ActionController::Base.
def @base.protect_against_forgery?; false; end
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_text_area
assert_equal(%(<textarea id="body" name="body">Foo&#x000A;Bar&#x000A; Baz&#x000A; Boom</textarea>\n),
render('= text_area_tag "body", "Foo\nBar\n Baz\n Boom"', :action_view))
assert_equal(%(<textarea cols="40" id="post_body" name="post[body]" rows="20">Foo bar&#x000A;baz</textarea>\n),
render('= text_area :post, :body', :action_view))
assert_equal(%(<pre>Foo bar&#x000A; baz</pre>\n),
render('= content_tag "pre", "Foo bar\n baz"', :action_view))
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_content_tag_block
assert_equal(<<HTML.strip, render(<<HAML, :action_view))
<div><p>bar</p>
<strong>bar</strong>
</div>
HTML
- content_tag :div do
%p bar
%strong bar
HAML
end
def test_haml_tag_attribute_html_escaping
assert_equal("<p id='foo&amp;bar'>baz</p>\n", render("%p{:id => 'foo&bar'} baz", :escape_html => true))
end
def test_haml_tag_autoclosed_tags_are_closed
assert_equal("<br class='foo' />\n", render("- haml_tag :br, :class => 'foo'"))
end
def test_haml_tag_non_autoclosed_tags_arent_closed
assert_equal("<p></p>\n", render("- haml_tag :p"))
end
def test_haml_tag_renders_text_on_a_single_line
assert_equal("<p>#{'a' * 100}</p>\n", render("- haml_tag :p, 'a' * 100"))
end
def test_haml_tag_raises_error_for_multiple_content
assert_raise(Haml::Error) { render("- haml_tag :p, 'foo' do\n bar") }
end
def test_haml_tag_flags
assert_equal("<p />\n", render("- haml_tag :p, :/"))
assert_equal("<p>kumquat</p>\n", render("- haml_tag :p, :< do\n kumquat"))
assert_raise(Haml::Error) { render("- haml_tag :p, 'foo', :/") }
assert_raise(Haml::Error) { render("- haml_tag :p, :/ do\n foo") }
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|
haml_concat 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
def test_find_and_preserve_with_block
assert_equal("<pre>Foo&#x000A;Bar</pre>\nFoo\nBar\n",
render("= find_and_preserve do\n %pre\n Foo\n Bar\n Foo\n Bar"))
end
def test_preserve_with_block
assert_equal("<pre>Foo&#x000A;Bar</pre>&#x000A;Foo&#x000A;Bar\n",
render("= preserve do\n %pre\n Foo\n Bar\n Foo\n Bar"))
end
def test_init_haml_helpers
context = Object.new
class << context
include Haml::Helpers
end
context.init_haml_helpers
result = context.capture_haml do
context.haml_tag :p, :attr => "val" do
context.haml_concat "Blah"
end
end
assert_equal("<p attr='val'>\n Blah\n</p>\n", result)
end
def test_non_haml
assert_equal("false\n", render("= non_haml { is_haml? }"))
end
def test_content_tag_nested
assert_equal "<span><div>something</div></span>", render("= nested_tag", :action_view).strip
end
class ActsLikeTag
# We want to be able to have people include monkeypatched ActionView helpers
# without redefining is_haml?.
# This is accomplished via Object#is_haml?, and this is a test for it.
include ActionView::Helpers::TagHelper
def to_s
content_tag :p, 'some tag content'
end
end
def test_random_class_includes_tag_helper
assert_equal "<p>some tag content</p>", ActsLikeTag.new.to_s
end
end
= 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
- haml_buffer.tabulation = 10
%p boom
- concat "foo\n"
- haml_buffer.tabulation = 0
-#
-# ActionPack pre-2.0 has weird url_for issues here.
- if ActionPack::VERSION::MAJOR < 2
:plain
<p>
<form action="" method="post">
</p>
<div>
<form action="" method="post">
<div><input name="commit" type="submit" value="save" /></div>
<p>
@foo =
value one
</p>
Toplevel? false
<p>
@foo =
value three
</p>
</form>
<form action="" 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>
</div>
- else
%p
= form_tag ''
%div
- form_tag '' do
%div= submit_tag 'save'
- @foo = 'value one'
= test_partial 'partial'
- form_for :article, @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
- haml_concat "foo"
%div
- haml_concat "bar"
- haml_concat "boom"
baz
- haml_concat "boom, again"
- haml_tag :table do
- haml_tag :tr do
- haml_tag :td, {:class => 'cell'} do
- haml_tag :strong, "strong!"
- haml_concat "data"
- haml_tag :td do
- haml_concat "more_data"
- haml_tag :hr
- haml_tag :div, ''
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
# Note: this does *not* need to be called
# when using Haml helpers normally
# in Rails.
#
# Initializes the current object
# as though it were in the same context
# as a normal ActionView rendering
# using Haml.
# This is useful if you want to use the helpers in a context
# other than the normal setup with ActionView.
# For example:
#
# context = Object.new
# class << context
# include Haml::Helpers
# end
# context.init_haml_helpers
# context.haml_tag :p, "Stuff"
#
def init_haml_helpers
@haml_buffer = Haml::Buffer.new(@haml_buffer, Haml::Engine.new('').send(:options_for_buffer))
nil
end
# call-seq:
# non_haml { ... }
#
# Runs a block of code in a non-Haml context
# (i.e. #is_haml? will return false).
#
# This is mainly useful for rendering sub-templates such as partials in a non-Haml language,
# particularly where helpers may behave differently when run from Haml.
#
# Note that this is automatically applied to Rails partials.
def non_haml
was_active = @haml_buffer.active?
@haml_buffer.active = false
yield
ensure
@haml_buffer.active = was_active
end
# call-seq:
# find_and_preserve(input, tags = haml_buffer.options[:preserve])
# find_and_preserve {...}
#
# Uses preserve to convert any newlines inside whitespace-sensitive tags
# into the HTML entities for endlines.
# @tags@ is an array of tags to preserve.
# It defaults to the value of the <tt>:preserve</tt> option.
def find_and_preserve(input = '', tags = haml_buffer.options[:preserve], &block)
return find_and_preserve(capture_haml(&block)) if block
input = input.to_s
input.gsub(/<(#{tags.map(&Regexp.method(:escape)).join('|')})([^>]*)>(.*?)(<\/\1>)/im) do
"<#{$1}#{$2}>#{preserve($3)}</#{$1}>"
end
end
# call-seq:
# preserve(input)
# preserve {...}
#
# 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 = '', &block)
return preserve(capture_haml(&block)) if block
input.chomp("\n").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)
haml_buffer.tabulation += i
end
# Decrements the number of tabs the buffer automatically adds
# to the lines of the template.
#
# See also tab_up.
def tab_down(i = 1)
haml_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)
buffer = eval('_hamlout', block.binding) rescue haml_buffer
with_haml_buffer(buffer) do
position = haml_buffer.buffer.length
block.call(*args)
captured = haml_buffer.buffer.slice!(position..-1).split(/^/)
min_tabs = nil
captured.each do |line|
tabs = line.index(/[^ ]/) || line.length
min_tabs ||= tabs
min_tabs = min_tabs > tabs ? tabs : min_tabs
end
captured.map do |line|
line[min_tabs..-1]
end.join
end
end
def puts(*args) # :nodoc:
warn <<END
DEPRECATION WARNING:
The Haml #puts helper is deprecated and will be removed in version 2.4.
Use the #haml_concat helper instead.
END
haml_concat *args
end
# Outputs text directly to the Haml buffer, with the proper tabulation
def haml_concat(text = "")
haml_buffer.buffer << haml_indent << text.to_s << "\n"
nil
end
# Returns the string that should be used to indent the current line
def haml_indent
' ' * haml_buffer.tabulation
end
#
# call-seq:
# haml_tag(name, *flags, attributes = {}) {...}
# haml_tag(name, text, *flags, 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 haml_concat,
# the text will be properly indented.
#
# <tt>flags</tt> is a list of symbol flags
# like those that can be put at the end of a Haml tag
# (<tt>:/</tt>, <tt>:<</tt>, and <tt>:></tt>).
# Currently, only <tt>:/</tt> and <tt>:<</tt> are supported.
#
# For example,
#
# haml_tag :table do
# haml_tag :tr do
# haml_tag :td, {:class => 'cell'} do
# haml_tag :strong, "strong!"
# haml_concat "data"
# end
# haml_tag :td do
# haml_concat "more_data"
# end
# end
# end
#
# outputs
#
# <table>
# <tr>
# <td class='cell'>
# <strong>
# strong!
# </strong>
# data
# </td>
# <td>
# more_data
# </td>
# </tr>
# </table>
#
def haml_tag(name, *rest, &block)
name = name.to_s
text = rest.shift.to_s unless [Symbol, Hash, NilClass].any? {|t| rest.first.is_a? t}
flags = []
flags << rest.shift while rest.first.is_a? Symbol
attributes = Haml::Precompiler.build_attributes(haml_buffer.html?,
haml_buffer.options[:attr_wrapper],
rest.shift || {})
if text.nil? && block.nil? && (haml_buffer.options[:autoclose].include?(name) || flags.include?(:/))
haml_concat "<#{name}#{attributes} />"
return nil
end
if flags.include?(:/)
raise Error.new("Self-closing tags can't have content.") if text
raise Error.new("Illegal nesting: nesting within a self-closing tag is illegal.") if block
end
tag = "<#{name}#{attributes}>"
if block.nil?
tag << text.to_s << "</#{name}>"
haml_concat tag
return
end
if text
raise Error.new("Illegal nesting: content can't be both given to haml_tag :#{name} and nested within it.")
end
if flags.include?(:<)
tag << capture_haml(&block).strip << "</#{name}>"
haml_concat tag
return
end
haml_concat tag
tab_up
block.call
tab_down
haml_concat "</#{name}>"
nil
end
# Characters that need to be escaped to HTML entities from user input
HTML_ESCAPE = { '&'=>'&amp;', '<'=>'&lt;', '>'=>'&gt;', '"'=>'&quot;', "'"=>'&#039;', }
# Returns a copy of <tt>text</tt> with ampersands, angle brackets and quotes
# escaped into HTML entities.
def html_escape(text)
text.to_s.gsub(/[\"><&]/) { |s| HTML_ESCAPE[s] }
end
# Escapes HTML entities in <tt>text</tt>, but without escaping an ampersand
# that is already part of an escaped entity.
def escape_once(text)
text.to_s.gsub(/[\"><]|&(?!([a-zA-Z]+|(#\d+));)/) { |s| HTML_ESCAPE[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_buffer.nil? && @haml_buffer.active?
end
# Returns whether or not +block+ is defined directly in a Haml template.
def block_is_haml?(block)
eval('_hamlout', block.binding)
true
rescue
false
end
private
# call-seq:
# with_haml_buffer(buffer) {...}
#
# Runs the block with the given buffer as the currently active buffer.
def with_haml_buffer(buffer)
@haml_buffer, old_buffer = buffer, @haml_buffer
old_buffer.active, was_active = false, old_buffer.active? if old_buffer
@haml_buffer.active = true
yield
ensure
@haml_buffer.active = false
old_buffer.active = was_active if old_buffer
@haml_buffer = old_buffer
end
# Gets a reference to the current Haml::Buffer object.
def haml_buffer
@haml_buffer
end
# Gives a proc the same local "_hamlout" and "_erbout" variables
# that the current template has.
def haml_bind_proc(&proc)
_hamlout = haml_buffer
_erbout = _hamlout.buffer
proc { |*args| proc.call(*args) }
end
include ActionViewExtensions if self.const_defined? "ActionViewExtensions"
end
end
class Object
# Haml overrides various ActionView helpers,
# which call an #is_haml? method
# to determine whether or not the current context object
# is a proper Haml context.
# Because ActionView helpers may be included in non-ActionView::Base classes,
# it's a good idea to define is_haml? for all objects.
def is_haml?
false
end
end
&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="" method="post">
</p>
<div>
<form action="" method="post">
<div><input name="commit" type="submit" value="save" /></div>
<p>
@foo =
value one
</p>
Toplevel? false
<p>
@foo =
value three
</p>
</form>
<form action="" 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>
</div>
<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>
<hr />
<div></div>
%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
<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>
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
method = @@options[:xhtml] ? Hpricot.method(:XML) : method(:Hpricot)
@template = method.call(template.gsub('&', '&amp;'))
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) } if children
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]}", CGI.unescapeHTML(self.inner_text))
end
output += "%#{name}" unless name == 'div' && (static_id? || static_classname?)
if attributes
if static_id?
output += "##{attributes['id']}"
remove_attribute('id')
end
if static_classname?
attributes['class'].split(' ').each { |c| output += ".#{c}" }
remove_attribute('class')
end
output += haml_attributes if attributes.length > 0
end
(self.children || []).inject(output + "\n") do |output, child|
output + child.to_haml(tabs + 1)
end
end
private
def dynamic_attributes
@dynamic_attributes ||= begin
attributes.inject({}) do |dynamic, pair|
name, value = pair
unless value.empty?
full_match = nil
ruby_value = value.gsub(%r{<haml:loud>\s*(.+?)\s*</haml:loud>}) do
full_match = $`.empty? && $'.empty?
full_match ? $1: "\#{#{$1}}"
end
unless ruby_value == value
dynamic[name] = full_match ? ruby_value : %("#{ruby_value}")
end
end
dynamic
end
end
end
def static_attribute?(name)
attributes[name] and !dynamic_attribute?(name)
end
def dynamic_attribute?(name)
HTML.options[:rhtml] and dynamic_attributes.key?(name)
end
def static_id?
static_attribute? 'id'
end
def static_classname?
static_attribute? 'class'
end
# Returns a string representation of an attributes hash
# that's prettier than that produced by Hash#inspect
def haml_attributes
attrs = attributes.map do |name, value|
value = dynamic_attribute?(name) ? dynamic_attributes[name] : value.inspect
name = name.index(/\W/) ? name.inspect : ":#{name}"
"#{name} => #{value}"
end
"{ #{attrs.join(', ')} }"
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
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../lib/haml'
require 'haml/exec'
opts = Haml::Exec::HTML2Haml.new(ARGV)
opts.parse!
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../test_helper'
require 'haml/html'
class Html2HamlTest < Test::Unit::TestCase
def test_empty_render_should_remain_empty
assert_equal '', render('')
end
def test_id_and_class_should_be_removed_from_hash
assert_equal '%span#foo.bar', render('<span id="foo" class="bar"> </span>')
end
def test_no_tag_name_for_div_if_class_or_id_is_present
assert_equal '#foo', render('<div id="foo"> </div>')
assert_equal '.foo', render('<div class="foo"> </div>')
end
def test_multiple_class_names
assert_equal '.foo.bar.baz', render('<div class=" foo bar baz "> </div>')
end
def test_should_have_pretty_attributes
assert_equal_attributes('%input{ :type => "text", :name => "login" }',
render('<input type="text" name="login" />'))
assert_equal_attributes('%meta{ "http-equiv" => "Content-Type", :content => "text/html" }',
render('<meta http-equiv="Content-Type" content="text/html" />'))
end
def test_sqml_comment
assert_equal "/\n IE sucks", render('<!-- IE sucks -->')
end
def test_rhtml
assert_equal '- foo = bar', render_rhtml('<% foo = bar %>')
assert_equal '- foo = bar', render_rhtml('<% foo = bar -%>')
assert_equal '= h @item.title', render_rhtml('<%=h @item.title %>')
assert_equal '= h @item.title', render_rhtml('<%=h @item.title -%>')
end
def test_rhtml_with_html_special_chars
assert_equal '= 3 < 5 ? "OK" : "Your computer is b0rken"',
render_rhtml(%Q{<%= 3 < 5 ? "OK" : "Your computer is b0rken" %>})
end
def test_rhtml_in_class_attribute
assert_equal "%div{ :class => dyna_class }\n I have a dynamic attribute",
render_rhtml(%Q{<div class="<%= dyna_class %>">I have a dynamic attribute</div>})
end
def test_rhtml_in_id_attribute
assert_equal "%div{ :id => dyna_id }\n I have a dynamic attribute",
render_rhtml(%Q{<div id="<%= dyna_id %>">I have a dynamic attribute</div>})
end
def test_rhtml_in_attribute_results_in_string_interpolation
assert_equal %(%div{ :id => "item_\#{i}" }\n Ruby string interpolation FTW),
render_rhtml(%Q{<div id="item_<%= i %>">Ruby string interpolation FTW</div>})
end
def test_rhtml_in_attribute_with_trailing_content
assert_equal %(%div{ :class => "\#{12}!" }\n Bang!),
render_rhtml(%Q{<div class="<%= 12 %>!">Bang!</div>})
end
def test_rhtml_in_attribute_to_multiple_interpolations
assert_equal %(%div{ :class => "\#{12} + \#{13}" }\n Math is super),
render_rhtml(%Q{<div class="<%= 12 %> + <%= 13 %>">Math is super</div>})
end
def test_whitespace_eating_erb_tags
assert_equal %(- form_for),
render_rhtml(%Q{<%- form_for -%>})
end
protected
def render(text, options = {})
Haml::HTML.new(text, options).render.rstrip
end
def render_rhtml(text)
render(text, :rhtml => true)
end
def assert_equal_attributes(expected, result)
expected_attr, result_attr = [expected, result].map { |s| s.gsub!(/\{ (.+) \}/, ''); $1.split(', ').sort }
assert_equal expected_attr, result_attr
assert_equal expected, result
end
end
imported { otherconst: hello; myconst: goodbye; pre-mixin: here; }
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);
#foo { background-color: #baf; }
nonimported { myconst: hello; otherconst: goodbye; post-mixin: here; }
!preconst = hello
=premixin
pre-mixin: here
@import importee, basic, basic.css, ../results/complex.css, partial
nonimported
:myconst = !preconst
:otherconst = !postconst
+postmixin
!postconst = goodbye
=postmixin
post-mixin: here
imported
:otherconst = !preconst
:myconst = !postconst
+premixin
@import basic
midrule
:inthe middle
begin
require File.join(File.dirname(__FILE__), 'lib', 'haml') # From here
rescue LoadError
require 'haml' # From gem
end
# Load Haml and Sass
Haml.init_rails(binding)
Kernel.load File.join(File.dirname(__FILE__), '..', 'init.rb')
!!! 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}!
== Embedded? #{"twice! #{true}"}!
== Embedded? #{"one"} af"t"er #{"another"}!
%p== Embedded? false!
%p== Embedded? #{true}!
- embedded = true
%p== Embedded? #{embedded}!
%p== Embedded? #{"twice! #{true}"}!
%p== Embedded? #{"one"} af"t"er #{"another"}!
= "stuff followed by whitespace"
- if true
%strong block with whitespace
%p
\Escape
\- character
\%p foo
\yee\ha
/ Short comment
/
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 should appear!
%p{ :gorbachev => nil } this 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_self_closing{:dyn => 1 + 2}/
%body
:plain
hello
%div
%img
<?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!
Embedded? twice! true!
Embedded? one af"t"er another!
<p>Embedded? false!</p>
<p>Embedded? true!</p>
<p>Embedded? true!</p>
<p>Embedded? twice! true!</p>
<p>Embedded? one af"t"er another!</p>
stuff followed by whitespace
<strong>block with whitespace</strong>
<p>
Escape
- character
%p foo
yee\ha
</p>
<!-- Short comment -->
<!--
This is a block comment
cool, huh?
<strong>there can even be sub-tags!</strong>
Or script!
-->
<p class=''>class attribute should appear!</p>
<p>this 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_self_closing' dyn='3' />
<body>
hello
<div>
<img />
</div>
</body>
# allows testing with edge Rails by creating a test/rails symlink
linked_rails = File.dirname(__FILE__) + '/rails'
if File.exists?(linked_rails) && !$:.include?(linked_rails + '/activesupport/lib')
puts "[ using linked Rails ]"
$:.unshift linked_rails + '/activesupport/lib'
$:.unshift linked_rails + '/actionpack/lib'
else
require 'rubygems'
end
require 'action_controller'
require 'action_view'
! Not a Doctype !
%ul
%li a
%li b
%li c
%li d
%li e
%li f
%li g
%li h
%li i
! 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>
# 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'
require 'sass/constant/nil'
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
unless defined?(Sass::MERB_LOADED)
Sass::MERB_LOADED = true
version = Merb::VERSION.split('.').map { |n| n.to_i }
if version[0] <= 0 && version[1] < 5
root = MERB_ROOT
env = MERB_ENV
else
root = Merb.root.to_s
env = Merb.environment
end
Sass::Plugin.options.merge!(:template_location => root + '/public/stylesheets/sass',
:css_location => root + '/public/stylesheets',
:always_check => env != "production",
:full_exception => env != "production")
config = Merb::Plugins.config[:sass] || Merb::Plugins.config["sass"] || {}
if defined? config.symbolize_keys!
config.symbolize_keys!
end
Sass::Plugin.options.merge!(config)
if version[0] > 0 || version[1] >= 9
class Merb::Rack::Application # :nodoc:
def call_with_sass(env)
if !Sass::Plugin.checked_for_updates ||
Sass::Plugin.options[:always_update] || Sass::Plugin.options[:always_check]
Sass::Plugin.update_stylesheets
end
call_without_sass(env)
end
alias_method :call_without_sass, :call
alias_method :call, :call_with_sass
end
else
class MerbHandler # :nodoc:
def process_with_sass(request, response)
if !Sass::Plugin.checked_for_updates ||
Sass::Plugin.options[:always_update] || Sass::Plugin.options[:always_check]
Sass::Plugin.update_stylesheets
end
process_without_sass(request, response)
end
alias_method :process_without_sass, :process
alias_method :process, :process_with_sass
end
end
end
Copyright (c) 2006-2008 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.
#main {
width: 15em;
color: #0000ff;
}
#main p {
border-top-width: 2px;
border-top-color: #ffcc00;
border-left-width: 1px;
border-left-color: #000;
-moz-border-radius: 10px;
border-style: dotted;
border-width: 2px;
}
#main .cool {
width: 100px;
}
#left {
border-top-width: 2px;
border-top-color: #ffcc00;
border-left-width: 1px;
border-left-color: #000;
-moz-border-radius: 10px;
font-size: 2em;
font-weight: bold;
float: left;
}
#right {
border-top-width: 2px;
border-top-color: #ffcc00;
border-left-width: 1px;
border-left-color: #000;
-moz-border-radius: 10px;
color: #f00;
font-size: 20px;
float: right;
}
.bordered {
border-top-width: 2px;
border-top-color: #ffcc00;
border-left-width: 1px;
border-left-color: #000;
-moz-border-radius: 10px;
}
.complex {
color: #f00;
font-size: 20px;
text-decoration: none;
}
.complex:after {
content: ".";
display: block;
height: 0;
clear: both;
visibility: hidden;
}
* html .complex {
height: 1px;
color: #f00;
font-size: 20px;
}
.more-complex {
color: #f00;
font-size: 20px;
text-decoration: none;
display: inline;
-webkit-nonsense-top-right: 1px;
-webkit-nonsense-bottom-left: 1px;
}
.more-complex:after {
content: ".";
display: block;
height: 0;
clear: both;
visibility: hidden;
}
* html .more-complex {
height: 1px;
color: #f00;
font-size: 20px;
}
.more-complex a:hover {
text-decoration: underline;
color: #f00;
font-size: 20px;
border-top-width: 2px;
border-top-color: #ffcc00;
border-left-width: 1px;
border-left-color: #000;
-moz-border-radius: 10px;
}
!yellow = #fc0
=bordered
:border
:top
:width 2px
:color = !yellow
:left
:width 1px
:color #000
-moz-border-radius: 10px
=header-font
:color #f00
:font
:size 20px
=compound
+header-font
+bordered
=complex
+header-font
text:
decoration: none
&:after
content: "."
display: block
height: 0
clear: both
visibility: hidden
* html &
height: 1px
+header-font
=deep
a:hover
:text-decoration underline
+compound
#main
:width 15em
:color #0000ff
p
+bordered
:border
:style dotted
:width 2px
.cool
:width 100px
#left
+bordered
:font
:size 2em
:weight bold
:float left
#right
+bordered
+header-font
:float right
.bordered
+bordered
.complex
+complex
.more-complex
+complex
+deep
display: inline
-webkit-nonsense:
top-right: 1px
bottom-left: 1px
#main,
#header {
height: 50px; }
#main div,
#header div {
width: 100px; }
#main div a span,
#main div em span,
#header div a span,
#header div em span {
color: pink; }
#one div.nested,
#one span.nested,
#one p.nested,
#two div.nested,
#two span.nested,
#two p.nested,
#three div.nested,
#three span.nested,
#three p.nested {
font-weight: bold;
border-color: red;
display: block; }
#main,
#header
height: 50px
div
width: 100px
a,
em
span
color: pink
#one,
#two,
#three
div.nested,
span.nested,
p.nested
:font
:weight bold
:border-color red
:display block
#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; }
#right .header {
border-style: solid; }
#right .body {
border-style: dotted; }
#right .footer {
border-style: dashed; }
#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
#right
.header
:border-style solid
.body
:border-style dotted
.footer
:border-style dashed
#pi { width: 314px; }
require 'sass/constant/literal'
module Sass::Constant # :nodoc:
class Nil < Literal # :nodoc:
def to_s
''
end
end
end
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 ==(other)
self.class == other.class && other.children == children
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)
else
result << "#{child.to_s(1)}" + (@style == :compressed ? '' : "\n")
end
end
@style == :compressed ? result+"\n" : 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
%p
%q< Foo
%p
%q{:a => 1 + 1}< Foo
%p
%q<= "Foo\nBar"
%p
%q{:a => 1 + 1}<= "Foo\nBar"
%p
%q<
Foo
Bar
%p
%q{:a => 1 + 1}<
Foo
Bar
%p
%q<
%div
Foo
Bar
%p
%q{:a => 1 + 1}<
%div
Foo
Bar
-# Regression test
%p
%q<= "foo"
%q{:a => 1 + 1}
bar
<p>
<q>Foo</q>
</p>
<p>
<q a='2'>Foo</q>
</p>
<p>
<q>Foo
Bar</q>
</p>
<p>
<q a='2'>Foo
Bar</q>
</p>
<p>
<q>Foo
Bar</q>
</p>
<p>
<q a='2'>Foo
Bar</q>
</p>
<p>
<q><div>
Foo
Bar
</div></q>
</p>
<p>
<q a='2'><div>
Foo
Bar
</div></q>
</p>
<p>
<q>foo</q>
<q a='2'>
bar
</q>
</p>
%p
%p
%q>
Foo
%p
%p
%q{:a => 1 + 1}>
Foo
%p
%p
%q> Foo
%p
%p
%q{:a => 1 + 1}> Foo
%p
%p
%q>
= "Foo"
%p
%p
%q{:a => 1 + 1}>
= "Foo"
%p
%p
%q>= "Foo"
%p
%p
%q{:a => 1 + 1}>= "Foo"
%p
%p
%q>
= "Foo\nBar"
%p
%p
%q{:a => 1 + 1}>
= "Foo\nBar"
%p
%p
%q>= "Foo\nBar"
%p
%p
%q{:a => 1 + 1}>= "Foo\nBar"
%p
%p
- tab_up
foo
%q>
Foo
bar
- tab_down
%p
%p
- tab_up
foo
%q{:a => 1 + 1}>
Foo
bar
- tab_down
%p
%p
- tab_up
foo
%q> Foo
bar
- tab_down
%p
%p
- tab_up
foo
%q{:a => 1 + 1}> Foo
bar
- tab_down
%p
%p
- tab_up
foo
%q>
= "Foo"
bar
- tab_down
%p
%p
- tab_up
foo
%q{:a => 1 + 1}>
= "Foo"
bar
- tab_down
%p
%p
- tab_up
foo
%q>= "Foo"
bar
- tab_down
%p
%p
- tab_up
foo
%q{:a => 1 + 1}>= "Foo"
bar
- tab_down
%p
%p
- tab_up
foo
%q>
= "Foo\nBar"
bar
- tab_down
%p
%p
- tab_up
foo
%q{:a => 1 + 1}>
= "Foo\nBar"
bar
- tab_down
%p
%p
- tab_up
foo
%q>= "Foo\nBar"
bar
- tab_down
%p
%p
- tab_up
foo
%q{:a => 1 + 1}>= "Foo\nBar"
bar
- tab_down
%p
%p
%q>
%p
%p
%q>/
%p
%p
%q{:a => 1 + 1}>
%p
%p
%q{:a => 1 + 1}>/
<p>
<p><q>
Foo
</q></p>
</p>
<p>
<p><q a='2'>
Foo
</q></p>
</p>
<p>
<p><q>Foo</q></p>
</p>
<p>
<p><q a='2'>Foo</q></p>
</p>
<p>
<p><q>
Foo
</q></p>
</p>
<p>
<p><q a='2'>
Foo
</q></p>
</p>
<p>
<p><q>Foo</q></p>
</p>
<p>
<p><q a='2'>Foo</q></p>
</p>
<p>
<p><q>
Foo
Bar
</q></p>
</p>
<p>
<p><q a='2'>
Foo
Bar
</q></p>
</p>
<p>
<p><q>
Foo
Bar
</q></p>
</p>
<p>
<p><q a='2'>
Foo
Bar
</q></p>
</p>
<p>
<p>
foo<q>
Foo
</q>bar
</p>
</p>
<p>
<p>
foo<q a='2'>
Foo
</q>bar
</p>
</p>
<p>
<p>
foo<q>Foo</q>bar
</p>
</p>
<p>
<p>
foo<q a='2'>Foo</q>bar
</p>
</p>
<p>
<p>
foo<q>
Foo
</q>bar
</p>
</p>
<p>
<p>
foo<q a='2'>
Foo
</q>bar
</p>
</p>
<p>
<p>
foo<q>Foo</q>bar
</p>
</p>
<p>
<p>
foo<q a='2'>Foo</q>bar
</p>
</p>
<p>
<p>
foo<q>
Foo
Bar
</q>bar
</p>
</p>
<p>
<p>
foo<q a='2'>
Foo
Bar
</q>bar
</p>
</p>
<p>
<p>
foo<q>
Foo
Bar
</q>bar
</p>
</p>
<p>
<p>
foo<q a='2'>
Foo
Bar
</q>bar
</p>
</p>
<p>
<p><q></q></p>
</p>
<p>
<p><q /></p>
</p>
<p>
<p><q a='2'></q></p>
</p>
<p>
<p><q a='2' /></p>
</p>
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.empty? ? nil : unit
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
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
!!!
%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?
<!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>
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; }
a
:color #000
&:hover
:color #f00
p, div
:width 100em
& foo
:width 10em
&:hover, bar
:height 20em
#cool
:border
:style solid
:width 2em
.ie7 &, .ie6 &
:content string(Totally not cool.)
.firefox &
:content string(Quite cool.)
.wow, .snazzy
:font-family fantasy
&:hover, &:visited
:font-weight bold
%h1 Partial layout used with for block:
- render :layout => 'layout_for_partial.haml' do
%p Some content within a layout
<h1>Partial layout used with for block:</h1>
<div class='partial-layout'>
<h2>This is inside a partial layout</h2>
<p>Some content within a layout</p>
</div>
= render :file => "#{name}.haml"
- @foo = 'value one'
%p
@foo =
= @foo
- @foo = 'value two'
%p
@foo =
= @foo
= test_partial "partial"
%p
@foo =
= @foo
<p>
@foo =
value one
</p>
<p>
@foo =
value two
</p>
<p>
@foo =
value two
</p>
Toplevel? false
<p>
@foo =
value three
</p>
<p>
@foo =
value three
</p>
# This file makes Haml work with Rails
# by monkeypatching the core template-compilation methods.
# This is necessary for versions <= 2.0.1 because the template handler API
# wasn't sufficiently powerful to deal with caching and so forth.
# 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:
def delegate_template_exists_with_haml(template_path)
template_exists?(template_path, :haml) && [:haml]
end
alias_method :delegate_template_exists_without_haml, :delegate_template_exists?
alias_method :delegate_template_exists?, :delegate_template_exists_with_haml
def compile_template_with_haml(extension, template, file_name, local_assigns)
return compile_haml(template, file_name, local_assigns) if extension.to_s == "haml"
compile_template_without_haml(extension, template, file_name, local_assigns)
end
alias_method :compile_template_without_haml, :compile_template
alias_method :compile_template, :compile_template_with_haml
def compile_haml(template, file_name, local_assigns)
render_symbol = assign_method_name(:haml, template, file_name)
locals = local_assigns.keys
@@template_args[render_symbol] ||= {}
locals_keys = @@template_args[render_symbol].keys | locals
@@template_args[render_symbol] = locals_keys.inject({}) { |h, k| h[k] = true; h }
options = Haml::Template.options.dup
options[:filename] = file_name || 'compiled-template'
begin
Haml::Engine.new(template, options).def_method(CompiledTemplates, render_symbol, *locals_keys)
rescue Exception => e
if logger
logger.debug "ERROR: compiling #{render_symbol} RAISED #{e}"
logger.debug "Backtrace: #{e.backtrace.join("\n")}"
end
base_path = if defined?(extract_base_path_from)
# Rails 2.0.x
extract_base_path_from(file_name) || view_paths.first
else
# Rails <=1.2.6
@base_path
end
raise ActionView::TemplateError.new(base_path, file_name || template, @assigns, template, e)
end
@@compile_time[render_symbol] = Time.now
end
end
end
# :stopdoc:
# This file makes Haml work with Rails
# using the > 2.0.1 template handler API.
module Haml
class Plugin < ActionView::TemplateHandler
include ActionView::TemplateHandlers::Compilable if defined?(ActionView::TemplateHandlers::Compilable)
def compile(template)
options = Haml::Template.options.dup
# template is a template object in Rails >=2.1.0,
# a source string previously
if template.respond_to? :source
options[:filename] = template.filename
source = template.source
else
source = template
end
Haml::Engine.new(source, options).send(:precompiled_with_ambles, [])
end
def cache_fragment(block, name = {}, options = nil)
@view.fragment_for(block, name, options) do
eval("_hamlout.buffer", block.binding)
end
end
end
end
if defined? ActionView::Template and ActionView::Template.respond_to? :register_template_handler
ActionView::Template
else
ActionView::Base
end.register_template_handler(:haml, Haml::Plugin)
# In Rails 2.0.2, ActionView::TemplateError took arguments
# that we can't fill in from the Haml::Plugin context.
# Thus, we've got to monkeypatch ActionView::Base to catch the error.
if ActionView::TemplateError.instance_method(:initialize).arity == 5
class ActionView::Base
def compile_template(handler, template, file_name, local_assigns)
render_symbol = assign_method_name(handler, template, file_name)
# Move begin up two lines so it captures compilation exceptions.
begin
render_source = create_template_source(handler, template, render_symbol, local_assigns.keys)
line_offset = @@template_args[render_symbol].size + handler.line_offset
file_name = 'compiled-template' if file_name.blank?
CompiledTemplates.module_eval(render_source, file_name, -line_offset)
rescue Exception => e # errors from template code
if logger
logger.debug "ERROR: compiling #{render_symbol} RAISED #{e}"
logger.debug "Function body: #{render_source}"
logger.debug "Backtrace: #{e.backtrace.join("\n")}"
end
# There's no way to tell Haml about the filename,
# so we've got to insert it ourselves.
e.backtrace[0].gsub!('(haml)', file_name) if e.is_a?(Haml::Error)
raise ActionView::TemplateError.new(extract_base_path_from(file_name) || view_paths.first, file_name || template, @assigns, template, e)
end
@@compile_time[render_symbol] = Time.now
# logger.debug "Compiled template #{file_name || template}\n ==> #{render_symbol}" if logger
end
end
end
# :startdoc:
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
}
@@checked_for_updates = false
# Whether or not Sass has *ever* checked if the stylesheets need updates
# (in this Ruby instance).
def checked_for_updates
@@checked_for_updates
end
# Gets various options for Sass. See README.rdoc for details.
#--
# TODO: *DOCUMENT OPTIONS*
#++
def options
@@options
end
# Sets various options for Sass.
def options=(value)
@@options.merge!(value)
end
# Get the options ready to be passed to the Sass::Engine
def engine_options(additional_options = {})
opts = options.dup.merge(additional_options)
opts[:load_paths] = load_paths(opts)
opts
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
return if options[:never_update]
@@checked_for_updates = true
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 !forbid_update?(name) && (options[:always_update] || stylesheet_needs_update?(name))
css = css_filename(name)
File.delete(css) if File.exists?(css)
filename = template_filename(name)
engine = Engine.new(File.read(filename), engine_options(:filename => filename))
result = begin
engine.render
rescue Exception => e
exception_string(e)
end
# Create any directories that might be necessary
dirs = [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 load_paths(opts = options)
(opts[:load_paths] || []) + [options[:template_location]]
end
def exception_string(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
<<END
/*
#{e_string}
Backtrace:\n#{e.backtrace.join("\n")}
*/
body:before {
white-space: pre;
font-family: monospace;
content: "#{e_string.gsub('"', '\"').gsub("\n", '\\A ')}"; }
END
# Fix an emacs syntax-highlighting hiccup: '
else
"/* Internal stylesheet error */"
end
end
def template_filename(name)
"#{options[:template_location]}/#{name}.sass"
end
def css_filename(name)
"#{options[:css_location]}/#{name}.css"
end
def forbid_update?(name)
name.sub(/^.*\//, '')[0] == ?_
end
def stylesheet_needs_update?(name)
if !File.exists?(css_filename(name))
return true
else
css_mtime = File.mtime(css_filename(name))
File.mtime(template_filename(name)) > css_mtime ||
dependencies(template_filename(name)).any?(&dependency_updated?(css_mtime))
end
end
def dependency_updated?(css_mtime)
lambda do |dep|
File.mtime(dep) > css_mtime ||
dependencies(dep).any?(&dependency_updated?(css_mtime))
end
end
def dependencies(filename)
File.readlines(filename).grep(/^@import /).map do |line|
line[8..-1].split(',').map do |inc|
Sass::Engine.find_file_to_import(inc.strip, load_paths)
end
end.flatten.grep(/\.sass$/)
end
end
end
end
require 'sass/plugin/rails' if defined?(ActionController)
require 'sass/plugin/merb' if defined?(Merb::Plugins)
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../test_helper'
require 'sass/plugin'
require 'fileutils'
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_update_needed_when_modified
sleep(1)
FileUtils.touch(template_loc('basic'))
assert Sass::Plugin.stylesheet_needs_update?('basic')
Sass::Plugin.update_stylesheets
assert !Sass::Plugin.stylesheet_needs_update?('basic')
end
def test_update_needed_when_dependency_modified
sleep(1)
FileUtils.touch(template_loc('basic'))
assert Sass::Plugin.stylesheet_needs_update?('import')
Sass::Plugin.update_stylesheets
assert !Sass::Plugin.stylesheet_needs_update?('import')
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'
if defined?(MerbHandler)
MerbHandler.send(:define_method, :process_without_sass) { |*args| }
else
Merb::Rack::Application.send(:define_method, :call_without_sass) { |*args| }
end
set_plugin_opts
File.delete(tempfile_loc('basic'))
assert Sass::Plugin.stylesheet_needs_update?('basic')
if defined?(MerbHandler)
MerbHandler.new('.').process nil, nil
else
Merb::Rack::Application.new.call(::Rack::MockRequest.env_for('/'))
end
assert !Sass::Plugin.stylesheet_needs_update?('basic')
end
def test_doesnt_render_partials
assert !File.exists?(tempfile_loc('_partial'))
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 template_loc(name)
File.dirname(__FILE__) + "/templates/#{name}.sass"
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
undef :sass_old_process
def sass_old_process(*args); end
end
require 'strscan'
module Haml
module Precompiler
# 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 or script that is never HTML-escaped.
DOCTYPE = ?!
# Designates script, the result of which is output.
SCRIPT = ?=
# Designates script that is always HTML-escaped.
SANITIZE = ?&
# 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,
SANITIZE,
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 a Doctype command.
DOCTYPE_REGEX = /(\d\.\d)?[\s]*([a-z]*)/i
# The Regex that matches a literal string or symbol value
LITERAL_VALUE_REGEX = /^\s*(:(\w*)|(('|")([^\\\#'"]*?)\4))\s*$/
private
# Returns the precompiled string with the preamble and postamble
def precompiled_with_ambles(local_names)
preamble = <<END.gsub("\n", ";")
extend Haml::Helpers
_hamlout = @haml_buffer = Haml::Buffer.new(@haml_buffer, #{options_for_buffer.inspect})
_erbout = _hamlout.buffer
__in_erb_template = true
END
postamble = <<END.gsub("\n", ";")
@haml_buffer = @haml_buffer.upper
_erbout
END
preamble + locals_code(local_names) + @precompiled + postamble
end
def locals_code(names)
names = names.keys if Hash == names
names.map do |name|
# Can't use || because someone might explicitly pass in false with a symbol
sym_local = "_haml_locals[#{name.to_sym.inspect}]"
str_local = "_haml_locals[#{name.to_s.inspect}]"
"#{name} = #{sym_local}.nil? ? #{str_local} : #{sym_local}"
end.join(';') + ';'
end
Line = Struct.new(:text, :unstripped, :index, :spaces, :tabs)
def precompile
@haml_comment = @dont_indent_next_line = @dont_tab_up_next_text = false
@indentation = nil
old_line = Line.new
@template.split(/\r\n|\r|\n/).each_with_index do |text, index|
@next_line = line = Line.new(text.strip, text.lstrip.chomp, index)
line.spaces, line.tabs = count_soft_tabs(text)
suppress_render = handle_multiline(old_line) unless flat?
if old_line.text.nil? || suppress_render
old_line = line
resolve_newlines
newline
next
end
process_indent(old_line) unless old_line.text.empty?
if line.text.empty? && !flat?
newline
next
end
if flat?
push_flat(old_line)
old_line = line
newline
next
end
if old_line.spaces != old_line.tabs * 2
raise SyntaxError.new(<<END.strip, old_line.index)
#{old_line.spaces} space#{old_line.spaces == 1 ? ' was' : 's were'} used for indentation. Haml must be indented using two spaces.
END
end
unless old_line.text.empty? || @haml_comment
process_line(old_line.text, old_line.index, line.tabs > old_line.tabs && !line.text.empty?)
end
resolve_newlines
if !flat? && line.tabs - old_line.tabs > 1
raise SyntaxError.new(<<END.strip, line.index)
#{line.spaces} spaces were used for indentation. Haml must be indented using two spaces.
END
end
old_line = line
newline
end
# Close all the open tags
close until @to_close_stack.empty?
flush_merged_text
end
# Processes and deals with lowering indentation.
def process_indent(line)
return unless line.tabs <= @template_tabs && @template_tabs > 0
to_close = @template_tabs - line.tabs
to_close.times { |i| close unless to_close - 1 - i == 0 && mid_block_keyword?(line.text) }
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(text, index, block_opened)
@block_opened = block_opened
@index = index + 1
case text[0]
when DIV_CLASS, DIV_ID; render_div(text)
when ELEMENT; render_tag(text)
when COMMENT; render_comment(text[1..-1].strip)
when SANITIZE
return push_script(unescape_interpolation(text[3..-1].strip), false, false, false, true) if text[1..2] == "=="
return push_script(text[2..-1].strip, false, false, false, true) if text[1] == SCRIPT
push_plain text
when SCRIPT
return push_script(unescape_interpolation(text[2..-1].strip), false) if text[1] == SCRIPT
return push_script(text[1..-1], false, false, false, true) if options[:escape_html]
push_script(text[1..-1], false)
when FLAT_SCRIPT; push_flat_script(text[1..-1])
when SILENT_SCRIPT
return start_haml_comment if text[1] == SILENT_COMMENT
raise SyntaxError.new(<<END.rstrip, index) if text[1..-1].strip == "end"
You don't need to use "- end" in Haml. Use indentation instead:
- if foo?
%strong Foo!
- else
Not foo.
END
push_silent(text[1..-1], true)
newline_now
case_stmt = text[1..-1].split(' ', 2)[0] == "case"
block = @block_opened && !mid_block_keyword?(text)
push_and_tabulate([:script]) if block || case_stmt
push_and_tabulate(nil) if block && case_stmt
when FILTER; start_filtered(text[1..-1].downcase)
when DOCTYPE
return render_doctype(text) if text[0...3] == '!!!'
return push_script(unescape_interpolation(text[3..-1].strip), false) if text[1..2] == "=="
return push_script(text[2..-1].strip, false) if text[1] == SCRIPT
push_plain text
when ESCAPE; push_plain text[1..-1]
else push_plain text
end
end
# Returns whether or not the text is a silent script text with one
# of Ruby's mid-block keywords.
def mid_block_keyword?(text)
text.length > 2 && text[0] == SILENT_SCRIPT && MID_BLOCK_KEYWORDS.include?(text[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(line)
text = line.text
# A multiline string is active, and is being continued
if is_multiline?(text) && @multiline
@multiline.text << text[0...-1]
return true
end
# A multiline string has just been activated, start adding the lines
if is_multiline?(text) && (MULTILINE_STARTERS.include? text[0])
@multiline = Line.new text[0...-1], nil, line.index, nil, line.tabs
process_indent(line)
return true
end
# A multiline string has just ended, make line into the result
if @multiline && !line.text.empty?
process_line(@multiline.text, @multiline.index, line.tabs > @multiline.tabs)
@multiline = nil
end
return false
end
# Checks whether or not +line+ is in a multiline sequence.
def is_multiline?(text)
text && text.length > 1 && text[-1] == MULTILINE_CHAR_VALUE && text[-2] == ?\s
end
# Evaluates <tt>text</tt> in the context of the scope object, but
# does not output the result.
def push_silent(text, can_suppress = false)
flush_merged_text
return if can_suppress && options[:suppress_eval]
@precompiled << "#{text};"
end
# Adds <tt>text</tt> to <tt>@buffer</tt> with appropriate tabulation
# without parsing it.
def push_merged_text(text, tab_change = 0, indent = true)
@merged_text << (!indent || @dont_indent_next_line || @options[:ugly] ? text : "#{' ' * @output_tabs}#{text}")
@dont_indent_next_line = false
@tab_change += tab_change
end
# Concatenate <tt>text</tt> to <tt>@buffer</tt> without tabulation.
def concat_merged_text(text)
@merged_text << text
end
def push_text(text, tab_change = 0)
push_merged_text("#{text}\n", tab_change)
end
def flush_merged_text
return if @merged_text.empty?
@precompiled << "_hamlout.push_text(#{@merged_text.dump}"
@precompiled << ", #{@dont_tab_up_next_text.inspect}" if @dont_tab_up_next_text || @tab_change != 0
@precompiled << ", #{@tab_change}" if @tab_change != 0
@precompiled << ");"
@merged_text = ''
@dont_tab_up_next_text = false
@tab_change = 0
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.", @next_line.index)
end
push_text text
end
# Adds +text+ to <tt>@buffer</tt> while flattening text.
def push_flat(line)
tabulation = line.spaces - @flat_spaces
tabulation = tabulation > -1 ? tabulation : 0
@filter_buffer << "#{' ' * tabulation}#{line.unstripped}\n"
end
# Causes <tt>text</tt> to be evaluated in the context of
# the scope object and the result to be added to <tt>@buffer</tt>.
#
# If <tt>preserve_script</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, preserve_script, in_tag = false, preserve_tag = false,
escape_html = false, nuke_inner_whitespace = false)
# Prerender tabulation unless we're in a tag
push_merged_text '' unless in_tag
flush_merged_text
return if options[:suppress_eval]
raise SyntaxError.new("There's no Ruby code for = to evaluate.") if text.empty?
push_silent "haml_temp = #{text}"
newline_now
args = [preserve_script, in_tag, preserve_tag,
escape_html, nuke_inner_whitespace].map { |a| a.inspect }.join(', ')
out = "haml_temp = _hamlout.push_script(haml_temp, #{args});"
if @block_opened
push_and_tabulate([:loud, out])
else
@precompiled << out
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)
flush_merged_text
raise SyntaxError.new("There's no Ruby code for ~ to evaluate.") if text.empty?
push_script(text, true)
end
def start_haml_comment
return unless @block_opened
@haml_comment = true
push_and_tabulate([:haml_comment])
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
when nil; close_nil
end
end
# Puts a line in <tt>@precompiled</tt> that will add the closing tag of
# the most recently opened tag.
def close_tag(value)
tag, nuke_outer_whitespace, nuke_inner_whitespace = value
@output_tabs -= 1 unless nuke_inner_whitespace
@template_tabs -= 1
rstrip_buffer! if nuke_inner_whitespace
push_merged_text("</#{tag}>" + (nuke_outer_whitespace ? "" : "\n"),
nuke_inner_whitespace ? 0 : -1, !nuke_inner_whitespace)
@dont_indent_next_line = nuke_outer_whitespace
end
# Closes a Ruby block.
def close_block
push_silent "end", true
@template_tabs -= 1
end
# Closes a comment.
def close_comment(has_conditional)
@output_tabs -= 1
@template_tabs -= 1
close_tag = has_conditional ? "<![endif]-->" : "-->"
push_text(close_tag, -1)
end
# Closes a loud Ruby block.
def close_loud(command)
push_silent 'end', true
@precompiled << command
@template_tabs -= 1
end
# Closes a filtered block.
def close_filtered(filter)
@flat_spaces = -1
filter.internal_compile(self, @filter_buffer)
@filter_buffer = nil
@template_tabs -= 1
end
def close_haml_comment
@haml_comment = false
@template_tabs -= 1
end
def close_nil
@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)
return nil unless 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_static_hash(text)
return {} unless text
attributes = {}
text.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 or dynamic and we'll defer it to the actual Ruby parser
key = parse_literal_value key
value = parse_literal_value value
return nil if more || key.nil? || value.nil?
attributes[key] = value
end
attributes
end
# This is a class method so it can be accessed from Buffer.
def self.build_attributes(is_html, attr_wrapper, attributes = {})
quote_escape = attr_wrapper == '"' ? "&quot;" : "&apos;"
other_quote_char = attr_wrapper == '"' ? "'" : '"'
result = attributes.collect do |attr, value|
next if value.nil?
if value == true
next " #{attr}" if is_html
next " #{attr}=#{attr_wrapper}#{attr}#{attr_wrapper}"
elsif value == false
next
end
value = Haml::Helpers.preserve(Haml::Helpers.escape_once(value.to_s))
# We want to decide whether or not to escape quotes
value.gsub!('&quot;', '"')
this_attr_wrapper = attr_wrapper
if value.include? attr_wrapper
if value.include? other_quote_char
value = value.gsub(attr_wrapper, quote_escape)
else
this_attr_wrapper = other_quote_char
end
end
" #{attr}=#{this_attr_wrapper}#{value}#{this_attr_wrapper}"
end
result.compact.sort.join
end
def prerender_tag(name, self_close, attributes)
attributes_string = Precompiler.build_attributes(html?, @options[:attr_wrapper], attributes)
"<#{name}#{attributes_string}#{self_close && xhtml? ? ' /' : ''}>"
end
# Parses a line into tag_name, attributes, attributes_hash, object_ref, action, value
def parse_tag(line)
raise SyntaxError.new("Invalid tag: \"#{line}\".") unless match = line.scan(/%([-:\w]+)([-\w\.\#]*)(.*)/)[0]
tag_name, attributes, rest = match
attributes_hash, rest = parse_attributes(rest) if rest[0] == ?{
if rest
object_ref, rest = balance(rest, ?[, ?]) if rest[0] == ?[
attributes_hash, rest = parse_attributes(rest) if rest[0] == ?{ && attributes_hash.nil?
nuke_whitespace, action, value = rest.scan(/(<>|><|[><])?([=\/\~&!])?(.*)?/)[0]
nuke_whitespace ||= ''
nuke_outer_whitespace = nuke_whitespace.include? '>'
nuke_inner_whitespace = nuke_whitespace.include? '<'
end
value = value.to_s.strip
[tag_name, attributes, attributes_hash, object_ref, nuke_outer_whitespace,
nuke_inner_whitespace, action, value]
end
def parse_attributes(line)
scanner = StringScanner.new(line)
attributes_hash, rest = balance(scanner, ?{, ?})
attributes_hash = attributes_hash[1...-1] if attributes_hash
return attributes_hash, rest
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)
tag_name, attributes, attributes_hash, object_ref, nuke_outer_whitespace,
nuke_inner_whitespace, action, value = parse_tag(line)
raise SyntaxError.new("Illegal element: classes and ids must have values.") if attributes =~ /[\.#](\.|#|\z)/
# Get rid of whitespace outside of the tag if we need to
rstrip_buffer! if nuke_outer_whitespace
preserve_tag = options[:preserve].include?(tag_name)
nuke_inner_whitespace ||= preserve_tag
preserve_tag &&= !options[:ugly]
case action
when '/'; self_closing = true
when '~'; parse = preserve_script = true
when '='
parse = true
value = unescape_interpolation(value[1..-1].strip) if value[0] == ?=
when '&', '!'
if value[0] == ?=
parse = true
value = (value[1] == ?= ? unescape_interpolation(value[2..-1].strip) : value[1..-1].strip)
end
end
if parse && @options[:suppress_eval]
parse = false
value = ''
end
escape_html = (action == '&' || (action != '!' && @options[:escape_html]))
object_ref = "nil" if object_ref.nil? || @options[:suppress_eval]
static_attributes = parse_static_hash(attributes_hash) # Try pre-compiling a static attributes hash
attributes_hash = nil if static_attributes || @options[:suppress_eval]
attributes = parse_class_and_id(attributes)
Buffer.merge_attrs(attributes, static_attributes) if static_attributes
raise SyntaxError.new("Illegal nesting: nesting within a self-closing tag is illegal.", @next_line.index) if @block_opened && self_closing
raise SyntaxError.new("Illegal nesting: content can't be both given on the same line as %#{tag_name} and nested within it.", @next_line.index) if @block_opened && !value.empty?
raise SyntaxError.new("There's no Ruby code for #{action} to evaluate.") if parse && value.empty?
raise SyntaxError.new("Self-closing tags can't have content.") if self_closing && !value.empty?
self_closing ||= !!( !@block_opened && value.empty? && @options[:autoclose].include?(tag_name) )
dont_indent_next_line =
(nuke_outer_whitespace && !@block_opened) ||
(nuke_inner_whitespace && @block_opened)
# Check if we can render the tag directly to text and not process it in the buffer
if object_ref == "nil" && attributes_hash.nil? && !preserve_script
tag_closed = !@block_opened && !self_closing && !parse
open_tag = prerender_tag(tag_name, self_closing, attributes)
if tag_closed
open_tag << "#{value}</#{tag_name}>"
open_tag << "\n" unless nuke_outer_whitespace
else
open_tag << "\n" unless parse || nuke_inner_whitespace || (self_closing && nuke_outer_whitespace)
end
push_merged_text(open_tag, tag_closed || self_closing || nuke_inner_whitespace ? 0 : 1,
!nuke_outer_whitespace)
@dont_indent_next_line = dont_indent_next_line
return if tag_closed
else
flush_merged_text
content = value.empty? || parse ? 'nil' : value.dump
attributes_hash = ', ' + attributes_hash if attributes_hash
args = [tag_name, self_closing, !@block_opened, preserve_tag, escape_html,
attributes, nuke_outer_whitespace, nuke_inner_whitespace
].map { |v| v.inspect }.join(', ')
push_silent "_hamlout.open_tag(#{args}, #{object_ref}, #{content}#{attributes_hash})"
@dont_tab_up_next_text = @dont_indent_next_line = dont_indent_next_line
end
return if self_closing
if value.empty?
push_and_tabulate([:element, [tag_name, nuke_outer_whitespace, nuke_inner_whitespace]])
@output_tabs += 1 unless nuke_inner_whitespace
return
end
if parse
flush_merged_text
push_script(value, preserve_script, true, preserve_tag, escape_html, nuke_inner_whitespace)
@dont_tab_up_next_text = true
concat_merged_text("</#{tag_name}>" + (nuke_outer_whitespace ? "" : "\n"))
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, line = balance(line, ?[, ?]) if line[0] == ?[
line.strip!
conditional << ">" if conditional
if @block_opened && !line.empty?
raise SyntaxError.new('Illegal nesting: nesting within a tag that already has content is illegal.', @next_line.index)
end
open = "<!--#{conditional} "
# Render it statically if possible
unless line.empty?
return push_text("#{open}#{line} #{conditional ? "<![endif]-->" : "-->"}")
end
push_text(open, 1)
@output_tabs += 1
push_and_tabulate([:comment, !conditional.nil?])
unless line.empty?
push_text(line)
close
end
end
# Renders an XHTML doctype or XML shebang.
def render_doctype(line)
raise SyntaxError.new("Illegal nesting: nesting within a header command is illegal.", @next_line.index) if @block_opened
doctype = text_for_doctype(line)
push_text doctype if doctype
end
def text_for_doctype(text)
text = text[3..-1].lstrip.downcase
if text.index("xml") == 0
return nil if html?
wrapper = @options[:attr_wrapper]
return "<?xml version=#{wrapper}1.0#{wrapper} encoding=#{wrapper}#{text.split(' ')[1] || "utf-8"}#{wrapper} ?>"
end
if html5?
'<!DOCTYPE html>'
else
version, type = text.scan(DOCTYPE_REGEX)[0]
if xhtml?
if version == "1.1"
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'
else
case type
when "strict"; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
when "frameset"; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">'
when "mobile"; '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">'
when "basic"; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">'
else '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
end
end
elsif html4?
case type
when "strict"; '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">'
when "frameset"; '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">'
else '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">'
end
end
end
end
# Starts a filtered block.
def start_filtered(name)
raise Error.new("Invalid filter name \":#{name}\".") unless name =~ /^\w+$/
raise Error.new("Filter \"#{name}\" is not defined.") unless filter = Filters.defined[name]
push_and_tabulate([:filtered, filter])
@flat_spaces = @template_tabs * 2
@filter_buffer = String.new
@block_opened = false
end
def contains_interpolation?(str)
str.include?('#{')
end
def unescape_interpolation(str)
scan = StringScanner.new(str.dump)
str = ''
while scan.scan(/(.*?)(\\+)\#\{/)
escapes = (scan[2].size - 1) / 2
str << scan.matched[0...-3 - escapes]
if escapes % 2 == 1
str << '#{'
else
# Use eval to get rid of string escapes
str << '#{' + eval('"' + balance(scan, ?{, ?}, 1)[0][0...-1] + '"') + "}"
end
end
str + scan.rest
end
def balance(scanner, start, finish, count = 0)
str = ''
scanner = StringScanner.new(scanner) unless scanner.is_a? StringScanner
regexp = Regexp.new("(.*?)[\\#{start.chr}\\#{finish.chr}]")
while scanner.scan(regexp)
str << scanner.matched
count += 1 if scanner.matched[-1] == start
count -= 1 if scanner.matched[-1] == finish
return [str.strip, scanner.rest] if count == 0
end
raise SyntaxError.new("Unbalanced brackets.")
end
# Counts the tabulation of a line.
def count_soft_tabs(line)
spaces = line.index(/([^ ]|$)/)
if line[spaces] == ?\t
return 0, 0 if line.strip.empty?
raise SyntaxError.new(<<END.strip, @next_line.index)
A tab character was used for indentation. Haml must be indented using two spaces.
Are you sure you have soft tabs enabled in your editor?
END
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
def flat?
@flat_spaces != -1
end
def newline
@newlines += 1
end
def newline_now
@precompiled << "\n"
@newlines -= 1
end
def resolve_newlines
return unless @newlines > 0
@precompiled << "\n" * @newlines
@newlines = 0
end
# Get rid of and whitespace at the end of the buffer
# or the merged text
def rstrip_buffer!
unless @merged_text.empty?
@merged_text.rstrip!
else
push_silent("_erbout.rstrip!", false)
@dont_tab_up_next_text = true
end
end
end
end
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")
# :stopdoc:
module ActionController
class Base
alias_method :sass_old_process, :process
def process(*args)
if !Sass::Plugin.checked_for_updates ||
Sass::Plugin.options[:always_update] || Sass::Plugin.options[:always_check]
Sass::Plugin.update_stylesheets
end
sass_old_process(*args)
end
end
end
# :startdoc:
end
require 'rubygems'
require 'rake'
# ----- Benchmarking -----
desc <<END
Benchmark haml against ERb.
TIMES=n sets the number of runs. Defaults to 1000.
END
task :benchmark do
sh "ruby test/benchmark.rb #{ENV['TIMES']}"
end
# ----- Default: Testing ------
if ENV["RUN_CODE_RUN"] == "true"
task :default => :"test:rails_compatibility"
else
task :default => :test
end
require 'rake/testtask'
Rake::TestTask.new do |t|
t.libs << 'lib'
test_files = FileList['test/**/*_test.rb']
test_files.exclude('test/rails/*')
t.test_files = test_files
t.verbose = true
end
Rake::Task[:test].send(:add_comment, <<END)
To run with an alternate version of Rails, make test/rails a symlink to that version.
END
# ----- Packaging -----
require 'rake/gempackagetask'
load 'haml.gemspec'
Rake::GemPackageTask.new(HAML_GEMSPEC) do |pkg|
if Rake.application.top_level_tasks.include?('release')
pkg.need_tar_gz = true
pkg.need_tar_bz2 = true
pkg.need_zip = true
end
end
task :revision_file do
require 'lib/haml'
if Haml.version[:rev] && !Rake.application.top_level_tasks.include?('release')
File.open('REVISION', 'w') { |f| f.puts Haml.version[:rev] }
elsif Rake.application.top_level_tasks.include?('release')
File.open('REVISION', 'w') { |f| f.puts "(release)" }
else
File.open('REVISION', 'w') { |f| f.puts "(unknown)" }
end
end
Rake::Task[:package].prerequisites.insert(0, :revision_file)
# We also need to get rid of this file after packaging.
at_exit { File.delete('REVISION') rescue nil }
desc "Install Haml as a gem."
task :install => [:package] do
sudo = RUBY_PLATFORM =~ /win32/ ? '' : 'sudo'
sh %{#{sudo} gem install --no-ri pkg/haml-#{File.read('VERSION').strip}}
end
desc "Release a new Haml package to Rubyforge. Requires the NAME and VERSION flags."
task :release => [:package] do
name, version = ENV['NAME'], ENV['VERSION']
raise "Must supply NAME and VERSION for release task." unless name && version
sh %{rubyforge login}
sh %{rubyforge add_release haml haml "#{name} (v#{version})" pkg/haml-#{version}.gem}
sh %{rubyforge add_file haml haml "#{name} (v#{version})" pkg/haml-#{version}.tar.gz}
sh %{rubyforge add_file haml haml "#{name} (v#{version})" pkg/haml-#{version}.tar.bz2}
sh %{rubyforge add_file haml haml "#{name} (v#{version})" pkg/haml-#{version}.zip}
end
# ----- Documentation -----
begin
require 'hanna/rdoctask'
rescue LoadError
require 'rake/rdoctask'
end
Rake::RDocTask.new do |rdoc|
rdoc.title = 'Haml/Sass'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include(*FileList.new('*') do |list|
list.exclude(/(^|[^.a-z])[a-z]+/)
list.exclude('TODO')
end.to_a)
rdoc.rdoc_files.include('lib/**/*.rb')
rdoc.rdoc_files.exclude('TODO')
rdoc.rdoc_files.exclude('lib/haml/buffer.rb')
rdoc.rdoc_files.exclude('lib/sass/tree/*')
rdoc.rdoc_dir = 'rdoc'
rdoc.main = 'README.rdoc'
end
# ----- Coverage -----
begin
require 'rcov/rcovtask'
Rcov::RcovTask.new do |t|
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
rescue LoadError; end
# ----- Profiling -----
begin
require 'ruby-prof'
desc <<END
Run a profile of haml.
ENGINE=str sets the engine to be profiled. Defaults to Haml.
TIMES=n sets the number of runs. Defaults to 1000.
FILE=str sets the file to profile.
Defaults to 'standard' for Haml and 'complex' for Sass.
OUTPUT=str sets the ruby-prof output format.
Can be Flat, CallInfo, or Graph. Defaults to Flat. Defaults to Flat.
END
task :profile do
engine = (ENV['ENGINE'] || 'haml').downcase
times = (ENV['TIMES'] || '1000').to_i
file = ENV['FILE']
if engine == 'sass'
require 'lib/sass'
file = File.read("#{File.dirname(__FILE__)}/test/sass/templates/#{file || 'complex'}.sass")
result = RubyProf.profile { times.times { Sass::Engine.new(file).render } }
else
require 'lib/haml'
file = File.read("#{File.dirname(__FILE__)}/test/haml/templates/#{file || 'standard'}.haml")
obj = Object.new
Haml::Engine.new(file).def_method(obj, :render)
result = RubyProf.profile { times.times { obj.render } }
end
RubyProf.const_get("#{(ENV['OUTPUT'] || 'Flat').capitalize}Printer").new(result).print
end
rescue LoadError; end
# ----- Testing Multiple Rails Versions -----
rails_versions = [
"v2.3.0",
"v2.2.2",
"v2.1.2",
"v2.0.5"
]
namespace :test do
desc "Test all supported versions of rails. This takes a while."
task :rails_compatibility do
`rm -rf test/rails`
puts "Checking out rails. Please wait."
`git clone git://github.com/rails/rails.git test/rails` rescue nil
begin
rails_versions.each do |version|
Dir.chdir "test/rails" do
`git checkout #{version}`
end
puts "Testing Rails #{version}"
Rake::Task['test'].reenable
Rake::Task['test'].execute
end
ensure
`rm -rf test/rails`
end
end
end
= render :layout => 'layout' do
During
Before
During
After
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 ==(other)
self.class == other.class && rules == other.rules && super
end
def rules
Array(rule)
end
def add_rules(node)
self.rule = rules
self.rule += node.rules
end
def continued?
rule[-1] == ?,
end
def to_s(tabs, super_rules = nil)
attributes = []
sub_rules = []
rule_split = /\s*,\s*/
rule_separator = @style == :compressed ? ',' : ', '
line_separator = [:nested, :expanded].include?(@style) ? ",\n" : rule_separator
rule_indent = ' ' * (tabs - 1)
total_rule = if super_rules
super_rules.split(",\n").map do |super_line|
super_line.strip.split(rule_split).map do |super_rule|
self.rules.map do |line|
rule_indent + line.gsub(/,$/, '').split(rule_split).map do |rule|
if rule.include?(PARENT)
rule.gsub(PARENT, super_rule)
else
"#{super_rule} #{rule}"
end
end.join(rule_separator)
end.join(line_separator)
end.join(rule_separator)
end.join(line_separator)
elsif self.rules.any? { |r| r.include?(PARENT) }
raise Sass::SyntaxError.new("Base-level rules cannot contain the parent-selector-referencing character '#{PARENT}'.", line)
else
per_rule_indent, total_indent = [:nested, :expanded].include?(@style) ? [rule_indent, ''] : ['', rule_indent]
total_indent + self.rules.map do |r|
per_rule_indent + r.gsub(/,$/, '').gsub(rule_split, rule_separator).rstrip
end.join(line_separator)
end
children.each do |child|
if child.is_a? RuleNode
sub_rules << child
else
attributes << child
end
end
to_return = ''
if !attributes.empty?
old_spaces = ' ' * (tabs - 1)
spaces = ' ' * tabs
if @style == :compact
attributes = attributes.map { |a| a.to_s(1) }.join(' ')
to_return << "#{total_rule} { #{attributes} }\n"
elsif @style == :compressed
attributes = attributes.map { |a| a.to_s(1) }.join(';')
to_return << "#{total_rule}{#{attributes}}"
else
attributes = attributes.map { |a| a.to_s(tabs + 1) }.join("\n")
end_attrs = (@style == :expanded ? "\n" + old_spaces : ' ')
to_return << "#{total_rule} {\n#{attributes}#{end_attrs}}\n"
end
end
tabs += 1 unless attributes.empty? || @style != :nested
sub_rules.each do |sub|
to_return << sub.to_s(tabs, total_rule)
end
to_return
end
end
end
#!/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!
;;; sass-mode.el -- Major mode for editing Sass files
;;; Written by Nathan Weizenbaum
;;; Because Sass's indentation schema is similar
;;; to that of YAML and Python, many indentation-related
;;; functions are similar to those in yaml-mode and python-mode.
;;; To install, save this somewhere and add the following to your .emacs file:
;;;
;;; (add-to-list 'load-path "/path/to/sass-mode.el")
;;; (require 'sass-mode nil 't)
;;; (add-to-list 'auto-mode-alist '("\\.sass$" . sass-mode))
;;;
;;; Code:
(require 'haml-mode)
;; User definable variables
(defgroup sass nil
"Support for the Sass template language."
:group 'languages
:prefix "sass-")
(defcustom sass-mode-hook nil
"Hook run when entering Sass mode."
:type 'hook
:group 'sass)
(defcustom sass-indent-offset 2
"Amount of offset per level of indentation."
:type 'integer
:group 'sass)
(defvar sass-non-block-openers
'("^ *:[^ \t]+[ \t]+[^ \t]"
"^ *[^ \t:]+[ \t]*[=:][ \t]*[^ \t]")
"A list of regexps that match lines of Sass that couldn't have
text nested beneath them.")
;; Font lock
(defconst sass-font-lock-keywords
'(("^ *\\(\t\\)" 1 'haml-tab-face)
("^@.*" 0 font-lock-constant-face)
("\\(\'[^']*'\\)" 1 font-lock-string-face append)
("\\(\"[^\"]*\"\\)" 1 font-lock-string-face append)
("\\(#[0-9a-fA-F]\\{3\\}\\{1,2\\}\\>\\)" 1 font-lock-string-face append)
("\\(:[A-Za-z-]+\\|[A-Za-z-]+:\\)" 0 font-lock-constant-face append)
("![a-z0-9_-]+" 0 font-lock-variable-name-face append)
("^ *\\(/[/*].*\\)$" 1 font-lock-comment-face append)
("\\(?:^\\|,\\) *\\(#[a-z0-9_-]+\/?\\)" 1 font-lock-keyword-face)
("\\(?:^\\|,\\) *\\(\\.[a-z0-9_-]+\/?\\)" 1 font-lock-type-face)
("\\(?:^\\|,\\) *\\(&\\|[a-z0-9_]+\/?\\)" 1 font-lock-function-name-face)
("\\([=]\\)" 0 font-lock-preprocessor-face prepend)
("\\(?:^\\|,\\) *\\(#[a-z0-9_]+\/?\\)" (1 font-lock-keyword-face)
("\\.[a-z0-9_-]+" nil nil (0 font-lock-type-face)))
("\\(?:^\\|,\\) *\\(\\.[a-z0-9_]+\/?\\)" (1 font-lock-type-face)
("\\.[a-z0-9_-]+" nil nil (0 font-lock-type-face)))
("\\(?:^\\|,\\) *\\(\\.[a-z0-9_]+\/?\\)" (1 font-lock-type-face)
("\\#[a-z0-9_-]+" nil nil (0 font-lock-keyword-face)))
("\\(?:^\\|,\\) *\\(&\\|[a-z0-9_]+\/?\\)" (1 font-lock-function-name-face)
("\\.[a-z0-9_-]+" nil nil (0 font-lock-type-face)))
("\\(?:^\\|,\\) *\\(&\\|[a-z0-9_]+\/?\\)" (1 font-lock-function-name-face)
("\\#[a-z0-9_-]+" nil nil (0 font-lock-keyword-face)))))
;; Constants
;; Mode setup
;;;###autoload
(define-derived-mode sass-mode haml-mode "Sass"
"Major mode for editing Sass files."
(set (make-local-variable 'haml-indent-function) 'sass-indent-p)
(set (make-local-variable 'haml-indent-offset) sass-indent-offset)
(setq font-lock-defaults '(sass-font-lock-keywords nil t)))
;; Indentation
(defun sass-indent-p ()
"Returns true if the current line can have lines nested beneath it."
(loop for opener in sass-non-block-openers
unless (looking-at opener) return t
return nil))
;; Setup/Activation
(provide 'sass-mode)
dir = File.dirname(__FILE__)
$LOAD_PATH.unshift dir unless $LOAD_PATH.include?(dir)
require 'haml/version'
# = 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 three ways:
# as a plugin for Ruby on Rails,
# as a standalone Ruby module,
# and as a command-line tool.
# 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 "merb-haml"
#
# to config/dependencies.rb.
#
# Sass templates in Rails 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.
#
# To run Sass from the command line, just use
#
# sass input.sass output.css
#
# Use <tt>sass --help</tt> for full documentation.
#
# 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>
# ...
#
# Like CSS, you can stretch rules over multiple lines.
# However, unlike CSS, you can only do this if each line but the last
# ends with a comma.
# For example:
#
# .users #userTab,
# .posts #postsTab
# <attributes>
#
# === 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% }
#
# By default, either attribute syntax may be used.
# If you want to force one or the other,
# see the <tt>:attribute_syntax</tt> option below.
#
# === 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; }
#
# === Rule Escaping
#
# In case, for whatever reason, you need to write a rule
# that begins with a Sass-meaningful character,
# you can escape it with a backslash (<tt>\</tt>).
# For example:
#
# #main
# \+div
# clear: both
#
# is compiled to:
#
# #main +div {
# clear: both; }
#
# == 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.) }
#
# === Optional Assignment
#
# You can assign Sass constants if they aren't already assigned
# using the ||= assignment operator.
# This means that if the constant has already been assigned to,
# it won't be re-assigned,
# but if it doesn't have a value yet,
# it will be given one.
# For example:
#
# !content = "First content"
# !content ||= "Second content?"
#
# #main
# content = content
#
# is compiled to:
#
# #main {
# content: First content; }
#
# However,
#
# !content ||= "Second content?"
#
# #main
# content = content
#
# is compiled to:
#
# #main {
# content: Second content?; }
#
# === 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.
#
# === @font-face, @media, etc.
#
# Sass behaves as you'd expect for normal CSS @-directives.
# For example:
#
# @font-face
# font-family: "Bitstream Vera Sans"
# src: url(http://foo.bar/bvs")
#
# compiles to:
#
# @font-face {
# font-family: "Bitstream Vera Sans";
# src: url(http://foo.bar/bvs"); }
#
# and
#
# @media print
# #sidebar
# display: none
#
# #main
# background-color: white
#
# compiles to:
#
# @media print {
# #sidebar {
# display: none; }
#
# #main {
# background-color: white; }
# }
#
# == 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; }
#
# You can also nest text beneath a comment to comment out a whole block.
# For example:
#
# // A very awesome rule
# #awesome.rule
# // Don't use these attributes
# color: green
# font-size: 10em
# color: red
#
# becomes
#
# #awesome.rule {
# color: red; }
#
# === 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; }
#
# You can also nest content beneath loud comments. For example:
#
# #pbj
# /* This rule describes
# the styling of the element
# that represents
# a peanut butter and jelly sandwich.
# :background-image url(/images/pbj.png)
# :color red
#
# becomes
#
# #pbj {
# /* This rule describes
# * the styling of the element
# * that represents
# * a peanut butter and jelly sandwich. */
# background-image: url(/images/pbj.png);
# color: red; }
#
# == Mixins
#
# Mixins enable you to define groups of CSS attributes and
# then include them inline in any number of selectors
# throughout the document.
#
# === Defining a Mixin
#
# To define a mixin you use a slightly modified form of selector syntax.
# For example the 'large-text' mixin is defined as follows:
#
# =large-text
# :font
# :family Arial
# :size 20px
# :weight bold
# :color #ff0000
#
# The initial '=' marks this as a mixin rather than a standard selector.
# The CSS rules that follow won't be included until the mixin is referenced later on.
# Anything you can put into a standard selector,
# you can put into a mixin definition. e.g.
#
# =clearfix
# display: inline-block
# &:after
# content: "."
# display: block
# height: 0
# clear: both
# visibility: hidden
# * html &
# height: 1px
#
#
# === Mixing it in
#
# Inlining a defined mixin is simple,
# just prepend a '+' symbol to the name of a mixin defined earlier in the document.
# So to inline the 'large-text' defined earlier,
# we include the statment '+large-text' in our selector definition thus:
#
# .page-title
# +large-text
# :padding 4px
# :margin
# :top 10px
#
#
# This will produce the following CSS output:
#
# .page-title {
# font-family: Arial;
# font-size: 20px;
# font-weight: bold;
# color: #ff0000;
# padding: 4px;
# margin-top: 10px;
# }
#
# Any number of mixins may be defined and there is no limit on
# the number that can be included in a particular selector.
#
# Mixin definitions can also include references to other mixins defined earlier in the file.
# E.g.
#
# =highlighted-background
# background:
# color: #fc0
# =header-text
# font:
# size: 20px
#
# =compound
# +highlighted-background
# +header-text
#
#
# == 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; }
#
# === <tt>:compressed</tt>
#
# Compressed style takes up the minimum amount of space possible,
# having no whitespace except that necessary to separate selectors
# and a newline at the end of the file.
# It's not meant to be human-readable.
# 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 <tt>Sass::Plugin.options</tt> hash
# in <tt>environment.rb</tt> in Rails...
#
# Sass::Plugin.options[:style] = :compact
#
# ...or by setting the <tt>Merb::Plugin.config[:sass]</tt> hash in <tt>init.rb</tt> in Merb...
#
# Merb::Plugin.config[:sass][:style] = :compact
#
# ...or by passing an options hash to Sass::Engine.new.
# Available options are:
#
# [<tt>:style</tt>] Sets the style of the CSS output.
# See the section on Output Style, above.
#
# [<tt>:attribute_syntax</tt>] Forces the document to use one syntax for attributes.
# If the correct syntax isn't used, an error is thrown.
# <tt>:normal</tt> forces the use of a colon
# before the attribute name.
# For example: <tt>:color #0f3</tt>
# or <tt>:width = !main_width</tt>.
# <tt>:alternate</tt> forces the use of a colon or equals sign
# after the attribute name.
# For example: <tt>color: #0f3</tt>
# or <tt>width = !main_width</tt>.
# By default, either syntax is valid.
#
# [<tt>:never_update</tt>] Whether the CSS files should never be updated,
# even if the template file changes.
# Setting this to true may give small performance gains.
# It always defaults to false.
# Only has meaning within Ruby on Rails or Merb.
#
# [<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 in production mode, true otherwise.
# Only has meaning within Ruby on Rails or Merb.
#
# [<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
extend Haml::Version
# A string representing the version of Sass.
# A more fine-grained representation is available from Sass.version.
VERSION = version[:string] unless defined?(Sass::VERSION)
end
require 'haml/util'
require 'sass/engine'
require 'sass/plugin' if defined?(Merb::Plugins)
%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
<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>
!!!
%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!
.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|" }.join
= [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"
= "foo".each_line do |line|
- nil
.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"
self << '<!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') do
head do
title "Hampton Catlin Is Totally Awesome"
meta("http-equiv" => "Content-Type", :content => "text/html; charset=utf-8")
end
body do
# You're In my house now!
div :class => "header" do
self << %|Yes, ladies and gentileman. He is just that egotistical.
Fantastic! This should be multi-line output
The question is if this would translate! Ahah!|
self << 1 + 9 + 8 + 2 #numbers should work and this should be ignored
end
div(:id => "body") { self << "Quotes should be loved! Just like people!"}
120.times do |number|
number
end
self << "Wow.|"
p do
self << "Holy cow " +
"multiline " +
"tags! " +
"A pipe (|) even!"
self << [1, 2, 3].collect { |n| "PipesIgnored|" }
self << [1, 2, 3].collect { |n|
n.to_s
}.join("|")
end
div(:class => "silent") do
foo = String.new
foo << "this"
foo << " shouldn't"
foo << " evaluate"
self << foo + " but now it should!"
# Woah crap a comment!
end
# That was a line that shouldn't close everything.
ul(:class => "really cool") do
('a'..'f').each do |a|
li a
end
end
div((@should_eval = "with this text"), :id => "combo", :class => "of_divs_with_underscore")
[ 104, 101, 108, 108, 111 ].map do |byte|
byte.chr
end
div(:class => "footer") do
strong("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", :class => "shout")
end
end
end
<!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! -->
<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|" }.join %>
<%= [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>
<%= "foo".each_line do |line|
nil
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>
<!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! -->
<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>
foo
<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>
!!!
%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!
.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|" }.join
= [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"
= "foo".each_line do |line|
- nil
.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"
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 funcall(other)
Sass::Constant::String.from_value("#{self.to_s}(#{other.to_s})")
end
def to_s
@value
end
end
end
#subdir { font-size: 20px; font-weight: bold; }
#subdir
:font
:size 20px
:weight bold
%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 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>
require 'haml/engine'
module Haml
class Template
class << self
@@options = {}
# Gets various options for Haml. See README.rdoc for details.
def options
@@options
end
# Sets various options for Haml. See README.rdoc for details.
def options=(value)
@@options = value
end
end
end
end
# Decide how we want to load Haml into Rails.
# Patching was necessary for versions <= 2.0.1,
# but we can make it a normal handler for higher versions.
if defined?(ActionView::TemplateHandler)
require 'haml/template/plugin'
else
require 'haml/template/patch'
end
if defined?(RAILS_ROOT)
# Update init.rb to the current version
# if it's out of date.
#
# We can probably remove this as of v1.9,
# because the new init file is sufficiently flexible
# to not need updating.
rails_init_file = File.join(RAILS_ROOT, 'vendor', 'plugins', 'haml', 'init.rb')
haml_init_file = Haml.scope('init.rb')
begin
if File.exists?(rails_init_file)
require 'fileutils'
FileUtils.cp(haml_init_file, rails_init_file) unless FileUtils.cmp(rails_init_file, haml_init_file)
end
rescue SystemCallError
warn <<END
HAML WARNING:
#{rails_init_file} is out of date and couldn't be automatically updated.
Please run `haml --rails #{File.expand_path(RAILS_ROOT)}' to update it.
END
end
end
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../test_helper'
require 'haml/template'
require 'sass/plugin'
require File.dirname(__FILE__) + '/mocks/article'
require 'action_pack/version'
module Haml::Filters::Test
include Haml::Filters::Base
def render(text)
"TESTING HAHAHAHA!"
end
end
module Haml::Helpers
def test_partial(name, locals = {})
Haml::Engine.new(File.read(File.join(TemplateTest::TEMPLATE_PATH, "_#{name}.haml"))).render(self, locals)
end
end
class Egocentic
def method_missing(*args)
self
end
end
class DummyController
attr_accessor :logger
def initialize
@logger = Egocentic.new
end
def self.controller_path
''
end
end
class TemplateTest < Test::Unit::TestCase
TEMPLATE_PATH = File.join(File.dirname(__FILE__), "templates")
TEMPLATES = %w{ very_basic standard helpers
whitespace_handling original_engine list helpful
silent_script tag_parsing just_stuff partials
filters nuke_outer_whitespace nuke_inner_whitespace
render_layout }
# partial layouts were introduced in 2.0.0
TEMPLATES << 'partial_layout' unless ActionPack::VERSION::MAJOR < 2
def setup
@base = create_base
# filters template uses :sass
Sass::Plugin.options.update(:line_comments => true, :style => :compact)
end
def create_base
vars = { 'article' => Article.new, 'foo' => 'value one' }
unless Haml::Util.has?(:instance_method, ActionView::Base, :finder)
base = ActionView::Base.new(TEMPLATE_PATH, vars)
else
# Rails 2.1.0
base = ActionView::Base.new([], vars)
base.finder.append_view_path(TEMPLATE_PATH)
end
if Haml::Util.has?(:private_method, base, :evaluate_assigns)
base.send(:evaluate_assigns)
else
# Rails 2.2
base.send(:_evaluate_assigns_and_ivars)
end
# This is used by form_for.
# It's usually provided by ActionController::Base.
def base.protect_against_forgery?; false; end
base.controller = DummyController.new
base
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, &render_method)
if ActionPack::VERSION::MAJOR < 2 || ActionPack::VERSION::MINOR < 2
render_method ||= proc { |name| @base.render(name) }
else
render_method ||= proc { |name| @base.render(:file => name) }
end
load_result(name).split("\n").zip(render_method[name].split("\n")).each_with_index do |pair, line|
message = "template: #{name}\nline: #{line}"
assert_equal(pair.first, pair.last, message)
end
rescue ActionView::TemplateError => e
if e.message =~ /Can't run [\w:]+ filter; required (one of|file) ((?:'\w+'(?: or )?)+)(, but none were found| not found)/
puts "\nCouldn't require #{$2}; skipping a test."
else
raise e
end
end
def test_empty_render_should_remain_empty
assert_equal('', render(''))
end
TEMPLATES.each do |template|
define_method "test_template_should_render_correctly [template: #{template}] " do
assert_renders_correctly template
end
end
def test_templates_should_render_correctly_with_render_proc
assert_renders_correctly("standard") do |name|
engine = Haml::Engine.new(File.read(File.dirname(__FILE__) + "/templates/#{name}.haml"))
engine.render_proc(@base).call
end
end
def test_templates_should_render_correctly_with_def_method
assert_renders_correctly("standard") do |name|
engine = Haml::Engine.new(File.read(File.dirname(__FILE__) + "/templates/#{name}.haml"))
engine.def_method(@base, "render_standard")
@base.render_standard
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_haml_options
Haml::Template.options = { :suppress_eval => true }
assert_equal({ :suppress_eval => true }, Haml::Template.options)
old_base, @base = @base, create_base
assert_renders_correctly("eval_suppressed")
@base = old_base
Haml::Template.options = {}
end
def test_exceptions_should_work_correctly
begin
render("- raise 'oops!'")
rescue Exception => e
assert_equal("oops!", e.message)
assert_match(/^\(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_match(/^\(haml\):5/, e.backtrace[0])
else
assert false
end
end
end
lib_dir = File.dirname(__FILE__) + '/../lib'
require File.dirname(__FILE__) + '/linked_rails'
require 'test/unit'
$:.unshift lib_dir unless $:.include?(lib_dir)
require 'haml'
require 'sass'
# required because of Sass::Plugin
unless defined? RAILS_ROOT
RAILS_ROOT = '.'
MERB_ENV = RAILS_ENV = 'testing'
end
require 'sinatra'
require 'dm-core'
# Heroku has a limited number of gems installed, and chance is that you need
# some additional gems, like haml. The trick is to vendor them with your app.
# First, add this snippet, which will add vendor/*/lib to your load path:
Dir['vendor/*'].each do |lib|
$:.unshift(File.join(File.dirname(__FILE__), lib, 'lib'))
end
# Next, unpack all the gems you need in vendor:
#
# $ mkdir vendor
# $ cd vendor/
# $ gem unpack haml
#
# And finally require it (which isn't really necesarry in this case, since
# Sinatra does it for you, but for sake of the example we'll do it):
require 'haml'
# Make fuckin' sure your DataMapper models are defined *before* the configure
# block, otherwise your DB won't be updated and you're in for trouble and
# what-not.
class Todo
include DataMapper::Resource
property :id, Integer, :serial => true
property :text, String
end
configure do
# Heroku has some valuable information in the environment variables.
# DATABASE_URL is a complete URL for the Postgres database that Heroku
# provides for you, something like: postgres://user:password@host/db, which
# is what DM wants. This is also a convenient check wether we're in production
# / not.
DataMapper.setup(:default, (ENV["DATABASE_URL"] || "sqlite3:///#{Dir.pwd}/db.sqlite3"))
DataMapper.auto_upgrade!
end
get '/' do
@todos = Todo.all
haml :index
end
post '/' do
Todo.create(:text => params['todo'])
redirect '/'
end
__END__
@@ index
!!!
%html
%head
%title Toodeloo
%body
%h1 Toodeloo
%ul
- @todos.each do |todo|
%li= todo.text
%form{:action => '/', :method => 'POST'}
%input{:type => 'text', :name => 'todo'}
%input{:type => 'submit', :name => 'Todo!'}
%a{:href => 'http://gist.github.com/68277'} Read more..
module Haml
module Util
class << self; include Haml::Util; end
RUBY_VERSION = ::RUBY_VERSION.split(".").map {|s| s.to_i}
def ruby1_8?
Haml::Util::RUBY_VERSION[0] == 1 && Haml::Util::RUBY_VERSION[1] < 9
end
def has?(attr, klass, method)
klass.send("#{attr}s").include?(ruby1_8? ? method.to_s : method.to_sym)
end
def each_char(str, &block)
if ruby1_8?
str.each_byte(&block)
else
str.each_char(&block)
end
end
end
end
require 'sass/tree/node'
module Sass::Tree
class ValueNode < Node
attr_accessor :value
def initialize(value, style)
@value = value
super(style)
end
def ==(other)
self.class == other.class && value == other.value && super
end
def to_s(tabs = 0)
value
end
end
end
module Haml
module Version
# Returns a hash representing the version of Haml.
# The :major, :minor, and :teeny keys have their respective numbers.
# The :string key contains a human-readable string representation of the version.
# If Haml is checked out from Git,
# the :rev key will have the revision hash.
def version
return @@version if defined?(@@version)
numbers = File.read(scope('VERSION')).strip.split('.').map { |n| n.to_i }
@@version = {
:major => numbers[0],
:minor => numbers[1],
:teeny => numbers[2]
}
@@version[:string] = [:major, :minor, :teeny].map { |comp| @@version[comp] }.compact.join('.')
if File.exists?(scope('REVISION'))
rev = File.read(scope('REVISION')).strip
rev = nil if rev !~ /^([a-f0-9]+|\(.*\))$/
end
if (rev.nil? || rev == '(unknown)') && File.exists?(scope('.git/HEAD'))
rev = File.read(scope('.git/HEAD')).strip
if rev =~ /^ref: (.*)$/
rev = File.read(scope(".git/#{$1}")).strip
end
end
if rev
@@version[:rev] = rev
unless rev[0] == ?(
@@version[:string] << "."
@@version[:string] << rev[0...7]
end
end
@@version
end
# Returns the path of file relative to the Haml root.
def scope(file) # :nodoc:
File.expand_path File.join(File.dirname(__FILE__), '..', '..', file)
end
end
end
!!!
%html
%head
%body
<!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>
#whitespace_test
= test_partial "text_area", :value => "Oneline"
= test_partial "text_area", :value => "Two\nlines"
~ test_partial "text_area", :value => "Oneline"
~ test_partial "text_area", :value => "Two\nlines"
#flattened~ test_partial "text_area", :value => "Two\nlines"
.hithere
~ "Foo bar"
~ "<pre>foo bar</pre>"
~ "<pre>foo\nbar</pre>"
%p~ "<pre>foo\nbar</pre>"
%p~ "foo\nbar"
.foo
~ 13
~ "<textarea>\na\n</textarea>".each_line do |l|
- haml_concat l.strip
#whitespace_test
= test_partial "text_area", :value => "Oneline"
= test_partial "text_area", :value => "Two\nlines"
= find_and_preserve test_partial("text_area", :value => "Oneline")
= find_and_preserve test_partial("text_area", :value => "Two\nlines")
#flattened= find_and_preserve test_partial("text_area", :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
<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</textarea>
<div class='text_area_test_area'>
<textarea>Two&#x000A;lines</textarea>
</div>
<textarea>BLAH</textarea>
<div id='flattened'>
<div class='text_area_test_area'>
<textarea>Two&#x000A;lines</textarea>
</div>
<textarea>BLAH</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>
a
</textarea>
<textarea>&#x000A;a</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</textarea>
<div class='text_area_test_area'>
<textarea>Two&#x000A;lines</textarea>
</div>
<textarea>BLAH</textarea>
<div id='flattened'>
<div class='text_area_test_area'>
<textarea>Two&#x000A;lines</textarea>
</div>
<textarea>BLAH</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!</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/
<strong>indeed!</strong></textarea>
</div>
<div class='foo'>
13
</div>
<pre> __ ______ __ ______&#x000A;.----.| |--.|__ |.----.| |--..--------.| __ |&#x000A;| __|| ||__ || __|| < | || __ |&#x000A;|____||__|__||______||____||__|__||__|__|__||______|</pre>
<pre>foo
bar</pre>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment