haml 1.7.2 → 1.8.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 (71) hide show
  1. data/README +17 -9
  2. data/Rakefile +12 -4
  3. data/VERSION +1 -1
  4. data/init.rb +1 -6
  5. data/lib/haml.rb +65 -7
  6. data/lib/haml/buffer.rb +49 -84
  7. data/lib/haml/engine.rb +155 -797
  8. data/lib/haml/error.rb +3 -33
  9. data/lib/haml/exec.rb +86 -65
  10. data/lib/haml/filters.rb +57 -27
  11. data/lib/haml/helpers.rb +52 -9
  12. data/lib/haml/helpers/action_view_mods.rb +1 -1
  13. data/lib/haml/html.rb +20 -5
  14. data/lib/haml/precompiler.rb +671 -0
  15. data/lib/haml/template.rb +20 -73
  16. data/lib/haml/template/patch.rb +51 -0
  17. data/lib/haml/template/plugin.rb +21 -0
  18. data/lib/sass.rb +78 -3
  19. data/lib/sass/constant.rb +45 -19
  20. data/lib/sass/constant.rb.rej +42 -0
  21. data/lib/sass/constant/string.rb +4 -0
  22. data/lib/sass/css.rb +162 -39
  23. data/lib/sass/engine.rb +38 -14
  24. data/lib/sass/plugin.rb +79 -44
  25. data/lib/sass/tree/attr_node.rb +12 -11
  26. data/lib/sass/tree/comment_node.rb +9 -3
  27. data/lib/sass/tree/directive_node.rb +51 -0
  28. data/lib/sass/tree/node.rb +13 -6
  29. data/lib/sass/tree/rule_node.rb +34 -12
  30. data/test/benchmark.rb +85 -52
  31. data/test/haml/engine_test.rb +172 -84
  32. data/test/haml/helper_test.rb +31 -3
  33. data/test/haml/html2haml_test.rb +60 -0
  34. data/test/haml/markaby/standard.mab +52 -0
  35. data/test/haml/results/eval_suppressed.xhtml +4 -1
  36. data/test/haml/results/helpers.xhtml +15 -4
  37. data/test/haml/results/just_stuff.xhtml +9 -1
  38. data/test/haml/results/standard.xhtml +0 -1
  39. data/test/haml/rhtml/_av_partial_1.rhtml +12 -0
  40. data/test/haml/rhtml/_av_partial_2.rhtml +8 -0
  41. data/test/haml/rhtml/action_view.rhtml +62 -0
  42. data/test/haml/rhtml/standard.rhtml +0 -1
  43. data/test/haml/template_test.rb +41 -21
  44. data/test/haml/templates/_av_partial_1.haml +9 -0
  45. data/test/haml/templates/_av_partial_2.haml +5 -0
  46. data/test/haml/templates/action_view.haml +47 -0
  47. data/test/haml/templates/eval_suppressed.haml +1 -0
  48. data/test/haml/templates/helpers.haml +9 -3
  49. data/test/haml/templates/just_stuff.haml +10 -1
  50. data/test/haml/templates/partials.haml +1 -1
  51. data/test/haml/templates/standard.haml +0 -1
  52. data/test/profile.rb +2 -2
  53. data/test/sass/engine_test.rb +113 -3
  54. data/test/sass/engine_test.rb.rej +18 -0
  55. data/test/sass/plugin_test.rb +34 -11
  56. data/test/sass/results/compact.css +1 -1
  57. data/test/sass/results/complex.css +1 -1
  58. data/test/sass/results/compressed.css +1 -0
  59. data/test/sass/results/constants.css +3 -1
  60. data/test/sass/results/expanded.css +2 -1
  61. data/test/sass/results/import.css +2 -0
  62. data/test/sass/results/nested.css +2 -1
  63. data/test/sass/templates/_partial.sass +2 -0
  64. data/test/sass/templates/compact.sass +2 -0
  65. data/test/sass/templates/complex.sass +1 -0
  66. data/test/sass/templates/compressed.sass +15 -0
  67. data/test/sass/templates/constants.sass +9 -0
  68. data/test/sass/templates/expanded.sass +2 -0
  69. data/test/sass/templates/import.sass +1 -1
  70. data/test/sass/templates/nested.sass +2 -0
  71. metadata +22 -2
@@ -0,0 +1,42 @@
1
+ ***************
2
+ *** 106,121 ****
3
+ end
4
+
5
+ # Time for a unary minus!
6
+ - if negative_okay && symbol == :minus
7
+ - negative_okay = true
8
+ to_return << :neg
9
+ next
10
+ end
11
+
12
+ # Are we looking at an operator?
13
+ if symbol && (str.empty? || symbol != :mod)
14
+ str = reset_str.call
15
+ - negative_okay = true
16
+ to_return << symbol
17
+ next
18
+ end
19
+ --- 107,129 ----
20
+ end
21
+
22
+ # Time for a unary minus!
23
+ + if beginning_of_token && symbol == :minus
24
+ + beginning_of_token = true
25
+ to_return << :neg
26
+ next
27
+ end
28
+
29
+ + # Is this a constant?
30
+ + if beginning_of_token && symbol == :const
31
+ + beginning_of_token = true
32
+ + to_return << :const
33
+ + next
34
+ + end
35
+ +
36
+ # Are we looking at an operator?
37
+ if symbol && (str.empty? || symbol != :mod)
38
+ str = reset_str.call
39
+ + beginning_of_token = true
40
+ to_return << symbol
41
+ next
42
+ end
@@ -10,6 +10,10 @@ module Sass::Constant # :nodoc:
10
10
  def plus(other)
11
11
  Sass::Constant::String.from_value(self.to_s + other.to_s)
12
12
  end
13
+
14
+ def funcall(other)
15
+ Sass::Constant::String.from_value("#{self.to_s}(#{other.to_s})")
16
+ end
13
17
 
14
18
  def to_s
15
19
  @value
@@ -41,20 +41,52 @@ module Sass
41
41
  end
42
42
  end
43
43
  end
44
+
45
+ # This class is based on the Ruby 1.9 ordered hashes.
46
+ # It keeps the semantics and most of the efficiency of normal hashes
47
+ # while also keeping track of the order in which elements were set.
48
+ class OrderedHash
49
+ Node = Struct.new('Node', :key, :value, :next)
50
+ include Enumerable
51
+
52
+ def initialize
53
+ @hash = {}
54
+ end
55
+
56
+ def [](key)
57
+ @hash[key] && @hash[key].value
58
+ end
59
+
60
+ def []=(key, value)
61
+ node = Node.new(key, value, nil)
62
+ if @first.nil?
63
+ @first = @last = node
64
+ else
65
+ @last.next = node
66
+ @last = node
67
+ end
68
+ @hash[key] = node
69
+ value
70
+ end
71
+
72
+ def each
73
+ return unless @first
74
+ yield [@first.key, @first.value]
75
+ node = @first
76
+ yield [node.key, node.value] while node = node.next
77
+ self
78
+ end
79
+
80
+ def values
81
+ self.map { |k, v| v }
82
+ end
83
+ end
84
+
44
85
  # :startdoc:
45
86
 
46
87
  # This class contains the functionality used in the +css2sass+ utility,
47
88
  # namely converting CSS documents to Sass templates.
48
89
  class CSS
49
- # :stopdoc:
50
-
51
- # The Regexp matching a CSS rule
52
- RULE_RE = /\s*([^\{]+)\s*\{/
53
-
54
- # The Regexp matching a CSS attribute
55
- ATTR_RE = /\s*[^::\{\}]+\s*:\s*[^:;\{\}]+\s*;/
56
-
57
- # :startdoc:
58
90
 
59
91
  # Creates a new instance of Sass::CSS that will compile the given document
60
92
  # to a Sass string when +render+ is called.
@@ -84,9 +116,12 @@ module Sass
84
116
  def build_tree
85
117
  root = Tree::Node.new(nil)
86
118
  whitespace
87
- directives(root)
88
- rules(root)
89
- sort_rules(root)
119
+ directives root
120
+ rules root
121
+ expand_commas root
122
+ nest_rules root
123
+ flatten_rules root
124
+ fold_commas root
90
125
  root
91
126
  end
92
127
 
@@ -131,11 +166,11 @@ module Sass
131
166
  assert_match /:/
132
167
 
133
168
  value = ''
134
- while @template.scan(/[^;\s]+/)
169
+ while @template.scan(/[^;\s\}]+/)
135
170
  value << @template[0] << whitespace
136
171
  end
137
172
 
138
- assert_match /;/
173
+ assert_match /(;|(?=\}))/
139
174
  rule << Tree::AttrNode.new(name, value, nil)
140
175
  end
141
176
 
@@ -161,37 +196,125 @@ module Sass
161
196
  whitespace
162
197
  end
163
198
 
164
- def sort_rules(root)
165
- root.children.sort! do |c1, c2|
166
- if c1.is_a?(Tree::RuleNode) && c2.is_a?(Tree::RuleNode)
167
- c1.rule <=> c2.rule
168
- elsif !(c1.is_a?(Tree::RuleNode) || c2.is_a?(Tree::RuleNode)) || c2.is_a?(Tree::RuleNode)
169
- -1
199
+ # Transform
200
+ #
201
+ # foo, bar, baz
202
+ # color: blue
203
+ #
204
+ # into
205
+ #
206
+ # foo
207
+ # color: blue
208
+ # bar
209
+ # color: blue
210
+ # baz
211
+ # color: blue
212
+ #
213
+ # Yes, this expands the amount of code,
214
+ # but it's necessary to get nesting to work properly.
215
+ def expand_commas(root)
216
+ root.children.map! do |child|
217
+ next child unless Tree::RuleNode === child && child.rule.include?(',')
218
+ child.rule.split(',').map do |rule|
219
+ node = Tree::RuleNode.new(rule, nil)
220
+ node.children = child.children
221
+ node
222
+ end
223
+ end
224
+ root.children.flatten!
225
+ end
226
+
227
+ # Nest rules so that
228
+ #
229
+ # foo
230
+ # color: green
231
+ # foo bar
232
+ # color: red
233
+ # foo baz
234
+ # color: blue
235
+ #
236
+ # becomes
237
+ #
238
+ # foo
239
+ # color: green
240
+ # bar
241
+ # color: red
242
+ # baz
243
+ # color: blue
244
+ #
245
+ def nest_rules(root)
246
+ rules = OrderedHash.new
247
+ root.children.select { |c| Tree::RuleNode === c }.each do |child|
248
+ root.children.delete child
249
+ first, rest = child.rule.split(' ', 2)
250
+ rules[first] ||= Tree::RuleNode.new(first, nil)
251
+ if rest
252
+ child.rule = rest
253
+ rules[first] << child
170
254
  else
171
- 1
255
+ rules[first].children += child.children
172
256
  end
173
257
  end
174
258
 
175
- prev_rules = []
176
- prev_rule_values = []
177
- root.children.each do |child|
178
- if child.is_a? Tree::RuleNode
179
- joined_prev_values = prev_rule_values.join(' ')
180
- until prev_rules.empty? || child.rule =~ /^#{Regexp.escape(joined_prev_values)}/
181
- prev_rules.pop
182
- prev_rule_values.pop
183
- end
184
-
185
- unless prev_rules.empty?
186
- child.rule.slice!(0..(joined_prev_values.size))
187
- prev_rules[-1] << child
188
- root.children.delete child
189
- end
190
-
191
- prev_rules << child
192
- prev_rule_values << child.rule
259
+ rules.values.each { |v| nest_rules(v) }
260
+ root.children += rules.values
261
+ end
262
+
263
+ # Flatten rules so that
264
+ #
265
+ # foo
266
+ # bar
267
+ # baz
268
+ # color: red
269
+ #
270
+ # becomes
271
+ #
272
+ # foo bar baz
273
+ # color: red
274
+ #
275
+ def flatten_rules(root)
276
+ root.children.each { |child| flatten_rule(child) if child.is_a?(Tree::RuleNode) }
277
+ end
278
+
279
+ def flatten_rule(rule)
280
+ while rule.children.size == 1 && rule.children.first.is_a?(Tree::RuleNode)
281
+ child = rule.children.first
282
+ rule.rule = "#{rule.rule} #{child.rule}"
283
+ rule.children = child.children
284
+ end
285
+
286
+ flatten_rules(rule)
287
+ end
288
+
289
+ # Transform
290
+ #
291
+ # foo
292
+ # bar
293
+ # color: blue
294
+ # baz
295
+ # color: blue
296
+ #
297
+ # into
298
+ #
299
+ # foo
300
+ # bar, baz
301
+ # color: blue
302
+ #
303
+ def fold_commas(root)
304
+ prev_rule = nil
305
+ root.children.map! do |child|
306
+ next child unless Tree::RuleNode === child
307
+
308
+ if prev_rule && prev_rule.children == child.children
309
+ prev_rule.rule << ", #{child.rule}"
310
+ next nil
193
311
  end
312
+
313
+ fold_commas(child)
314
+ prev_rule = child
315
+ child
194
316
  end
317
+ root.children.compact!
195
318
  end
196
319
  end
197
320
  end
@@ -3,6 +3,7 @@ require 'sass/tree/value_node'
3
3
  require 'sass/tree/rule_node'
4
4
  require 'sass/tree/comment_node'
5
5
  require 'sass/tree/attr_node'
6
+ require 'sass/tree/directive_node'
6
7
  require 'sass/constant'
7
8
  require 'sass/error'
8
9
  require 'haml/util'
@@ -38,6 +39,9 @@ module Sass
38
39
 
39
40
  # The character used to denote a compiler directive.
40
41
  DIRECTIVE_CHAR = ?@
42
+
43
+ # Designates a non-parsed rule.
44
+ ESCAPE_CHAR = ?\\
41
45
 
42
46
  # The regex that matches and extracts data from
43
47
  # attributes of the form <tt>:name attr</tt>.
@@ -69,7 +73,7 @@ module Sass
69
73
  }.merge! options
70
74
  @template = template.split(/\n\r|\n/)
71
75
  @lines = []
72
- @constants = {}
76
+ @constants = {"important" => "!important"}
73
77
  end
74
78
 
75
79
  # Processes the template and returns the result as a string.
@@ -232,6 +236,8 @@ module Sass
232
236
  parse_comment(line)
233
237
  when DIRECTIVE_CHAR
234
238
  parse_directive(line)
239
+ when ESCAPE_CHAR
240
+ Tree::RuleNode.new(line[1..-1], @options[:style])
235
241
  else
236
242
  if line =~ ATTRIBUTE_ALTERNATE_MATCHER
237
243
  parse_attribute(line, ATTRIBUTE_ALTERNATE)
@@ -242,6 +248,14 @@ module Sass
242
248
  end
243
249
 
244
250
  def parse_attribute(line, attribute_regx)
251
+ if @options[:attribute_syntax] == :normal &&
252
+ attribute_regx == ATTRIBUTE_ALTERNATE
253
+ raise SyntaxError.new("Illegal attribute syntax: can't use alternate syntax when :attribute_syntax => :normal is set.")
254
+ elsif @options[:attribute_syntax] == :alternate &&
255
+ attribute_regx == ATTRIBUTE
256
+ raise SyntaxError.new("Illegal attribute syntax: can't use normal syntax when :attribute_syntax => :alternate is set.")
257
+ end
258
+
245
259
  name, eq, value = line.scan(attribute_regx)[0]
246
260
 
247
261
  if name.nil? || value.nil?
@@ -281,7 +295,7 @@ module Sass
281
295
  when "import"
282
296
  import(value)
283
297
  else
284
- raise SyntaxError.new("Unknown compiler directive: #{"@#{directive} #{value}".dump}", @line)
298
+ Tree::DirectiveNode.new(line, @options[:style])
285
299
  end
286
300
  end
287
301
 
@@ -290,7 +304,13 @@ module Sass
290
304
 
291
305
  files.split(/,\s*/).each do |filename|
292
306
  engine = nil
293
- filename = find_file_to_import(filename)
307
+
308
+ begin
309
+ filename = self.class.find_file_to_import(filename, @options[:load_paths])
310
+ rescue Exception => e
311
+ raise SyntaxError.new(e.message, @line)
312
+ end
313
+
294
314
  if filename =~ /\.css$/
295
315
  nodes << Tree::ValueNode.new("@import url(#{filename});", @options[:style])
296
316
  else
@@ -319,10 +339,9 @@ module Sass
319
339
  nodes
320
340
  end
321
341
 
322
- def find_file_to_import(filename)
342
+ def self.find_file_to_import(filename, load_paths)
323
343
  was_sass = false
324
344
  original_filename = filename
325
- new_filename = nil
326
345
 
327
346
  if filename[-5..-1] == ".sass"
328
347
  filename = filename[0...-5]
@@ -331,18 +350,11 @@ module Sass
331
350
  return filename
332
351
  end
333
352
 
334
- @options[:load_paths].each do |path|
335
- full_path = File.join(path, filename) + '.sass'
336
-
337
- if File.readable?(full_path)
338
- new_filename = full_path
339
- break
340
- end
341
- end
353
+ new_filename = find_full_path("#{filename}.sass", load_paths)
342
354
 
343
355
  if new_filename.nil?
344
356
  if was_sass
345
- raise SyntaxError.new("File to import not found or unreadable: #{original_filename}", @line)
357
+ raise Exception.new("File to import not found or unreadable: #{original_filename}")
346
358
  else
347
359
  return filename + '.css'
348
360
  end
@@ -350,5 +362,17 @@ module Sass
350
362
  new_filename
351
363
  end
352
364
  end
365
+
366
+ def self.find_full_path(filename, load_paths)
367
+ load_paths.each do |path|
368
+ ["_#{filename}", filename].each do |name|
369
+ full_path = File.join(path, name)
370
+ if File.readable?(full_path)
371
+ return full_path
372
+ end
373
+ end
374
+ end
375
+ nil
376
+ end
353
377
  end
354
378
  end
@@ -26,62 +26,40 @@ module Sass
26
26
  def options=(value)
27
27
  @@options.merge!(value)
28
28
  end
29
-
29
+
30
30
  # Checks each stylesheet in <tt>options[:css_location]</tt>
31
31
  # to see if it needs updating,
32
32
  # and updates it using the corresponding template
33
33
  # from <tt>options[:templates]</tt>
34
34
  # if it does.
35
35
  def update_stylesheets
36
+ return if options[:never_update]
37
+
36
38
  Dir.glob(File.join(options[:template_location], "**", "*.sass")).entries.each do |file|
37
-
39
+
38
40
  # Get the relative path to the file with no extension
39
41
  name = file.sub(options[:template_location] + "/", "")[0...-5]
40
-
41
- if options[:always_update] || stylesheet_needs_update?(name)
42
+
43
+ if !forbid_update?(name) && (options[:always_update] || stylesheet_needs_update?(name))
42
44
  css = css_filename(name)
43
45
  File.delete(css) if File.exists?(css)
44
-
46
+
45
47
  filename = template_filename(name)
46
48
  l_options = @@options.dup
47
49
  l_options[:filename] = filename
48
- l_options[:load_paths] = (l_options[:load_paths] || []) + [l_options[:template_location]]
50
+ l_options[:load_paths] = load_paths
49
51
  engine = Engine.new(File.read(filename), l_options)
50
- begin
51
- result = engine.render
52
- rescue Exception => e
53
- if options[:full_exception]
54
- e_string = "#{e.class}: #{e.message}"
55
-
56
- if e.is_a? Sass::SyntaxError
57
- e_string << "\non line #{e.sass_line}"
58
-
59
- if e.sass_filename
60
- e_string << " of #{e.sass_filename}"
61
-
62
- if File.exists?(e.sass_filename)
63
- e_string << "\n\n"
64
-
65
- min = [e.sass_line - 5, 0].max
66
- File.read(e.sass_filename).rstrip.split("\n")[
67
- min .. e.sass_line + 5
68
- ].each_with_index do |line, i|
69
- e_string << "#{min + i + 1}: #{line}\n"
70
- end
71
- end
72
- end
73
- end
74
- result = "/*\n#{e_string}\n\nBacktrace:\n#{e.backtrace.join("\n")}\n*/"
75
- else
76
- result = "/* Internal stylesheet error */"
77
- end
78
- end
79
-
52
+ result = begin
53
+ engine.render
54
+ rescue Exception => e
55
+ exception_string(e)
56
+ end
57
+
80
58
  # Create any directories that might be necessary
81
59
  dirs = [l_options[:css_location]]
82
60
  name.split("/")[0...-1].each { |dir| dirs << "#{dirs[-1]}/#{dir}" }
83
61
  dirs.each { |dir| Dir.mkdir(dir) unless File.exist?(dir) }
84
-
62
+
85
63
  # Finally, write the file
86
64
  File.open(css, 'w') do |file|
87
65
  file.print(result)
@@ -89,19 +67,76 @@ module Sass
89
67
  end
90
68
  end
91
69
  end
92
-
70
+
93
71
  private
94
-
72
+
73
+ def load_paths
74
+ (options[:load_paths] || []) + [options[:template_location]]
75
+ end
76
+
77
+ def exception_string(e)
78
+ if options[:full_exception]
79
+ e_string = "#{e.class}: #{e.message}"
80
+
81
+ if e.is_a? Sass::SyntaxError
82
+ e_string << "\non line #{e.sass_line}"
83
+
84
+ if e.sass_filename
85
+ e_string << " of #{e.sass_filename}"
86
+
87
+ if File.exists?(e.sass_filename)
88
+ e_string << "\n\n"
89
+
90
+ min = [e.sass_line - 5, 0].max
91
+ File.read(e.sass_filename).rstrip.split("\n")[
92
+ min .. e.sass_line + 5
93
+ ].each_with_index do |line, i|
94
+ e_string << "#{min + i + 1}: #{line}\n"
95
+ end
96
+ end
97
+ end
98
+ end
99
+ "/*\n#{e_string}\n\nBacktrace:\n#{e.backtrace.join("\n")}\n*/"
100
+ else
101
+ "/* Internal stylesheet error */"
102
+ end
103
+ end
104
+
95
105
  def template_filename(name)
96
- "#{@@options[:template_location]}/#{name}.sass"
106
+ "#{options[:template_location]}/#{name}.sass"
97
107
  end
98
-
108
+
99
109
  def css_filename(name)
100
- "#{@@options[:css_location]}/#{name}.css"
110
+ "#{options[:css_location]}/#{name}.css"
111
+ end
112
+
113
+ def forbid_update?(name)
114
+ name[0] == ?_
101
115
  end
102
-
116
+
103
117
  def stylesheet_needs_update?(name)
104
- !File.exists?(css_filename(name)) || (File.mtime(template_filename(name)) - 2) > File.mtime(css_filename(name))
118
+ if !File.exists?(css_filename(name))
119
+ return true
120
+ else
121
+ css_mtime = File.mtime(css_filename(name))
122
+ File.mtime(template_filename(name)) > css_mtime ||
123
+ dependencies(template_filename(name)).any?(&dependency_updated?(css_mtime))
124
+ end
125
+ end
126
+
127
+ def dependency_updated?(css_mtime)
128
+ lambda do |dep|
129
+ File.mtime(dep) > css_mtime ||
130
+ dependencies(dep).any?(&dependency_updated?(css_mtime))
131
+ end
132
+ end
133
+
134
+ def dependencies(filename)
135
+ File.readlines(filename).grep(/^@import /).map do |line|
136
+ line[8..-1].split(',').map do |inc|
137
+ Sass::Engine.find_file_to_import(inc.strip, load_paths)
138
+ end
139
+ end.flatten.grep(/\.sass$/)
105
140
  end
106
141
  end
107
142
  end