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
@@ -0,0 +1,197 @@
1
+ require File.dirname(__FILE__) + '/../sass'
2
+ require 'sass/tree/node'
3
+ require 'strscan'
4
+
5
+ module Sass
6
+ # :stopdoc:
7
+ module Tree
8
+ class Node
9
+ def to_sass
10
+ result = ''
11
+
12
+ children.each do |child|
13
+ result << "#{child.to_sass(0)}\n"
14
+ end
15
+
16
+ result
17
+ end
18
+ end
19
+
20
+ class ValueNode
21
+ def to_sass(tabs)
22
+ "#{value}\n"
23
+ end
24
+ end
25
+
26
+ class RuleNode
27
+ def to_sass(tabs)
28
+ str = "#{' ' * tabs}#{rule}\n"
29
+
30
+ children.each do |child|
31
+ str << "#{child.to_sass(tabs + 1)}"
32
+ end
33
+
34
+ str
35
+ end
36
+ end
37
+
38
+ class AttrNode
39
+ def to_sass(tabs)
40
+ "#{' ' * tabs}:#{name} #{value}\n"
41
+ end
42
+ end
43
+ end
44
+ # :startdoc:
45
+
46
+ # This class contains the functionality used in the +css2sass+ utility,
47
+ # namely converting CSS documents to Sass templates.
48
+ 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
+
59
+ # Creates a new instance of Sass::CSS that will compile the given document
60
+ # to a Sass string when +render+ is called.
61
+ def initialize(template)
62
+ if template.is_a? IO
63
+ template = template.read
64
+ end
65
+
66
+ @template = StringScanner.new(template)
67
+ end
68
+
69
+ # Processes the document and returns the result as a string
70
+ # containing the CSS template.
71
+ def render
72
+ begin
73
+ build_tree.to_sass
74
+ rescue Exception => err
75
+ line = @template.string[0...@template.pos].split("\n").size
76
+
77
+ err.backtrace.unshift "(css):#{line}"
78
+ raise err
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def build_tree
85
+ root = Tree::Node.new(nil)
86
+ whitespace
87
+ directives(root)
88
+ rules(root)
89
+ sort_rules(root)
90
+ root
91
+ end
92
+
93
+ def directives(root)
94
+ while @template.scan(/@/)
95
+ name = @template.scan /[^\s;]+/
96
+ whitespace
97
+ value = @template.scan /[^;]+/
98
+ assert_match /;/
99
+ whitespace
100
+
101
+ if name == "import" && value =~ /^(url\()?"?([^\s\(\)\"]+)\.css"?\)?$/
102
+ value = $2
103
+ end
104
+
105
+ root << Tree::ValueNode.new("@#{name} #{value};", nil)
106
+ end
107
+ end
108
+
109
+ def rules(root)
110
+ rules = []
111
+ while @template.scan(/[^\{\s]+/)
112
+ rules << @template[0]
113
+ whitespace
114
+
115
+ if @template.scan(/\{/)
116
+ result = Tree::RuleNode.new(rules.join(' '), nil)
117
+ root << result
118
+ rules = []
119
+
120
+ whitespace
121
+ attributes(result)
122
+ end
123
+ end
124
+ end
125
+
126
+ def attributes(rule)
127
+ while @template.scan(/[^:\}\s]+/)
128
+ name = @template[0]
129
+ whitespace
130
+
131
+ assert_match /:/
132
+
133
+ value = ''
134
+ while @template.scan(/[^;\s]+/)
135
+ value << @template[0] << whitespace
136
+ end
137
+
138
+ assert_match /;/
139
+ rule << Tree::AttrNode.new(name, value, nil)
140
+ end
141
+
142
+ assert_match /\}/
143
+ end
144
+
145
+ def whitespace
146
+ space = @template.scan(/\s*/) || ''
147
+
148
+ # If we've hit a comment,
149
+ # go past it and look for more whitespace
150
+ if @template.scan(/\/\*/)
151
+ @template.scan_until(/\*\//)
152
+ return space + whitespace
153
+ end
154
+ return space
155
+ end
156
+
157
+ def assert_match(re)
158
+ if !@template.scan(re)
159
+ raise Exception.new("Invalid CSS!")
160
+ end
161
+ whitespace
162
+ end
163
+
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
170
+ else
171
+ 1
172
+ end
173
+ end
174
+
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
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
@@ -1,8 +1,11 @@
1
1
  require 'sass/tree/node'
2
2
  require 'sass/tree/value_node'
3
3
  require 'sass/tree/rule_node'
4
+ require 'sass/tree/comment_node'
5
+ require 'sass/tree/attr_node'
4
6
  require 'sass/constant'
5
7
  require 'sass/error'
8
+ require 'haml/util'
6
9
 
7
10
  module Sass
8
11
  # This is the class where all the parsing and processing of the Sass
@@ -16,17 +19,37 @@ module Sass
16
19
  class Engine
17
20
  # The character that begins a CSS attribute.
18
21
  ATTRIBUTE_CHAR = ?:
19
-
22
+
20
23
  # The character that designates that
21
24
  # an attribute should be assigned to the result of constant arithmetic.
22
25
  SCRIPT_CHAR = ?=
23
-
24
- # The string that begins one-line comments.
25
- COMMENT_STRING = '//'
26
26
 
27
- # The regex that matches attributes.
28
- ATTRIBUTE = /:([^\s=]+)\s*(=?)\s*(.*)/
29
-
27
+ # The character that designates the beginning of a comment,
28
+ # either Sass or CSS.
29
+ COMMENT_CHAR = ?/
30
+
31
+ # The character that follows the general COMMENT_CHAR and designates a Sass comment,
32
+ # which is not output as a CSS comment.
33
+ SASS_COMMENT_CHAR = ?/
34
+
35
+ # The character that follows the general COMMENT_CHAR and designates a CSS comment,
36
+ # which is embedded in the CSS document.
37
+ CSS_COMMENT_CHAR = ?*
38
+
39
+ # The character used to denote a compiler directive.
40
+ DIRECTIVE_CHAR = ?@
41
+
42
+ # The regex that matches and extracts data from
43
+ # attributes of the form <tt>:name attr</tt>.
44
+ ATTRIBUTE = /^:([^\s=:]+)\s*(=?)(?:\s+|$)(.*)/
45
+
46
+ # The regex that matches attributes of the form <tt>name: attr</tt>.
47
+ ATTRIBUTE_ALTERNATE_MATCHER = /^[^\s:]+\s*[=:](\s|$)/
48
+
49
+ # The regex that matches and extracts data from
50
+ # attributes of the form <tt>name: attr</tt>.
51
+ ATTRIBUTE_ALTERNATE = /^([^\s=:]+)(\s*=|:)(?:\s+|$)(.*)/
52
+
30
53
  # Creates a new instace of Sass::Engine that will compile the given
31
54
  # template string when <tt>render</tt> is called.
32
55
  # See README for available options.
@@ -41,139 +64,287 @@ module Sass
41
64
  #
42
65
  def initialize(template, options={})
43
66
  @options = {
44
- :style => :nested
67
+ :style => :nested,
68
+ :load_paths => ['.']
45
69
  }.merge! options
46
- @template = template.split("\n")
70
+ @template = template.split(/\n\r|\n/)
47
71
  @lines = []
48
72
  @constants = {}
49
73
  end
50
-
74
+
51
75
  # Processes the template and returns the result as a string.
52
76
  def render
53
77
  begin
54
- split_lines
55
-
56
- root = Tree::Node.new(@options[:style])
57
- index = 0
58
- while @lines[index]
59
- child, index = build_tree(index)
60
- child.line = index if child
61
- root << child if child
62
- end
63
- @line = nil
64
-
65
- root.to_s
78
+ render_to_tree.to_s
66
79
  rescue SyntaxError => err
67
- err.add_backtrace_entry(@options[:filename])
80
+ unless err.sass_filename
81
+ err.add_backtrace_entry(@options[:filename])
82
+ end
68
83
  raise err
69
84
  end
70
85
  end
71
86
 
72
87
  alias_method :to_css, :render
73
-
88
+
89
+ protected
90
+
91
+ def constants
92
+ @constants
93
+ end
94
+
95
+ def render_to_tree
96
+ split_lines
97
+
98
+ root = Tree::Node.new(@options[:style])
99
+ index = 0
100
+ while @lines[index]
101
+ child, index = build_tree(index)
102
+
103
+ if child.is_a? Tree::Node
104
+ child.line = index
105
+ root << child
106
+ elsif child.is_a? Array
107
+ child.each do |c|
108
+ root << c
109
+ end
110
+ end
111
+ end
112
+ @line = nil
113
+
114
+ root
115
+ end
116
+
74
117
  private
75
-
118
+
76
119
  # Readies each line in the template for parsing,
77
120
  # and computes the tabulation of the line.
78
121
  def split_lines
122
+ @line = 0
79
123
  old_tabs = 0
80
124
  @template.each_with_index do |line, index|
81
- @line = index + 1
82
-
83
- # TODO: Allow comments appended to the end of lines,
84
- # find some way to make url(http://www.google.com/) work
85
- unless line[0..1] == COMMENT_STRING # unless line is a comment
86
- tabs = count_tabs(line)
87
-
88
- if tabs # if line isn't blank
89
- if tabs - old_tabs > 1
90
- raise SyntaxError.new("Illegal Indentation: Only two space characters are allowed as tabulation.", @line)
91
- end
92
- @lines << [line.strip, tabs]
93
-
94
- old_tabs = tabs
125
+ @line += 1
126
+
127
+ tabs = count_tabs(line)
128
+
129
+ if line[0] == COMMENT_CHAR && line[1] == SASS_COMMENT_CHAR && tabs == 0
130
+ tabs = old_tabs
131
+ end
132
+
133
+ if tabs # if line isn't blank
134
+ if tabs - old_tabs > 1
135
+ raise SyntaxError.new("Illegal Indentation: Only two space characters are allowed as tabulation.", @line)
95
136
  end
137
+ @lines << [line.strip, tabs]
138
+
139
+ old_tabs = tabs
140
+ else
141
+ @lines << ['//', old_tabs]
96
142
  end
97
143
  end
144
+
98
145
  @line = nil
99
146
  end
100
-
147
+
101
148
  # Counts the tabulation of a line.
102
149
  def count_tabs(line)
103
150
  spaces = line.index(/[^ ]/)
104
151
  if spaces
105
152
  if spaces % 2 == 1 || line[spaces] == ?\t
106
- raise SyntaxError.new("Illegal Indentation: Only two space characters are allowed as tabulation.", @line)
153
+ # Make sure a line with just tabs isn't an error
154
+ return nil if line.strip.empty?
155
+
156
+ raise SyntaxError.new("Illegal Indentation: Only two space characters are allowed as tabulation.", @line)
107
157
  end
108
158
  spaces / 2
109
159
  else
110
160
  nil
111
161
  end
112
162
  end
113
-
163
+
114
164
  def build_tree(index)
115
165
  line, tabs = @lines[index]
116
166
  index += 1
117
167
  @line = index
118
168
  node = parse_line(line)
119
169
 
120
- # Node is nil if it's non-outputting, like a constant assignment
121
- return nil, index unless node
122
-
123
170
  has_children = has_children?(index, tabs)
124
-
125
- while has_children
126
- child, index = build_tree(index)
127
171
 
128
- if child.nil?
129
- raise SyntaxError.new("Constants may only be declared at the root of a document.", @line)
172
+ # Node is a symbol if it's non-outputting, like a constant assignment
173
+ unless node.is_a? Tree::Node
174
+ if has_children
175
+ if node == :constant
176
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath constants.", @line)
177
+ elsif node.is_a? Array
178
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath import directives.", @line)
179
+ end
180
+ end
181
+
182
+ return node, index
183
+ end
184
+
185
+ if node.is_a? Tree::CommentNode
186
+ while has_children
187
+ line, index = raw_next_line(index)
188
+ node << line
189
+
190
+ has_children = has_children?(index, tabs)
130
191
  end
192
+ else
193
+ while has_children
194
+ child, index = build_tree(index)
131
195
 
132
- child.line = @line
133
- node << child if child
134
- has_children = has_children?(index, tabs)
196
+ if child == :constant
197
+ raise SyntaxError.new("Constants may only be declared at the root of a document.", @line)
198
+ elsif child.is_a? Array
199
+ raise SyntaxError.new("Import directives may only be used at the root of a document.", @line)
200
+ elsif child.is_a? Tree::Node
201
+ child.line = @line
202
+ node << child
203
+ end
204
+
205
+ has_children = has_children?(index, tabs)
206
+ end
135
207
  end
136
-
208
+
137
209
  return node, index
138
210
  end
139
-
211
+
140
212
  def has_children?(index, tabs)
141
213
  next_line = @lines[index]
142
214
  next_line && next_line[1] > tabs
143
215
  end
144
-
216
+
217
+ def raw_next_line(index)
218
+ [@lines[index][0], index + 1]
219
+ end
220
+
145
221
  def parse_line(line)
146
222
  case line[0]
147
- when ATTRIBUTE_CHAR
148
- parse_attribute(line)
149
- when Constant::CONSTANT_CHAR
150
- parse_constant(line)
223
+ when ATTRIBUTE_CHAR
224
+ parse_attribute(line, ATTRIBUTE)
225
+ when Constant::CONSTANT_CHAR
226
+ parse_constant(line)
227
+ when COMMENT_CHAR
228
+ parse_comment(line)
229
+ when DIRECTIVE_CHAR
230
+ parse_directive(line)
231
+ else
232
+ if line =~ ATTRIBUTE_ALTERNATE_MATCHER
233
+ parse_attribute(line, ATTRIBUTE_ALTERNATE)
151
234
  else
152
235
  Tree::RuleNode.new(line, @options[:style])
236
+ end
153
237
  end
154
238
  end
155
-
156
- def parse_attribute(line)
157
- name, eq, value = line.scan(ATTRIBUTE)[0]
239
+
240
+ def parse_attribute(line, attribute_regx)
241
+ name, eq, value = line.scan(attribute_regx)[0]
158
242
 
159
243
  if name.nil? || value.nil?
160
244
  raise SyntaxError.new("Invalid attribute: \"#{line}\"", @line)
161
245
  end
162
-
163
- if eq[0] == SCRIPT_CHAR
246
+
247
+ if eq.strip[0] == SCRIPT_CHAR
164
248
  value = Sass::Constant.parse(value, @constants, @line).to_s
165
249
  end
166
-
250
+
167
251
  Tree::AttrNode.new(name, value, @options[:style])
168
252
  end
169
-
253
+
170
254
  def parse_constant(line)
171
255
  name, value = line.scan(Sass::Constant::MATCH)[0]
172
256
  unless name && value
173
257
  raise SyntaxError.new("Invalid constant: \"#{line}\"", @line)
174
258
  end
175
259
  @constants[name] = Sass::Constant.parse(value, @constants, @line)
176
- nil
260
+ :constant
261
+ end
262
+
263
+ def parse_comment(line)
264
+ if line[1] == SASS_COMMENT_CHAR
265
+ :comment
266
+ elsif line[1] == CSS_COMMENT_CHAR
267
+ Tree::CommentNode.new(line, @options[:style])
268
+ else
269
+ Tree::RuleNode.new(line, @options[:style])
270
+ end
271
+ end
272
+
273
+ def parse_directive(line)
274
+ directive, value = line[1..-1].split(/\s+/, 2)
275
+
276
+ case directive
277
+ when "import"
278
+ import(value)
279
+ else
280
+ raise SyntaxError.new("Unknown compiler directive: #{"@#{directive} #{value}".dump}", @line)
281
+ end
282
+ end
283
+
284
+ def import(files)
285
+ nodes = []
286
+
287
+ files.split(/,\s*/).each do |filename|
288
+ engine = nil
289
+ filename = find_file_to_import(filename)
290
+ if filename =~ /\.css$/
291
+ nodes << Tree::ValueNode.new("@import #{filename}", @options[:style])
292
+ else
293
+ File.open(filename) do |file|
294
+ new_options = @options.dup
295
+ new_options[:filename] = filename
296
+ engine = Sass::Engine.new(file.read, @options)
297
+ end
298
+
299
+ engine.constants.merge! @constants
300
+
301
+ begin
302
+ root = engine.render_to_tree
303
+ rescue Sass::SyntaxError => err
304
+ err.add_backtrace_entry(filename)
305
+ raise err
306
+ end
307
+ root.children.each do |child|
308
+ child.filename = filename
309
+ nodes << child
310
+ end
311
+ @constants = engine.constants
312
+ end
313
+ end
314
+
315
+ nodes
316
+ end
317
+
318
+ def find_file_to_import(filename)
319
+ was_sass = false
320
+ original_filename = filename
321
+ new_filename = nil
322
+
323
+ if filename[-5..-1] == ".sass"
324
+ filename = filename[0...-5]
325
+ was_sass = true
326
+ elsif filename[-4..-1] == ".css"
327
+ return filename
328
+ end
329
+
330
+ @options[:load_paths].each do |path|
331
+ full_path = File.join(path, filename) + '.sass'
332
+
333
+ if File.readable?(full_path)
334
+ new_filename = full_path
335
+ break
336
+ end
337
+ end
338
+
339
+ if new_filename.nil?
340
+ if was_sass
341
+ raise SyntaxError.new("File to import not found or unreadable: #{original_filename}", @line)
342
+ else
343
+ return filename + '.css'
344
+ end
345
+ else
346
+ new_filename
347
+ end
177
348
  end
178
349
  end
179
350
  end