haml 1.5.2 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of haml might be problematic. Click here for more details.

Files changed (66) hide show
  1. data/MIT-LICENSE +1 -1
  2. data/Rakefile +1 -0
  3. data/VERSION +1 -1
  4. data/bin/css2sass +7 -0
  5. data/bin/html2haml +0 -82
  6. data/lib/haml.rb +43 -6
  7. data/lib/haml/buffer.rb +81 -72
  8. data/lib/haml/engine.rb +240 -110
  9. data/lib/haml/exec.rb +120 -5
  10. data/lib/haml/helpers.rb +88 -3
  11. data/lib/haml/helpers/action_view_extensions.rb +45 -0
  12. data/lib/haml/helpers/action_view_mods.rb +30 -17
  13. data/lib/haml/html.rb +173 -0
  14. data/lib/haml/template.rb +1 -26
  15. data/lib/haml/util.rb +18 -0
  16. data/lib/sass.rb +181 -3
  17. data/lib/sass/constant.rb +38 -9
  18. data/lib/sass/constant/color.rb +25 -1
  19. data/lib/sass/constant/literal.rb +10 -8
  20. data/lib/sass/css.rb +197 -0
  21. data/lib/sass/engine.rb +239 -68
  22. data/lib/sass/error.rb +2 -2
  23. data/lib/sass/plugin.rb +11 -3
  24. data/lib/sass/tree/attr_node.rb +25 -17
  25. data/lib/sass/tree/comment_node.rb +14 -0
  26. data/lib/sass/tree/node.rb +18 -1
  27. data/lib/sass/tree/rule_node.rb +17 -5
  28. data/lib/sass/tree/value_node.rb +4 -0
  29. data/test/haml/engine_test.rb +42 -25
  30. data/test/haml/helper_test.rb +28 -3
  31. data/test/haml/results/eval_suppressed.xhtml +6 -0
  32. data/test/haml/results/helpers.xhtml +26 -2
  33. data/test/haml/results/helpful.xhtml +2 -0
  34. data/test/haml/results/just_stuff.xhtml +17 -2
  35. data/test/haml/results/standard.xhtml +1 -1
  36. data/test/haml/results/whitespace_handling.xhtml +1 -11
  37. data/test/haml/rhtml/standard.rhtml +1 -1
  38. data/test/haml/template_test.rb +7 -2
  39. data/test/haml/templates/eval_suppressed.haml +7 -2
  40. data/test/haml/templates/helpers.haml +16 -1
  41. data/test/haml/templates/helpful.haml +2 -0
  42. data/test/haml/templates/just_stuff.haml +23 -4
  43. data/test/haml/templates/standard.haml +3 -3
  44. data/test/haml/templates/whitespace_handling.haml +0 -50
  45. data/test/sass/engine_test.rb +35 -10
  46. data/test/sass/plugin_test.rb +10 -6
  47. data/test/sass/results/alt.css +4 -0
  48. data/test/sass/results/complex.css +4 -3
  49. data/test/sass/results/constants.css +3 -3
  50. data/test/sass/results/import.css +27 -0
  51. data/test/sass/results/nested.css +7 -0
  52. data/test/sass/results/parent_ref.css +13 -0
  53. data/test/sass/results/subdir/nested_subdir/nested_subdir.css +1 -0
  54. data/test/sass/results/subdir/subdir.css +1 -0
  55. data/test/sass/templates/alt.sass +16 -0
  56. data/test/sass/templates/bork2.sass +2 -0
  57. data/test/sass/templates/complex.sass +19 -1
  58. data/test/sass/templates/constants.sass +8 -0
  59. data/test/sass/templates/import.sass +8 -0
  60. data/test/sass/templates/importee.sass +10 -0
  61. data/test/sass/templates/nested.sass +8 -0
  62. data/test/sass/templates/parent_ref.sass +25 -0
  63. data/test/sass/templates/subdir/nested_subdir/nested_subdir.sass +3 -0
  64. data/test/sass/templates/subdir/subdir.sass +6 -0
  65. metadata +95 -75
  66. data/test/haml/results/semantic.cache +0 -15
@@ -23,9 +23,9 @@ module Sass
23
23
  # +filename+ should be the file in which the error occurred,
24
24
  # if applicable (defaults to "(sass)").
25
25
  def add_backtrace_entry(filename) # :nodoc:
26
- @sass_filename = filename
26
+ @sass_filename ||= filename
27
27
  self.backtrace ||= []
28
- self.backtrace.unshift "#{filename || '(sass)'}:#{@sass_line}"
28
+ self.backtrace.unshift "#{@sass_filename || '(sass)'}:#{@sass_line}"
29
29
  end
30
30
 
31
31
  def to_s # :nodoc:
@@ -37,9 +37,11 @@ module Sass
37
37
  # from <tt>options[:templates]</tt>
38
38
  # if it does.
39
39
  def update_stylesheets
40
- Dir[options[:template_location] + '/*.sass'].each do |file|
41
- name = File.basename(file)[0...-5]
40
+ Dir.glob(File.join(options[:template_location], "**", "*.sass")).entries.each do |file|
42
41
 
42
+ # Get the relative path to the file with no extension
43
+ name = file.sub(options[:template_location] + "/", "")[0...-5]
44
+
43
45
  if options[:always_update] || stylesheet_needs_update?(name)
44
46
  css = css_filename(name)
45
47
  File.delete(css) if File.exists?(css)
@@ -47,6 +49,7 @@ module Sass
47
49
  filename = template_filename(name)
48
50
  l_options = @@options.dup
49
51
  l_options[:filename] = filename
52
+ l_options[:load_paths] = (l_options[:load_paths] || []) + [l_options[:template_location]]
50
53
  engine = Engine.new(File.read(filename), l_options)
51
54
  begin
52
55
  result = engine.render
@@ -78,7 +81,12 @@ module Sass
78
81
  end
79
82
  end
80
83
 
81
- Dir.mkdir(l_options[:css_location]) unless File.exist?(l_options[:css_location])
84
+ # Create any directories that might be necessary
85
+ dirs = [l_options[:css_location]]
86
+ name.split("/")[0...-1].each { |dir| dirs << "#{dirs[-1]}/#{dir}" }
87
+ dirs.each { |dir| Dir.mkdir(dir) unless File.exist?(dir) }
88
+
89
+ # Finally, write the file
82
90
  File.open(css, 'w') do |file|
83
91
  file.print(result)
84
92
  end
@@ -10,29 +10,31 @@ module Sass::Tree
10
10
  end
11
11
 
12
12
  def to_s(parent_name = nil)
13
- if name[-1] == ?: || value[-1] == ?;
13
+ if value[-1] == ?;
14
14
  raise Sass::SyntaxError.new("Invalid attribute: #{declaration.dump} (This isn't CSS!)", @line)
15
15
  end
16
16
  real_name = name
17
17
  real_name = "#{parent_name}-#{real_name}" if parent_name
18
- if children.size > 0
19
- to_return = String.new
20
- children.each do |kid|
21
- if @style == :compact
22
- to_return << "#{kid.to_s(real_name)} "
23
- else
24
- to_return << "#{kid.to_s(real_name)}\n"
25
- end
26
- end
27
- to_return << "\n" unless @style == :compact
28
- to_return[0...-1]
29
- else
30
- if value.length < 1
31
- raise Sass::SyntaxError.new("Invalid attribute: #{declaration.dump}", @line)
32
- end
33
18
 
34
- "#{real_name}: #{value};"
19
+ if value.empty? && children.empty?
20
+ raise Sass::SyntaxError.new("Invalid attribute: #{declaration.dump}", @line)
21
+ end
22
+
23
+ join_string = @style == :compact ? ' ' : "\n"
24
+ to_return = ''
25
+ if !value.empty?
26
+ to_return << "#{real_name}: #{value};#{join_string}"
27
+ end
28
+
29
+ children.each do |kid|
30
+ if @style == :compact
31
+ to_return << "#{kid.to_s(real_name)} "
32
+ else
33
+ to_return << "#{kid.to_s(real_name)}\n"
34
+ end
35
35
  end
36
+ to_return << "\n" unless children.empty? || @style == :compact
37
+ to_return[0...-1]
36
38
  end
37
39
 
38
40
  private
@@ -40,5 +42,11 @@ module Sass::Tree
40
42
  def declaration
41
43
  ":#{name} #{value}"
42
44
  end
45
+
46
+ def invalid_child?(child)
47
+ if !child.is_a?(AttrNode)
48
+ "Illegal nesting: Only attributes may be nested beneath attributes."
49
+ end
50
+ end
43
51
  end
44
52
  end
@@ -0,0 +1,14 @@
1
+ require 'sass/tree/node'
2
+
3
+ module Sass::Tree
4
+ class CommentNode < ValueNode
5
+ def initialize(value, style)
6
+ super(value[2..-1].strip, style)
7
+ end
8
+
9
+ def to_s(parent_name = nil)
10
+ join_string = @style == :compact ? ' ' : "\n * "
11
+ "/* #{value}#{join_string unless children.empty?}#{children.join join_string} */"
12
+ end
13
+ end
14
+ end
@@ -3,6 +3,7 @@ module Sass
3
3
  class Node
4
4
  attr_accessor :children
5
5
  attr_accessor :line
6
+ attr_accessor :filename
6
7
 
7
8
  def initialize(style)
8
9
  @style = style
@@ -10,6 +11,9 @@ module Sass
10
11
  end
11
12
 
12
13
  def <<(child)
14
+ if msg = invalid_child?(child)
15
+ raise Sass::SyntaxError.new(msg, child.line)
16
+ end
13
17
  @children << child
14
18
  end
15
19
 
@@ -20,10 +24,23 @@ module Sass
20
24
  raise SyntaxError.new('Attributes aren\'t allowed at the root of a document.', child.line)
21
25
  end
22
26
 
23
- result += "#{child.to_s(1)}\n"
27
+ begin
28
+ result += "#{child.to_s(1)}\n"
29
+ rescue SyntaxError => e
30
+ raise e
31
+ end
24
32
  end
25
33
  result[0...-1]
26
34
  end
35
+
36
+ private
37
+
38
+ # This method should be overridden by subclasses to return an error message
39
+ # if the given child node is invalid,
40
+ # and false or nil otherwise.
41
+ def invalid_child?(child)
42
+ false
43
+ end
27
44
  end
28
45
  end
29
46
  end
@@ -3,6 +3,9 @@ require 'sass/tree/attr_node'
3
3
 
4
4
  module Sass::Tree
5
5
  class RuleNode < ValueNode
6
+ # The character used to include the parent selector
7
+ PARENT = '&'
8
+
6
9
  alias_method :rule, :value
7
10
  alias_method :rule=, :value=
8
11
 
@@ -11,17 +14,25 @@ module Sass::Tree
11
14
  sub_rules = []
12
15
  total_rule = if super_rules
13
16
  super_rules.split(/,\s*/).collect! do |s|
14
- self.rule.split(/,\s*/).collect! {|r| "#{s} #{r}"}.join(", ")
17
+ self.rule.split(/,\s*/).collect do |r|
18
+ if r.include?(PARENT)
19
+ r.gsub(PARENT, s)
20
+ else
21
+ "#{s} #{r}"
22
+ end
23
+ end.join(", ")
15
24
  end.join(", ")
25
+ elsif self.rule.include?(PARENT)
26
+ raise Sass::SyntaxError.new("Base-level rules cannot contain the parent-selector-referencing character '#{PARENT}'", line)
16
27
  else
17
28
  self.rule
18
29
  end
19
30
 
20
31
  children.each do |child|
21
- if child.is_a? AttrNode
22
- attributes << child
23
- else
32
+ if child.is_a? RuleNode
24
33
  sub_rules << child
34
+ else
35
+ attributes << child
25
36
  end
26
37
  end
27
38
 
@@ -40,7 +51,8 @@ module Sass::Tree
40
51
  end
41
52
  end
42
53
 
43
- sub_rules.each { |sub| to_return << sub.to_s(tabs + 1, total_rule) }
54
+ tabs += 1 unless attributes.empty?
55
+ sub_rules.each { |sub| to_return << sub.to_s(tabs, total_rule) }
44
56
  to_return
45
57
  end
46
58
  end
@@ -8,5 +8,9 @@ module Sass::Tree
8
8
  @value = value
9
9
  super(style)
10
10
  end
11
+
12
+ def to_s(tabs = 0)
13
+ value
14
+ end
11
15
  end
12
16
  end
@@ -53,12 +53,18 @@ class EngineTest < Test::Unit::TestCase
53
53
  assert_equal("<strong>Hi there!</strong>\n", engine.to_html)
54
54
  end
55
55
 
56
+ def test_double_equals
57
+ assert_equal("<p>Hello World</p>\n", render('%p== Hello #{who}', :locals => {:who => 'World'}))
58
+ assert_equal("<p>\n Hello World\n</p>\n", render("%p\n == Hello \#{who}", :locals => {:who => 'World'}))
59
+ end
60
+
56
61
  # Options tests
57
62
 
58
63
  def test_stop_eval
59
64
  assert_equal("", render("= 'Hello'", :suppress_eval => true))
60
- assert_equal("", render("- _hamlout << 'foo'", :suppress_eval => true))
61
- assert_equal("<div id='foo' />\n", render("#foo{:yes => 'no'}/", :suppress_eval => true))
65
+ assert_equal("", render("- puts 'foo'", :suppress_eval => true))
66
+ assert_equal("<div id='foo' yes='no' />\n", render("#foo{:yes => 'no'}/", :suppress_eval => true))
67
+ assert_equal("<div id='foo' />\n", render("#foo{:yes => 'no', :call => a_function() }/", :suppress_eval => true))
62
68
  assert_equal("<div />\n", render("%div[1]/", :suppress_eval => true))
63
69
 
64
70
  begin
@@ -77,27 +83,30 @@ class EngineTest < Test::Unit::TestCase
77
83
  assert_equal("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n", render("!!! XML", :attr_wrapper => '"'))
78
84
  end
79
85
 
86
+ def test_attrs_parsed_correctly
87
+ assert_equal("<p boom=>biddly='bar => baz'>\n</p>\n", render("%p{'boom=>biddly' => 'bar => baz'}"))
88
+ assert_equal("<p foo,bar='baz, qux'>\n</p>\n", render("%p{'foo,bar' => 'baz, qux'}"))
89
+ assert_equal("<p escaped='quo\nte'>\n</p>\n", render("%p{ :escaped => \"quo\\nte\"}"))
90
+ assert_equal("<p escaped='quo4te'>\n</p>\n", render("%p{ :escaped => \"quo\#{2 + 2}te\"}"))
91
+ end
92
+
80
93
  def test_locals
81
94
  assert_equal("<p>Paragraph!</p>\n", render("%p= text", :locals => { :text => "Paragraph!" }))
82
95
  end
96
+
97
+ def test_recompile_with_new_locals
98
+ template = "%p= (text == 'first time') ? text : new_text"
99
+ assert_equal("<p>first time</p>\n", render(template, :locals => { :text => "first time" }))
100
+ assert_equal("<p>second time</p>\n", render(template, :locals => { :text => "recompile", :new_text => "second time" }))
83
101
 
84
- def test_precompiled
85
- precompiled = <<-END
86
- def _haml_render
87
- _hamlout = @haml_stack[-1]
88
- _erbout = _hamlout.buffer
89
-
90
- _hamlout.open_tag("p", 0, nil, true, "", nil, nil, false)
91
- @haml_lineno = 1
92
- haml_temp = "Haml Rocks Socks"
93
- haml_temp = _hamlout.push_script(haml_temp, 1, false)
94
- _hamlout.close_tag("p", 0)
95
- end
96
- END
102
+ # Make sure the method called will return junk unless recompiled
103
+ method_name = Haml::Engine.send(:class_variable_get, '@@method_names')[template]
104
+ Haml::Engine::CompiledTemplates.module_eval "def #{method_name}(stuff); @haml_stack[-1].push_text 'NOT RECOMPILED', 0; end"
97
105
 
98
- assert_equal("<p>Haml Rocks Socks</p>\n", render("%h1 I shall not be rendered", :precompiled => precompiled))
106
+ assert_equal("NOT RECOMPILED\n", render(template, :locals => { :text => "first time" }))
107
+ assert_equal("<p>first time</p>\n", render(template, :locals => { :text => "first time", :foo => 'bar' }))
99
108
  end
100
-
109
+
101
110
  def test_comps
102
111
  assert_equal(-1, "foo" <=> nil)
103
112
  assert_equal(1, nil <=> "foo")
@@ -105,12 +114,9 @@ class EngineTest < Test::Unit::TestCase
105
114
 
106
115
  def test_rec_merge
107
116
  hash1 = {1=>2, 3=>{5=>7, 8=>9}}
108
- hash1_2 = hash1.clone
109
117
  hash2 = {4=>5, 3=>{5=>2, 16=>12}}
110
118
  hash3 = {1=>2, 4=>5, 3=>{5=>2, 8=>9, 16=>12}}
111
119
 
112
- assert_equal(hash3, hash1.rec_merge(hash2))
113
- assert_equal(hash1_2, hash1)
114
120
  hash1.rec_merge!(hash2)
115
121
  assert_equal(hash3, hash1)
116
122
  end
@@ -131,11 +137,11 @@ class EngineTest < Test::Unit::TestCase
131
137
 
132
138
  def test_syntax_errors
133
139
  errs = [ "!!!\n a", "a\n b", "a\n:foo\nb", "/ a\n b",
134
- "% a", "%p a\n b", "a\n%p=\nb", "%p=\n a",
135
- "a\n%p~\nb", "a\n~\nb", "%p/\n a", "%p\n \t%a b",
136
- "%a\n b\nc", "%a\n b\nc",
137
- ":notafilter\n This isn't\n a filter!",
138
- ]
140
+ "% a", "%p a\n b", "a\n%p=\nb", "%p=\n a",
141
+ "a\n%p~\nb", "a\n~\nb", "a\n~\n b", "%p~\n b", "%p/\n a",
142
+ "%p\n \t%a b", "%a\n b\nc", "%a\n b\nc",
143
+ ":notafilter\n This isn't\n a filter!",
144
+ ".{} a", "\#{} a", ".= 'foo'", "%a/ b" ]
139
145
  errs.each do |err|
140
146
  begin
141
147
  render(err)
@@ -228,4 +234,15 @@ class EngineTest < Test::Unit::TestCase
228
234
 
229
235
  NOT_LOADED.delete 'redcloth'
230
236
  end
237
+
238
+ def test_local_assigns_dont_modify_class
239
+ assert_equal("bar\n", render("= foo", :locals => {:foo => 'bar'}))
240
+ assert_equal(nil, defined?(foo))
241
+ end
242
+
243
+ def test_object_ref_with_nil_id
244
+ user = Struct.new('User', :id).new
245
+ assert_equal("<p class='struct_user' id='struct_user_new'>New User</p>\n",
246
+ render("%p[user] New User", :locals => {:user => user}))
247
+ end
231
248
  end
@@ -17,7 +17,8 @@ class HelperTest < Test::Unit::TestCase
17
17
  if options == :action_view
18
18
  @base.render :inline => text, :type => :haml
19
19
  else
20
- Haml::Engine.new(text, options).to_html
20
+ scope = options.delete :scope_object
21
+ Haml::Engine.new(text, options).to_html(scope ? scope : Object.new)
21
22
  end
22
23
  end
23
24
 
@@ -34,7 +35,7 @@ class HelperTest < Test::Unit::TestCase
34
35
 
35
36
  def test_list_of_should_render_correctly
36
37
  assert_equal("<li>1</li>\n<li>2</li>\n", render("= list_of([1, 2]) do |i|\n = i"))
37
- assert_equal("<li>1</li>\n", render("= list_of([[1]]) do |i|\n = i.first"))
38
+ assert_equal("<li>[1]</li>\n", render("= list_of([[1]]) do |i|\n = i.inspect"))
38
39
  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",
39
40
  render("= list_of(['Fee', 'Fi', 'Fo', 'Fum']) do |title|\n %h1= title\n %p A word!"))
40
41
  end
@@ -64,7 +65,7 @@ class HelperTest < Test::Unit::TestCase
64
65
  ActionView::Base.new.render(:inline => "<%= concat('foo') %>")
65
66
  rescue ArgumentError, NameError
66
67
  proper_behavior = true
67
- end
68
+ end
68
69
  assert(proper_behavior)
69
70
  end
70
71
 
@@ -106,5 +107,29 @@ class HelperTest < Test::Unit::TestCase
106
107
  assert_equal("false", @base.render(:inline => '<%= is_haml? %>'))
107
108
  assert_equal("false\n", render("= render :inline => '<%= is_haml? %>'", :action_view))
108
109
  end
110
+
111
+ def test_page_class
112
+ controller = Struct.new(:controller_name, :action_name).new('troller', 'tion')
113
+ scope = Struct.new(:controller).new(controller)
114
+ result = render("%div{:class => page_class} MyDiv", :scope_object => scope)
115
+ expected = "<div class='troller tion'>MyDiv</div>\n"
116
+ assert_equal expected, result
117
+ end
118
+
119
+ def test_indented_capture
120
+ assert_equal(" \n Foo\n ", @base.render(:inline => " <% res = capture do %>\n Foo\n <% end %><%= res %>"))
121
+ end
122
+
123
+ def test_capture_deals_properly_with_collections
124
+ Haml::Helpers.module_eval do
125
+ def trc(collection, &block)
126
+ collection.each do |record|
127
+ puts capture_haml(record, &block)
128
+ end
129
+ end
130
+ end
131
+
132
+ assert_equal("1\n\n2\n\n3\n\n", render("- trc([1, 2, 3]) do |i|\n = i.inspect"))
133
+ end
109
134
  end
110
135
 
@@ -1,2 +1,8 @@
1
1
  <p></p>
2
2
  <h1>Me!</h1>
3
+ <div id='foo'>
4
+ <p id='bar'>All</p>
5
+ <br />
6
+ <p class='baz'>This</p>
7
+ Should render
8
+ </div>
@@ -45,9 +45,11 @@
45
45
  click
46
46
  <a href='thing'>here</a>.
47
47
  <p>baz</p>
48
- <p>boom</p>
48
+ <p>boom</p>
49
49
  foo
50
- <form action="hello/world" method="post">
50
+ <p>
51
+ <form action="hello/world" method="post">
52
+ </p>
51
53
  <form action="heeheeaform" method="post">
52
54
  <div><input name="commit" type="submit" value="save" /></div>
53
55
  </form>
@@ -58,3 +60,25 @@ foo
58
60
  <input id="article_body" name="article[body]" size="30" type="text" value="World" />
59
61
  </form>
60
62
  <li><a href='http://www.google.com'>google</a></li>
63
+ <p>
64
+ foo
65
+ <div>
66
+ bar
67
+ </div>
68
+ boom
69
+ baz
70
+ boom, again
71
+ </p>
72
+ <table>
73
+ <tr>
74
+ <td class='cell'>
75
+ <strong>
76
+ strong!
77
+ </strong>
78
+ data
79
+ </td>
80
+ <td>
81
+ more_data
82
+ </td>
83
+ </tr>
84
+ </table>
@@ -6,3 +6,5 @@
6
6
  <div class='article class' id='article_1'>class</div>
7
7
  <div class='article class' id='id_article_1'>id class</div>
8
8
  <div class='article full' id='article_1'>boo</div>
9
+ <div class='article full' id='article_1'>moo</div>
10
+ <div class='article articleFull' id='article_1'>foo</div>