haml 1.8.0 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. data/FAQ +138 -0
  2. data/MIT-LICENSE +1 -1
  3. data/{README → README.rdoc} +66 -3
  4. data/REVISION +1 -0
  5. data/Rakefile +115 -147
  6. data/VERSION +1 -1
  7. data/bin/css2sass +0 -0
  8. data/bin/haml +2 -1
  9. data/bin/html2haml +0 -0
  10. data/bin/sass +0 -0
  11. data/init.rb +6 -1
  12. data/lib/haml/buffer.rb +122 -64
  13. data/lib/haml/engine.rb +77 -46
  14. data/lib/haml/error.rb +15 -6
  15. data/lib/haml/exec.rb +61 -10
  16. data/lib/haml/filters.rb +229 -74
  17. data/lib/haml/helpers/action_view_extensions.rb +1 -1
  18. data/lib/haml/helpers/action_view_mods.rb +109 -24
  19. data/lib/haml/helpers.rb +137 -76
  20. data/lib/haml/html.rb +8 -8
  21. data/lib/haml/precompiler.rb +280 -153
  22. data/lib/haml/template/patch.rb +10 -3
  23. data/lib/haml/template/plugin.rb +61 -10
  24. data/lib/haml/template.rb +14 -9
  25. data/lib/haml.rb +483 -214
  26. data/lib/sass/constant/color.rb +13 -13
  27. data/lib/sass/constant/literal.rb +8 -7
  28. data/lib/sass/constant/nil.rb +9 -0
  29. data/lib/sass/constant/number.rb +10 -10
  30. data/lib/sass/constant/operation.rb +4 -4
  31. data/lib/sass/constant/string.rb +3 -3
  32. data/lib/sass/constant.rb +46 -77
  33. data/lib/sass/css.rb +130 -56
  34. data/lib/sass/engine.rb +131 -43
  35. data/lib/sass/plugin/merb.rb +48 -12
  36. data/lib/sass/plugin/rails.rb +10 -4
  37. data/lib/sass/plugin.rb +33 -10
  38. data/lib/sass/tree/attr_node.rb +5 -5
  39. data/lib/sass/tree/directive_node.rb +2 -7
  40. data/lib/sass/tree/node.rb +1 -12
  41. data/lib/sass/tree/rule_node.rb +39 -31
  42. data/lib/sass/tree/value_node.rb +1 -1
  43. data/lib/sass.rb +194 -19
  44. data/rails/init.rb +1 -0
  45. data/test/benchmark.rb +67 -80
  46. data/test/haml/engine_test.rb +368 -152
  47. data/test/haml/helper_test.rb +68 -16
  48. data/test/haml/html2haml_test.rb +3 -4
  49. data/test/haml/results/content_for_layout.xhtml +1 -2
  50. data/test/haml/results/eval_suppressed.xhtml +2 -4
  51. data/test/haml/results/filters.xhtml +38 -30
  52. data/test/haml/results/helpers.xhtml +4 -8
  53. data/test/haml/results/just_stuff.xhtml +8 -7
  54. data/test/haml/results/nuke_inner_whitespace.xhtml +40 -0
  55. data/test/haml/results/nuke_outer_whitespace.xhtml +148 -0
  56. data/test/haml/results/original_engine.xhtml +3 -7
  57. data/test/haml/results/partials.xhtml +1 -0
  58. data/test/haml/results/tag_parsing.xhtml +1 -6
  59. data/test/haml/results/very_basic.xhtml +2 -4
  60. data/test/haml/results/whitespace_handling.xhtml +13 -21
  61. data/test/haml/template_test.rb +42 -57
  62. data/test/haml/templates/_partial.haml +1 -0
  63. data/test/haml/templates/filters.haml +39 -21
  64. data/test/haml/templates/helpers.haml +10 -10
  65. data/test/haml/templates/just_stuff.haml +8 -3
  66. data/test/haml/templates/nuke_inner_whitespace.haml +32 -0
  67. data/test/haml/templates/nuke_outer_whitespace.haml +144 -0
  68. data/test/haml/templates/partials.haml +1 -1
  69. data/test/haml/templates/tag_parsing.haml +0 -3
  70. data/test/haml/templates/whitespace_handling.haml +10 -10
  71. data/test/sass/engine_test.rb +97 -39
  72. data/test/sass/plugin_test.rb +4 -7
  73. data/test/sass/results/constants.css +2 -0
  74. data/test/sass/results/import.css +2 -2
  75. data/test/sass/results/mixins.css +95 -0
  76. data/test/sass/results/multiline.css +24 -0
  77. data/test/sass/templates/constants.sass +3 -0
  78. data/test/sass/templates/import.sass +4 -1
  79. data/test/sass/templates/importee.sass +4 -0
  80. data/test/sass/templates/mixins.sass +76 -0
  81. data/test/sass/templates/multiline.sass +20 -0
  82. data/test/test_helper.rb +18 -0
  83. metadata +70 -53
  84. data/lib/haml/helpers/action_view_mods.rb.rej +0 -30
  85. data/lib/haml/util.rb +0 -18
  86. data/lib/sass/constant.rb.rej +0 -42
  87. data/test/haml/runner.rb +0 -16
  88. data/test/profile.rb +0 -65
  89. data/test/sass/engine_test.rb.rej +0 -18
data/lib/sass/css.rb CHANGED
@@ -6,11 +6,11 @@ module Sass
6
6
  # :stopdoc:
7
7
  module Tree
8
8
  class Node
9
- def to_sass
9
+ def to_sass(opts = {})
10
10
  result = ''
11
11
 
12
12
  children.each do |child|
13
- result << "#{child.to_sass(0)}\n"
13
+ result << "#{child.to_sass(0, opts)}\n"
14
14
  end
15
15
 
16
16
  result
@@ -18,17 +18,17 @@ module Sass
18
18
  end
19
19
 
20
20
  class ValueNode
21
- def to_sass(tabs)
21
+ def to_sass(tabs, opts = {})
22
22
  "#{value}\n"
23
23
  end
24
24
  end
25
25
 
26
26
  class RuleNode
27
- def to_sass(tabs)
28
- str = "#{' ' * tabs}#{rule}\n"
27
+ def to_sass(tabs, opts = {})
28
+ str = "\n#{' ' * tabs}#{rule}#{children.any? { |c| c.is_a? AttrNode } ? "\n" : ''}"
29
29
 
30
30
  children.each do |child|
31
- str << "#{child.to_sass(tabs + 1)}"
31
+ str << "#{child.to_sass(tabs + 1, opts)}"
32
32
  end
33
33
 
34
34
  str
@@ -36,8 +36,14 @@ module Sass
36
36
  end
37
37
 
38
38
  class AttrNode
39
- def to_sass(tabs)
40
- "#{' ' * tabs}:#{name} #{value}\n"
39
+ def to_sass(tabs, opts = {})
40
+ "#{' ' * tabs}#{opts[:alternate] ? '' : ':'}#{name}#{opts[:alternate] ? ':' : ''} #{value}\n"
41
+ end
42
+ end
43
+
44
+ class DirectiveNode
45
+ def to_sass(tabs, opts = {})
46
+ "#{' ' * tabs}#{value}#{children.map {|c| c.to_sass(tabs + 1, opts)}}\n"
41
47
  end
42
48
  end
43
49
  end
@@ -46,25 +52,40 @@ module Sass
46
52
  # It keeps the semantics and most of the efficiency of normal hashes
47
53
  # while also keeping track of the order in which elements were set.
48
54
  class OrderedHash
49
- Node = Struct.new('Node', :key, :value, :next)
55
+ Node = Struct.new(:key, :value, :next, :prev)
50
56
  include Enumerable
51
57
 
52
58
  def initialize
53
59
  @hash = {}
54
60
  end
55
61
 
62
+ def initialize_copy(other)
63
+ @hash = other.instance_variable_get('@hash').clone
64
+ end
65
+
56
66
  def [](key)
57
67
  @hash[key] && @hash[key].value
58
68
  end
59
69
 
60
70
  def []=(key, value)
61
- node = Node.new(key, value, nil)
71
+ node = Node.new(key, value)
72
+
73
+ if old = @hash[key]
74
+ if old.prev
75
+ old.prev.next = old.next
76
+ else # old is @first and @last
77
+ @first = @last = nil
78
+ end
79
+ end
80
+
62
81
  if @first.nil?
63
82
  @first = @last = node
64
83
  else
84
+ node.prev = @last
65
85
  @last.next = node
66
86
  @last = node
67
87
  end
88
+
68
89
  @hash[key] = node
69
90
  value
70
91
  end
@@ -90,11 +111,12 @@ module Sass
90
111
 
91
112
  # Creates a new instance of Sass::CSS that will compile the given document
92
113
  # to a Sass string when +render+ is called.
93
- def initialize(template)
114
+ def initialize(template, options = {})
94
115
  if template.is_a? IO
95
116
  template = template.read
96
117
  end
97
118
 
119
+ @options = options
98
120
  @template = StringScanner.new(template)
99
121
  end
100
122
 
@@ -102,10 +124,10 @@ module Sass
102
124
  # containing the CSS template.
103
125
  def render
104
126
  begin
105
- build_tree.to_sass
127
+ build_tree.to_sass(@options).strip + "\n"
106
128
  rescue Exception => err
107
129
  line = @template.string[0...@template.pos].split("\n").size
108
-
130
+
109
131
  err.backtrace.unshift "(css):#{line}"
110
132
  raise err
111
133
  end
@@ -116,46 +138,42 @@ module Sass
116
138
  def build_tree
117
139
  root = Tree::Node.new(nil)
118
140
  whitespace
119
- directives root
120
- rules root
121
- expand_commas root
122
- nest_rules root
123
- flatten_rules root
124
- fold_commas root
141
+ rules root
142
+ expand_commas root
143
+ parent_ref_rules root
144
+ remove_parent_refs root
145
+ flatten_rules root
146
+ fold_commas root
125
147
  root
126
148
  end
127
149
 
128
- def directives(root)
129
- while @template.scan(/@/)
130
- name = @template.scan /[^\s;]+/
131
- whitespace
132
- value = @template.scan /[^;]+/
133
- assert_match /;/
150
+ def rules(root)
151
+ while r = rule
152
+ root << r
134
153
  whitespace
135
-
136
- if name == "import" && value =~ /^(url\()?"?([^\s\(\)\"]+)\.css"?\)?$/
137
- value = $2
138
- end
139
-
140
- root << Tree::ValueNode.new("@#{name} #{value};", nil)
141
154
  end
142
155
  end
143
156
 
144
- def rules(root)
145
- rules = []
146
- while @template.scan(/[^\{\s]+/)
147
- rules << @template[0]
148
- whitespace
157
+ def rule
158
+ return unless rule = @template.scan(/[^\{\};]+/)
159
+ rule.strip!
160
+ directive = rule[0] == ?@
149
161
 
150
- if @template.scan(/\{/)
151
- result = Tree::RuleNode.new(rules.join(' '), nil)
152
- root << result
153
- rules = []
162
+ if directive
163
+ node = Tree::DirectiveNode.new(rule, nil)
164
+ return node if @template.scan(/;/)
154
165
 
155
- whitespace
156
- attributes(result)
157
- end
166
+ assert_match /\{/
167
+ whitespace
168
+
169
+ rules(node)
170
+ return node
158
171
  end
172
+
173
+ assert_match /\{/
174
+ node = Tree::RuleNode.new(rule, nil)
175
+ attributes(node)
176
+ return node
159
177
  end
160
178
 
161
179
  def attributes(rule)
@@ -164,12 +182,12 @@ module Sass
164
182
  whitespace
165
183
 
166
184
  assert_match /:/
167
-
185
+
168
186
  value = ''
169
187
  while @template.scan(/[^;\s\}]+/)
170
188
  value << @template[0] << whitespace
171
189
  end
172
-
190
+
173
191
  assert_match /(;|(?=\}))/
174
192
  rule << Tree::AttrNode.new(name, value, nil)
175
193
  end
@@ -191,7 +209,10 @@ module Sass
191
209
 
192
210
  def assert_match(re)
193
211
  if !@template.scan(re)
194
- raise Exception.new("Invalid CSS!")
212
+ line = @template.string[0..@template.pos].count "\n"
213
+ # Display basic regexps as plain old strings
214
+ expected = re.source == Regexp.escape(re.source) ? "\"#{re.source}\"" : re.inspect
215
+ raise Exception.new("Invalid CSS on line #{line}: expected #{expected}")
195
216
  end
196
217
  whitespace
197
218
  end
@@ -224,7 +245,22 @@ module Sass
224
245
  root.children.flatten!
225
246
  end
226
247
 
227
- # Nest rules so that
248
+ # Make rules use parent refs so that
249
+ #
250
+ # foo
251
+ # color: green
252
+ # foo.bar
253
+ # color: blue
254
+ #
255
+ # becomes
256
+ #
257
+ # foo
258
+ # color: green
259
+ # &.bar
260
+ # color: blue
261
+ #
262
+ # This has the side effect of nesting rules,
263
+ # so that
228
264
  #
229
265
  # foo
230
266
  # color: green
@@ -237,29 +273,50 @@ module Sass
237
273
  #
238
274
  # foo
239
275
  # color: green
240
- # bar
276
+ # & bar
241
277
  # color: red
242
- # baz
278
+ # & baz
243
279
  # color: blue
244
- #
245
- def nest_rules(root)
280
+ #
281
+ def parent_ref_rules(root)
246
282
  rules = OrderedHash.new
247
283
  root.children.select { |c| Tree::RuleNode === c }.each do |child|
248
284
  root.children.delete child
249
- first, rest = child.rule.split(' ', 2)
285
+ first, rest = child.rule.scan(/^(&?(?: .|[^ ])[^.#: \[]*)([.#: \[].*)?$/).first
250
286
  rules[first] ||= Tree::RuleNode.new(first, nil)
251
287
  if rest
252
- child.rule = rest
288
+ child.rule = "&" + rest
253
289
  rules[first] << child
254
290
  else
255
291
  rules[first].children += child.children
256
292
  end
257
293
  end
258
294
 
259
- rules.values.each { |v| nest_rules(v) }
295
+ rules.values.each { |v| parent_ref_rules(v) }
260
296
  root.children += rules.values
261
297
  end
262
298
 
299
+ # Remove useless parent refs so that
300
+ #
301
+ # foo
302
+ # & bar
303
+ # color: blue
304
+ #
305
+ # becomes
306
+ #
307
+ # foo
308
+ # bar
309
+ # color: blue
310
+ #
311
+ def remove_parent_refs(root)
312
+ root.children.each do |child|
313
+ if child.is_a?(Tree::RuleNode)
314
+ child.rule.gsub! /^& /, ''
315
+ remove_parent_refs child
316
+ end
317
+ end
318
+ end
319
+
263
320
  # Flatten rules so that
264
321
  #
265
322
  # foo
@@ -271,7 +328,18 @@ module Sass
271
328
  #
272
329
  # foo bar baz
273
330
  # color: red
274
- #
331
+ #
332
+ # and
333
+ #
334
+ # foo
335
+ # &.bar
336
+ # color: blue
337
+ #
338
+ # becomes
339
+ #
340
+ # foo.bar
341
+ # color: blue
342
+ #
275
343
  def flatten_rules(root)
276
344
  root.children.each { |child| flatten_rule(child) if child.is_a?(Tree::RuleNode) }
277
345
  end
@@ -279,7 +347,13 @@ module Sass
279
347
  def flatten_rule(rule)
280
348
  while rule.children.size == 1 && rule.children.first.is_a?(Tree::RuleNode)
281
349
  child = rule.children.first
282
- rule.rule = "#{rule.rule} #{child.rule}"
350
+
351
+ if child.rule[0] == ?&
352
+ rule.rule = child.rule.gsub /^&/, rule.rule
353
+ else
354
+ rule.rule = "#{rule.rule} #{child.rule}"
355
+ end
356
+
283
357
  rule.children = child.children
284
358
  end
285
359
 
data/lib/sass/engine.rb CHANGED
@@ -6,7 +6,6 @@ require 'sass/tree/attr_node'
6
6
  require 'sass/tree/directive_node'
7
7
  require 'sass/constant'
8
8
  require 'sass/error'
9
- require 'haml/util'
10
9
 
11
10
  module Sass
12
11
  # This is the class where all the parsing and processing of the Sass
@@ -39,10 +38,16 @@ module Sass
39
38
 
40
39
  # The character used to denote a compiler directive.
41
40
  DIRECTIVE_CHAR = ?@
42
-
41
+
43
42
  # Designates a non-parsed rule.
44
43
  ESCAPE_CHAR = ?\\
45
44
 
45
+ # Designates block as mixin definition rather than CSS rules to output
46
+ MIXIN_DEFINITION_CHAR = ?=
47
+
48
+ # Includes named mixin declared using MIXIN_DEFINITION_CHAR
49
+ MIXIN_INCLUDE_CHAR = ?+
50
+
46
51
  # The regex that matches and extracts data from
47
52
  # attributes of the form <tt>:name attr</tt>.
48
53
  ATTRIBUTE = /^:([^\s=:]+)\s*(=?)(?:\s+|$)(.*)/
@@ -56,14 +61,14 @@ module Sass
56
61
 
57
62
  # Creates a new instace of Sass::Engine that will compile the given
58
63
  # template string when <tt>render</tt> is called.
59
- # See README for available options.
64
+ # See README.rdoc for available options.
60
65
  #
61
66
  #--
62
67
  #
63
68
  # TODO: Add current options to REFRENCE. Remember :filename!
64
69
  #
65
70
  # When adding options, remember to add information about them
66
- # to README!
71
+ # to README.rdoc!
67
72
  #++
68
73
  #
69
74
  def initialize(template, options={})
@@ -71,9 +76,10 @@ module Sass
71
76
  :style => :nested,
72
77
  :load_paths => ['.']
73
78
  }.merge! options
74
- @template = template.split(/\n\r|\n/)
79
+ @template = template.split(/\r\n|\r|\n/)
75
80
  @lines = []
76
81
  @constants = {"important" => "!important"}
82
+ @mixins = {}
77
83
  end
78
84
 
79
85
  # Processes the template and returns the result as a string.
@@ -96,16 +102,21 @@ module Sass
96
102
  @constants
97
103
  end
98
104
 
105
+ def mixins
106
+ @mixins
107
+ end
108
+
99
109
  def render_to_tree
100
110
  split_lines
101
111
 
102
112
  root = Tree::Node.new(@options[:style])
103
113
  index = 0
104
114
  while @lines[index]
115
+ old_index = index
105
116
  child, index = build_tree(index)
106
117
 
107
118
  if child.is_a? Tree::Node
108
- child.line = index
119
+ child.line = old_index + 1
109
120
  root << child
110
121
  elsif child.is_a? Array
111
122
  child.each do |c|
@@ -113,7 +124,7 @@ module Sass
113
124
  end
114
125
  end
115
126
  end
116
- @line = nil
127
+ @lines.clear
117
128
 
118
129
  root
119
130
  end
@@ -124,7 +135,7 @@ module Sass
124
135
  # and computes the tabulation of the line.
125
136
  def split_lines
126
137
  @line = 0
127
- old_tabs = 0
138
+ old_tabs = nil
128
139
  @template.each_with_index do |line, index|
129
140
  @line += 1
130
141
 
@@ -135,14 +146,16 @@ module Sass
135
146
  end
136
147
 
137
148
  if tabs # if line isn't blank
138
- if tabs - old_tabs > 1
139
- raise SyntaxError.new("Illegal Indentation: Only two space characters are allowed as tabulation.", @line)
149
+ raise SyntaxError.new("Indenting at the beginning of the document is illegal.", @line) if old_tabs.nil? && tabs > 0
150
+
151
+ if old_tabs && tabs - old_tabs > 1
152
+ raise SyntaxError.new("#{tabs * 2} spaces were used for indentation. Sass must be indented using two spaces.", @line)
140
153
  end
141
154
  @lines << [line.strip, tabs]
142
155
 
143
156
  old_tabs = tabs
144
157
  else
145
- @lines << ['//', old_tabs]
158
+ @lines << ['//', old_tabs || 0]
146
159
  end
147
160
  end
148
161
 
@@ -151,18 +164,20 @@ module Sass
151
164
 
152
165
  # Counts the tabulation of a line.
153
166
  def count_tabs(line)
154
- spaces = line.index(/[^ ]/)
155
- if spaces
156
- if spaces % 2 == 1 || line[spaces] == ?\t
157
- # Make sure a line with just tabs isn't an error
158
- return nil if line.strip.empty?
159
-
160
- raise SyntaxError.new("Illegal Indentation: Only two space characters are allowed as tabulation.", @line)
161
- end
162
- spaces / 2
163
- else
164
- nil
167
+ return nil if line.strip.empty?
168
+ return nil unless spaces = line.index(/[^ ]/)
169
+
170
+ if spaces % 2 == 1
171
+ raise SyntaxError.new(<<END.strip, @line)
172
+ #{spaces} space#{spaces == 1 ? ' was' : 's were'} used for indentation. Sass must be indented using two spaces.
173
+ END
174
+ elsif line[spaces] == ?\t
175
+ raise SyntaxError.new(<<END.strip, @line)
176
+ A tab character was used for indentation. Sass must be indented using two spaces.
177
+ Are you sure you have soft tabs enabled in your editor?
178
+ END
165
179
  end
180
+ spaces / 2
166
181
  end
167
182
 
168
183
  def build_tree(index)
@@ -177,15 +192,23 @@ module Sass
177
192
  unless node.is_a? Tree::Node
178
193
  if has_children
179
194
  if node == :constant
180
- raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath constants.", @line)
195
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath constants.", @line + 1)
181
196
  elsif node.is_a? Array
182
- raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath import directives.", @line)
197
+ # arrays can either be full of import statements
198
+ # or attributes from mixin includes
199
+ # in either case they shouldn't have children.
200
+ # Need to peek into the array in order to give meaningful errors
201
+ directive_type = (node.first.is_a?(Tree::DirectiveNode) ? "import" : "mixin")
202
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath #{directive_type} directives.", @line + 1)
183
203
  end
184
204
  end
185
205
 
206
+ index = @line if node == :mixin
186
207
  return node, index
187
208
  end
188
209
 
210
+ node.line = @line
211
+
189
212
  if node.is_a? Tree::CommentNode
190
213
  while has_children
191
214
  line, index = raw_next_line(index)
@@ -193,26 +216,52 @@ module Sass
193
216
 
194
217
  has_children = has_children?(index, tabs)
195
218
  end
196
- else
197
- while has_children
198
- child, index = build_tree(index)
199
219
 
200
- if child == :constant
201
- raise SyntaxError.new("Constants may only be declared at the root of a document.", @line)
202
- elsif child.is_a? Array
203
- raise SyntaxError.new("Import directives may only be used at the root of a document.", @line)
204
- elsif child.is_a? Tree::Node
205
- child.line = @line
206
- node << child
220
+ return node, index
221
+ end
222
+
223
+ # Resolve multiline rules
224
+ if node.is_a?(Tree::RuleNode)
225
+ if node.continued?
226
+ child, index = build_tree(index) if @lines[old_index = index]
227
+ if @lines[old_index].nil? || has_children?(old_index, tabs) || !child.is_a?(Tree::RuleNode)
228
+ raise SyntaxError.new("Rules can't end in commas.", @line)
207
229
  end
208
230
 
209
- has_children = has_children?(index, tabs)
231
+ node.add_rules child
210
232
  end
233
+ node.children = child.children if child
234
+ end
235
+
236
+ while has_children
237
+ child, index = build_tree(index)
238
+
239
+ validate_and_append_child(node, child)
240
+
241
+ has_children = has_children?(index, tabs)
211
242
  end
212
243
 
213
244
  return node, index
214
245
  end
215
246
 
247
+ def validate_and_append_child(parent, child)
248
+ case child
249
+ when :constant
250
+ raise SyntaxError.new("Constants may only be declared at the root of a document.", @line)
251
+ when :mixin
252
+ raise SyntaxError.new("Mixins may only be defined at the root of a document.", @line)
253
+ when Array
254
+ child.each do |c|
255
+ if c.is_a?(Tree::DirectiveNode)
256
+ raise SyntaxError.new("Import directives may only be used at the root of a document.", @line)
257
+ end
258
+ parent << c
259
+ end
260
+ when Tree::Node
261
+ parent << child
262
+ end
263
+ end
264
+
216
265
  def has_children?(index, tabs)
217
266
  next_line = ['//', 0]
218
267
  while !next_line.nil? && next_line[0] == '//' && next_line[1] = 0
@@ -238,6 +287,14 @@ module Sass
238
287
  parse_directive(line)
239
288
  when ESCAPE_CHAR
240
289
  Tree::RuleNode.new(line[1..-1], @options[:style])
290
+ when MIXIN_DEFINITION_CHAR
291
+ parse_mixin_definition(line)
292
+ when MIXIN_INCLUDE_CHAR
293
+ if line[1].nil? || line[1] == ?\s
294
+ Tree::RuleNode.new(line, @options[:style])
295
+ else
296
+ parse_mixin_include(line)
297
+ end
241
298
  else
242
299
  if line =~ ATTRIBUTE_ALTERNATE_MATCHER
243
300
  parse_attribute(line, ATTRIBUTE_ALTERNATE)
@@ -259,7 +316,7 @@ module Sass
259
316
  name, eq, value = line.scan(attribute_regx)[0]
260
317
 
261
318
  if name.nil? || value.nil?
262
- raise SyntaxError.new("Invalid attribute: \"#{line}\"", @line)
319
+ raise SyntaxError.new("Invalid attribute: \"#{line}\".", @line)
263
320
  end
264
321
 
265
322
  if eq.strip[0] == SCRIPT_CHAR
@@ -270,11 +327,18 @@ module Sass
270
327
  end
271
328
 
272
329
  def parse_constant(line)
273
- name, value = line.scan(Sass::Constant::MATCH)[0]
330
+ name, op, value = line.scan(Sass::Constant::MATCH)[0]
274
331
  unless name && value
275
- raise SyntaxError.new("Invalid constant: \"#{line}\"", @line)
332
+ raise SyntaxError.new("Invalid constant: \"#{line}\".", @line)
333
+ end
334
+
335
+ constant = Sass::Constant.parse(value, @constants, @line)
336
+ if op == '||='
337
+ @constants[name] ||= constant
338
+ else
339
+ @constants[name] = constant
276
340
  end
277
- @constants[name] = Sass::Constant.parse(value, @constants, @line)
341
+
278
342
  :constant
279
343
  end
280
344
 
@@ -291,14 +355,36 @@ module Sass
291
355
  def parse_directive(line)
292
356
  directive, value = line[1..-1].split(/\s+/, 2)
293
357
 
294
- case directive
295
- when "import"
358
+ # If value begins with url( or ",
359
+ # it's a CSS @import rule and we don't want to touch it.
360
+ if directive == "import" && value !~ /^(url\(|")/
296
361
  import(value)
297
362
  else
298
363
  Tree::DirectiveNode.new(line, @options[:style])
299
364
  end
300
365
  end
301
366
 
367
+ def parse_mixin_definition(line)
368
+ mixin_name = line[1..-1]
369
+ @mixins[mixin_name] = []
370
+ index = @line
371
+ line, tabs = @lines[index]
372
+ while !line.nil? && tabs > 0
373
+ child, index = build_tree(index)
374
+ validate_and_append_child(@mixins[mixin_name], child)
375
+ line, tabs = @lines[index]
376
+ end
377
+ :mixin
378
+ end
379
+
380
+ def parse_mixin_include(line)
381
+ mixin_name = line[1..-1]
382
+ unless @mixins.has_key?(mixin_name)
383
+ raise SyntaxError.new("Undefined mixin '#{mixin_name}'.", @line)
384
+ end
385
+ @mixins[mixin_name]
386
+ end
387
+
302
388
  def import(files)
303
389
  nodes = []
304
390
 
@@ -312,7 +398,7 @@ module Sass
312
398
  end
313
399
 
314
400
  if filename =~ /\.css$/
315
- nodes << Tree::ValueNode.new("@import url(#{filename});", @options[:style])
401
+ nodes << Tree::DirectiveNode.new("@import url(#{filename})", @options[:style])
316
402
  else
317
403
  File.open(filename) do |file|
318
404
  new_options = @options.dup
@@ -321,6 +407,7 @@ module Sass
321
407
  end
322
408
 
323
409
  engine.constants.merge! @constants
410
+ engine.mixins.merge! @mixins
324
411
 
325
412
  begin
326
413
  root = engine.render_to_tree
@@ -333,6 +420,7 @@ module Sass
333
420
  nodes << child
334
421
  end
335
422
  @constants = engine.constants
423
+ @mixins = engine.mixins
336
424
  end
337
425
  end
338
426
 
@@ -354,7 +442,7 @@ module Sass
354
442
 
355
443
  if new_filename.nil?
356
444
  if was_sass
357
- raise Exception.new("File to import not found or unreadable: #{original_filename}")
445
+ raise Exception.new("File to import not found or unreadable: #{original_filename}.")
358
446
  else
359
447
  return filename + '.css'
360
448
  end