mack-haml 0.8.1 → 0.8.2

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 (43) hide show
  1. data/lib/gems.rb +13 -0
  2. data/lib/gems/haml-2.0.4/VERSION +1 -0
  3. data/lib/gems/haml-2.0.4/bin/css2sass +7 -0
  4. data/lib/gems/haml-2.0.4/bin/haml +9 -0
  5. data/lib/gems/haml-2.0.4/bin/html2haml +7 -0
  6. data/lib/gems/haml-2.0.4/bin/sass +8 -0
  7. data/lib/gems/haml-2.0.4/lib/haml.rb +1040 -0
  8. data/lib/gems/haml-2.0.4/lib/haml/buffer.rb +239 -0
  9. data/lib/gems/haml-2.0.4/lib/haml/engine.rb +265 -0
  10. data/lib/gems/haml-2.0.4/lib/haml/error.rb +22 -0
  11. data/lib/gems/haml-2.0.4/lib/haml/exec.rb +364 -0
  12. data/lib/gems/haml-2.0.4/lib/haml/filters.rb +275 -0
  13. data/lib/gems/haml-2.0.4/lib/haml/helpers.rb +453 -0
  14. data/lib/gems/haml-2.0.4/lib/haml/helpers/action_view_extensions.rb +45 -0
  15. data/lib/gems/haml-2.0.4/lib/haml/helpers/action_view_mods.rb +179 -0
  16. data/lib/gems/haml-2.0.4/lib/haml/html.rb +227 -0
  17. data/lib/gems/haml-2.0.4/lib/haml/precompiler.rb +805 -0
  18. data/lib/gems/haml-2.0.4/lib/haml/template.rb +51 -0
  19. data/lib/gems/haml-2.0.4/lib/haml/template/patch.rb +58 -0
  20. data/lib/gems/haml-2.0.4/lib/haml/template/plugin.rb +72 -0
  21. data/lib/gems/haml-2.0.4/lib/sass.rb +863 -0
  22. data/lib/gems/haml-2.0.4/lib/sass/constant.rb +214 -0
  23. data/lib/gems/haml-2.0.4/lib/sass/constant/color.rb +101 -0
  24. data/lib/gems/haml-2.0.4/lib/sass/constant/literal.rb +54 -0
  25. data/lib/gems/haml-2.0.4/lib/sass/constant/nil.rb +9 -0
  26. data/lib/gems/haml-2.0.4/lib/sass/constant/number.rb +87 -0
  27. data/lib/gems/haml-2.0.4/lib/sass/constant/operation.rb +30 -0
  28. data/lib/gems/haml-2.0.4/lib/sass/constant/string.rb +22 -0
  29. data/lib/gems/haml-2.0.4/lib/sass/css.rb +394 -0
  30. data/lib/gems/haml-2.0.4/lib/sass/engine.rb +466 -0
  31. data/lib/gems/haml-2.0.4/lib/sass/error.rb +35 -0
  32. data/lib/gems/haml-2.0.4/lib/sass/plugin.rb +169 -0
  33. data/lib/gems/haml-2.0.4/lib/sass/plugin/merb.rb +56 -0
  34. data/lib/gems/haml-2.0.4/lib/sass/plugin/rails.rb +24 -0
  35. data/lib/gems/haml-2.0.4/lib/sass/tree/attr_node.rb +53 -0
  36. data/lib/gems/haml-2.0.4/lib/sass/tree/comment_node.rb +20 -0
  37. data/lib/gems/haml-2.0.4/lib/sass/tree/directive_node.rb +46 -0
  38. data/lib/gems/haml-2.0.4/lib/sass/tree/node.rb +42 -0
  39. data/lib/gems/haml-2.0.4/lib/sass/tree/rule_node.rb +89 -0
  40. data/lib/gems/haml-2.0.4/lib/sass/tree/value_node.rb +16 -0
  41. data/lib/gems/haml-2.0.4/rails/init.rb +1 -0
  42. data/lib/mack-haml.rb +1 -0
  43. metadata +65 -16
@@ -0,0 +1,30 @@
1
+ require 'sass/constant/string'
2
+ require 'sass/constant/number'
3
+ require 'sass/constant/color'
4
+
5
+ module Sass::Constant # :nodoc:
6
+ class Operation # :nodoc:
7
+ def initialize(operand1, operand2, operator)
8
+ @operand1 = operand1
9
+ @operand2 = operand2
10
+ @operator = operator
11
+ end
12
+
13
+ def to_s
14
+ self.perform.to_s
15
+ end
16
+
17
+ protected
18
+
19
+ def perform
20
+ literal1 = @operand1.perform
21
+ literal2 = @operand2.perform
22
+ begin
23
+ literal1.send(@operator, literal2)
24
+ rescue NoMethodError => e
25
+ raise e unless e.name.to_s == @operator.to_s
26
+ raise Sass::SyntaxError.new("Undefined operation: \"#{literal1} #{@operator} #{literal2}\".")
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,22 @@
1
+ require 'sass/constant/literal'
2
+
3
+ module Sass::Constant # :nodoc:
4
+ class String < Literal # :nodoc:
5
+
6
+ def parse(value)
7
+ @value = value
8
+ end
9
+
10
+ def plus(other)
11
+ Sass::Constant::String.from_value(self.to_s + other.to_s)
12
+ end
13
+
14
+ def funcall(other)
15
+ Sass::Constant::String.from_value("#{self.to_s}(#{other.to_s})")
16
+ end
17
+
18
+ def to_s
19
+ @value
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,394 @@
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(opts = {})
10
+ result = ''
11
+
12
+ children.each do |child|
13
+ result << "#{child.to_sass(0, opts)}\n"
14
+ end
15
+
16
+ result
17
+ end
18
+ end
19
+
20
+ class ValueNode
21
+ def to_sass(tabs, opts = {})
22
+ "#{value}\n"
23
+ end
24
+ end
25
+
26
+ class RuleNode
27
+ def to_sass(tabs, opts = {})
28
+ str = "\n#{' ' * tabs}#{rule}#{children.any? { |c| c.is_a? AttrNode } ? "\n" : ''}"
29
+
30
+ children.each do |child|
31
+ str << "#{child.to_sass(tabs + 1, opts)}"
32
+ end
33
+
34
+ str
35
+ end
36
+ end
37
+
38
+ class AttrNode
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"
47
+ end
48
+ end
49
+ end
50
+
51
+ # This class is based on the Ruby 1.9 ordered hashes.
52
+ # It keeps the semantics and most of the efficiency of normal hashes
53
+ # while also keeping track of the order in which elements were set.
54
+ class OrderedHash
55
+ Node = Struct.new(:key, :value, :next, :prev)
56
+ include Enumerable
57
+
58
+ def initialize
59
+ @hash = {}
60
+ end
61
+
62
+ def initialize_copy(other)
63
+ @hash = other.instance_variable_get('@hash').clone
64
+ end
65
+
66
+ def [](key)
67
+ @hash[key] && @hash[key].value
68
+ end
69
+
70
+ def []=(key, value)
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
+
81
+ if @first.nil?
82
+ @first = @last = node
83
+ else
84
+ node.prev = @last
85
+ @last.next = node
86
+ @last = node
87
+ end
88
+
89
+ @hash[key] = node
90
+ value
91
+ end
92
+
93
+ def each
94
+ return unless @first
95
+ yield [@first.key, @first.value]
96
+ node = @first
97
+ yield [node.key, node.value] while node = node.next
98
+ self
99
+ end
100
+
101
+ def values
102
+ self.map { |k, v| v }
103
+ end
104
+ end
105
+
106
+ # :startdoc:
107
+
108
+ # This class contains the functionality used in the +css2sass+ utility,
109
+ # namely converting CSS documents to Sass templates.
110
+ class CSS
111
+
112
+ # Creates a new instance of Sass::CSS that will compile the given document
113
+ # to a Sass string when +render+ is called.
114
+ def initialize(template, options = {})
115
+ if template.is_a? IO
116
+ template = template.read
117
+ end
118
+
119
+ @options = options
120
+ @template = StringScanner.new(template)
121
+ end
122
+
123
+ # Processes the document and returns the result as a string
124
+ # containing the CSS template.
125
+ def render
126
+ begin
127
+ build_tree.to_sass(@options).strip + "\n"
128
+ rescue Exception => err
129
+ line = @template.string[0...@template.pos].split("\n").size
130
+
131
+ err.backtrace.unshift "(css):#{line}"
132
+ raise err
133
+ end
134
+ end
135
+
136
+ private
137
+
138
+ def build_tree
139
+ root = Tree::Node.new(nil)
140
+ whitespace
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
147
+ root
148
+ end
149
+
150
+ def rules(root)
151
+ while r = rule
152
+ root << r
153
+ whitespace
154
+ end
155
+ end
156
+
157
+ def rule
158
+ return unless rule = @template.scan(/[^\{\};]+/)
159
+ rule.strip!
160
+ directive = rule[0] == ?@
161
+
162
+ if directive
163
+ node = Tree::DirectiveNode.new(rule, nil)
164
+ return node if @template.scan(/;/)
165
+
166
+ assert_match /\{/
167
+ whitespace
168
+
169
+ rules(node)
170
+ return node
171
+ end
172
+
173
+ assert_match /\{/
174
+ node = Tree::RuleNode.new(rule, nil)
175
+ attributes(node)
176
+ return node
177
+ end
178
+
179
+ def attributes(rule)
180
+ while @template.scan(/[^:\}\s]+/)
181
+ name = @template[0]
182
+ whitespace
183
+
184
+ assert_match /:/
185
+
186
+ value = ''
187
+ while @template.scan(/[^;\s\}]+/)
188
+ value << @template[0] << whitespace
189
+ end
190
+
191
+ assert_match /(;|(?=\}))/
192
+ rule << Tree::AttrNode.new(name, value, nil)
193
+ end
194
+
195
+ assert_match /\}/
196
+ end
197
+
198
+ def whitespace
199
+ space = @template.scan(/\s*/) || ''
200
+
201
+ # If we've hit a comment,
202
+ # go past it and look for more whitespace
203
+ if @template.scan(/\/\*/)
204
+ @template.scan_until(/\*\//)
205
+ return space + whitespace
206
+ end
207
+ return space
208
+ end
209
+
210
+ def assert_match(re)
211
+ if !@template.scan(re)
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}")
216
+ end
217
+ whitespace
218
+ end
219
+
220
+ # Transform
221
+ #
222
+ # foo, bar, baz
223
+ # color: blue
224
+ #
225
+ # into
226
+ #
227
+ # foo
228
+ # color: blue
229
+ # bar
230
+ # color: blue
231
+ # baz
232
+ # color: blue
233
+ #
234
+ # Yes, this expands the amount of code,
235
+ # but it's necessary to get nesting to work properly.
236
+ def expand_commas(root)
237
+ root.children.map! do |child|
238
+ next child unless Tree::RuleNode === child && child.rule.include?(',')
239
+ child.rule.split(',').map do |rule|
240
+ node = Tree::RuleNode.new(rule.strip, nil)
241
+ node.children = child.children
242
+ node
243
+ end
244
+ end
245
+ root.children.flatten!
246
+ end
247
+
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
264
+ #
265
+ # foo
266
+ # color: green
267
+ # foo bar
268
+ # color: red
269
+ # foo baz
270
+ # color: blue
271
+ #
272
+ # becomes
273
+ #
274
+ # foo
275
+ # color: green
276
+ # & bar
277
+ # color: red
278
+ # & baz
279
+ # color: blue
280
+ #
281
+ def parent_ref_rules(root)
282
+ rules = OrderedHash.new
283
+ root.children.select { |c| Tree::RuleNode === c }.each do |child|
284
+ root.children.delete child
285
+ first, rest = child.rule.scan(/^(&?(?: .|[^ ])[^.#: \[]*)([.#: \[].*)?$/).first
286
+ rules[first] ||= Tree::RuleNode.new(first, nil)
287
+ if rest
288
+ child.rule = "&" + rest
289
+ rules[first] << child
290
+ else
291
+ rules[first].children += child.children
292
+ end
293
+ end
294
+
295
+ rules.values.each { |v| parent_ref_rules(v) }
296
+ root.children += rules.values
297
+ end
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
+
320
+ # Flatten rules so that
321
+ #
322
+ # foo
323
+ # bar
324
+ # baz
325
+ # color: red
326
+ #
327
+ # becomes
328
+ #
329
+ # foo bar baz
330
+ # color: red
331
+ #
332
+ # and
333
+ #
334
+ # foo
335
+ # &.bar
336
+ # color: blue
337
+ #
338
+ # becomes
339
+ #
340
+ # foo.bar
341
+ # color: blue
342
+ #
343
+ def flatten_rules(root)
344
+ root.children.each { |child| flatten_rule(child) if child.is_a?(Tree::RuleNode) }
345
+ end
346
+
347
+ def flatten_rule(rule)
348
+ while rule.children.size == 1 && rule.children.first.is_a?(Tree::RuleNode)
349
+ child = rule.children.first
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
+
357
+ rule.children = child.children
358
+ end
359
+
360
+ flatten_rules(rule)
361
+ end
362
+
363
+ # Transform
364
+ #
365
+ # foo
366
+ # bar
367
+ # color: blue
368
+ # baz
369
+ # color: blue
370
+ #
371
+ # into
372
+ #
373
+ # foo
374
+ # bar, baz
375
+ # color: blue
376
+ #
377
+ def fold_commas(root)
378
+ prev_rule = nil
379
+ root.children.map! do |child|
380
+ next child unless Tree::RuleNode === child
381
+
382
+ if prev_rule && prev_rule.children == child.children
383
+ prev_rule.rule << ", #{child.rule}"
384
+ next nil
385
+ end
386
+
387
+ fold_commas(child)
388
+ prev_rule = child
389
+ child
390
+ end
391
+ root.children.compact!
392
+ end
393
+ end
394
+ end
@@ -0,0 +1,466 @@
1
+ require 'sass/tree/node'
2
+ require 'sass/tree/value_node'
3
+ require 'sass/tree/rule_node'
4
+ require 'sass/tree/comment_node'
5
+ require 'sass/tree/attr_node'
6
+ require 'sass/tree/directive_node'
7
+ require 'sass/constant'
8
+ require 'sass/error'
9
+
10
+ module Sass
11
+ # This is the class where all the parsing and processing of the Sass
12
+ # template is done. It can be directly used by the user by creating a
13
+ # new instance and calling <tt>render</tt> to render the template. For example:
14
+ #
15
+ # template = File.load('stylesheets/sassy.sass')
16
+ # sass_engine = Sass::Engine.new(template)
17
+ # output = sass_engine.render
18
+ # puts output
19
+ class Engine
20
+ # The character that begins a CSS attribute.
21
+ ATTRIBUTE_CHAR = ?:
22
+
23
+ # The character that designates that
24
+ # an attribute should be assigned to the result of constant arithmetic.
25
+ SCRIPT_CHAR = ?=
26
+
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
+ # Designates a non-parsed rule.
43
+ ESCAPE_CHAR = ?\\
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
+
51
+ # The regex that matches and extracts data from
52
+ # attributes of the form <tt>:name attr</tt>.
53
+ ATTRIBUTE = /^:([^\s=:]+)\s*(=?)(?:\s+|$)(.*)/
54
+
55
+ # The regex that matches attributes of the form <tt>name: attr</tt>.
56
+ ATTRIBUTE_ALTERNATE_MATCHER = /^[^\s:]+\s*[=:](\s|$)/
57
+
58
+ # The regex that matches and extracts data from
59
+ # attributes of the form <tt>name: attr</tt>.
60
+ ATTRIBUTE_ALTERNATE = /^([^\s=:]+)(\s*=|:)(?:\s+|$)(.*)/
61
+
62
+ # Creates a new instace of Sass::Engine that will compile the given
63
+ # template string when <tt>render</tt> is called.
64
+ # See README.rdoc for available options.
65
+ #
66
+ #--
67
+ #
68
+ # TODO: Add current options to REFRENCE. Remember :filename!
69
+ #
70
+ # When adding options, remember to add information about them
71
+ # to README.rdoc!
72
+ #++
73
+ #
74
+ def initialize(template, options={})
75
+ @options = {
76
+ :style => :nested,
77
+ :load_paths => ['.']
78
+ }.merge! options
79
+ @template = template.split(/\r\n|\r|\n/)
80
+ @lines = []
81
+ @constants = {"important" => "!important"}
82
+ @mixins = {}
83
+ end
84
+
85
+ # Processes the template and returns the result as a string.
86
+ def render
87
+ begin
88
+ render_to_tree.to_s
89
+ rescue SyntaxError => err
90
+ unless err.sass_filename
91
+ err.add_backtrace_entry(@options[:filename])
92
+ end
93
+ raise err
94
+ end
95
+ end
96
+
97
+ alias_method :to_css, :render
98
+
99
+ protected
100
+
101
+ def constants
102
+ @constants
103
+ end
104
+
105
+ def mixins
106
+ @mixins
107
+ end
108
+
109
+ def render_to_tree
110
+ split_lines
111
+
112
+ root = Tree::Node.new(@options[:style])
113
+ index = 0
114
+ while @lines[index]
115
+ old_index = index
116
+ child, index = build_tree(index)
117
+
118
+ if child.is_a? Tree::Node
119
+ child.line = old_index + 1
120
+ root << child
121
+ elsif child.is_a? Array
122
+ child.each do |c|
123
+ root << c
124
+ end
125
+ end
126
+ end
127
+ @lines.clear
128
+
129
+ root
130
+ end
131
+
132
+ private
133
+
134
+ # Readies each line in the template for parsing,
135
+ # and computes the tabulation of the line.
136
+ def split_lines
137
+ @line = 0
138
+ old_tabs = nil
139
+ @template.each_with_index do |line, index|
140
+ @line += 1
141
+
142
+ tabs = count_tabs(line)
143
+
144
+ if line[0] == COMMENT_CHAR && line[1] == SASS_COMMENT_CHAR && tabs == 0
145
+ tabs = old_tabs
146
+ end
147
+
148
+ if tabs # if line isn't blank
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)
153
+ end
154
+ @lines << [line.strip, tabs]
155
+
156
+ old_tabs = tabs
157
+ else
158
+ @lines << ['//', old_tabs || 0]
159
+ end
160
+ end
161
+
162
+ @line = nil
163
+ end
164
+
165
+ # Counts the tabulation of a line.
166
+ def count_tabs(line)
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
179
+ end
180
+ spaces / 2
181
+ end
182
+
183
+ def build_tree(index)
184
+ line, tabs = @lines[index]
185
+ index += 1
186
+ @line = index
187
+ node = parse_line(line)
188
+
189
+ has_children = has_children?(index, tabs)
190
+
191
+ # Node is a symbol if it's non-outputting, like a constant assignment
192
+ unless node.is_a? Tree::Node
193
+ if has_children
194
+ if node == :constant
195
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath constants.", @line + 1)
196
+ elsif node.is_a? Array
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)
203
+ end
204
+ end
205
+
206
+ index = @line if node == :mixin
207
+ return node, index
208
+ end
209
+
210
+ node.line = @line
211
+
212
+ if node.is_a? Tree::CommentNode
213
+ while has_children
214
+ line, index = raw_next_line(index)
215
+ node << line
216
+
217
+ has_children = has_children?(index, tabs)
218
+ end
219
+
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)
229
+ end
230
+
231
+ node.add_rules child
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)
242
+ end
243
+
244
+ return node, index
245
+ end
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
+
265
+ def has_children?(index, tabs)
266
+ next_line = ['//', 0]
267
+ while !next_line.nil? && next_line[0] == '//' && next_line[1] = 0
268
+ next_line = @lines[index]
269
+ index += 1
270
+ end
271
+ next_line && next_line[1] > tabs
272
+ end
273
+
274
+ def raw_next_line(index)
275
+ [@lines[index][0], index + 1]
276
+ end
277
+
278
+ def parse_line(line)
279
+ case line[0]
280
+ when ATTRIBUTE_CHAR
281
+ if line[1] != ATTRIBUTE_CHAR
282
+ parse_attribute(line, ATTRIBUTE)
283
+ else
284
+ # Support CSS3-style pseudo-elements,
285
+ # which begin with ::
286
+ Tree::RuleNode.new(line, @options[:style])
287
+ end
288
+ when Constant::CONSTANT_CHAR
289
+ parse_constant(line)
290
+ when COMMENT_CHAR
291
+ parse_comment(line)
292
+ when DIRECTIVE_CHAR
293
+ parse_directive(line)
294
+ when ESCAPE_CHAR
295
+ Tree::RuleNode.new(line[1..-1], @options[:style])
296
+ when MIXIN_DEFINITION_CHAR
297
+ parse_mixin_definition(line)
298
+ when MIXIN_INCLUDE_CHAR
299
+ if line[1].nil? || line[1] == ?\s
300
+ Tree::RuleNode.new(line, @options[:style])
301
+ else
302
+ parse_mixin_include(line)
303
+ end
304
+ else
305
+ if line =~ ATTRIBUTE_ALTERNATE_MATCHER
306
+ parse_attribute(line, ATTRIBUTE_ALTERNATE)
307
+ else
308
+ Tree::RuleNode.new(line, @options[:style])
309
+ end
310
+ end
311
+ end
312
+
313
+ def parse_attribute(line, attribute_regx)
314
+ if @options[:attribute_syntax] == :normal &&
315
+ attribute_regx == ATTRIBUTE_ALTERNATE
316
+ raise SyntaxError.new("Illegal attribute syntax: can't use alternate syntax when :attribute_syntax => :normal is set.")
317
+ elsif @options[:attribute_syntax] == :alternate &&
318
+ attribute_regx == ATTRIBUTE
319
+ raise SyntaxError.new("Illegal attribute syntax: can't use normal syntax when :attribute_syntax => :alternate is set.")
320
+ end
321
+
322
+ name, eq, value = line.scan(attribute_regx)[0]
323
+
324
+ if name.nil? || value.nil?
325
+ raise SyntaxError.new("Invalid attribute: \"#{line}\".", @line)
326
+ end
327
+
328
+ if eq.strip[0] == SCRIPT_CHAR
329
+ value = Sass::Constant.parse(value, @constants, @line).to_s
330
+ end
331
+
332
+ Tree::AttrNode.new(name, value, @options[:style])
333
+ end
334
+
335
+ def parse_constant(line)
336
+ name, op, value = line.scan(Sass::Constant::MATCH)[0]
337
+ unless name && value
338
+ raise SyntaxError.new("Invalid constant: \"#{line}\".", @line)
339
+ end
340
+
341
+ constant = Sass::Constant.parse(value, @constants, @line)
342
+ if op == '||='
343
+ @constants[name] ||= constant
344
+ else
345
+ @constants[name] = constant
346
+ end
347
+
348
+ :constant
349
+ end
350
+
351
+ def parse_comment(line)
352
+ if line[1] == SASS_COMMENT_CHAR
353
+ :comment
354
+ elsif line[1] == CSS_COMMENT_CHAR
355
+ Tree::CommentNode.new(line, @options[:style])
356
+ else
357
+ Tree::RuleNode.new(line, @options[:style])
358
+ end
359
+ end
360
+
361
+ def parse_directive(line)
362
+ directive, value = line[1..-1].split(/\s+/, 2)
363
+
364
+ # If value begins with url( or ",
365
+ # it's a CSS @import rule and we don't want to touch it.
366
+ if directive == "import" && value !~ /^(url\(|")/
367
+ import(value)
368
+ else
369
+ Tree::DirectiveNode.new(line, @options[:style])
370
+ end
371
+ end
372
+
373
+ def parse_mixin_definition(line)
374
+ mixin_name = line[1..-1].strip
375
+ @mixins[mixin_name] = []
376
+ index = @line
377
+ line, tabs = @lines[index]
378
+ while !line.nil? && tabs > 0
379
+ child, index = build_tree(index)
380
+ validate_and_append_child(@mixins[mixin_name], child)
381
+ line, tabs = @lines[index]
382
+ end
383
+ :mixin
384
+ end
385
+
386
+ def parse_mixin_include(line)
387
+ mixin_name = line[1..-1]
388
+ unless @mixins.has_key?(mixin_name)
389
+ raise SyntaxError.new("Undefined mixin '#{mixin_name}'.", @line)
390
+ end
391
+ @mixins[mixin_name]
392
+ end
393
+
394
+ def import(files)
395
+ nodes = []
396
+
397
+ files.split(/,\s*/).each do |filename|
398
+ engine = nil
399
+
400
+ begin
401
+ filename = self.class.find_file_to_import(filename, @options[:load_paths])
402
+ rescue Exception => e
403
+ raise SyntaxError.new(e.message, @line)
404
+ end
405
+
406
+ if filename =~ /\.css$/
407
+ nodes << Tree::DirectiveNode.new("@import url(#{filename})", @options[:style])
408
+ else
409
+ File.open(filename) do |file|
410
+ new_options = @options.dup
411
+ new_options[:filename] = filename
412
+ engine = Sass::Engine.new(file.read, @options)
413
+ end
414
+
415
+ engine.constants.merge! @constants
416
+ engine.mixins.merge! @mixins
417
+
418
+ begin
419
+ root = engine.render_to_tree
420
+ rescue Sass::SyntaxError => err
421
+ err.add_backtrace_entry(filename)
422
+ raise err
423
+ end
424
+ root.children.each do |child|
425
+ child.filename = filename
426
+ nodes << child
427
+ end
428
+ @constants = engine.constants
429
+ @mixins = engine.mixins
430
+ end
431
+ end
432
+
433
+ nodes
434
+ end
435
+
436
+ def self.find_file_to_import(filename, load_paths)
437
+ was_sass = false
438
+ original_filename = filename
439
+
440
+ if filename[-5..-1] == ".sass"
441
+ filename = filename[0...-5]
442
+ was_sass = true
443
+ elsif filename[-4..-1] == ".css"
444
+ return filename
445
+ end
446
+
447
+ new_filename = find_full_path("#{filename}.sass", load_paths)
448
+
449
+ return new_filename if new_filename
450
+ return filename + '.css' unless was_sass
451
+ raise SyntaxError.new("File to import not found or unreadable: #{original_filename}.", @line)
452
+ end
453
+
454
+ def self.find_full_path(filename, load_paths)
455
+ load_paths.each do |path|
456
+ ["_#{filename}", filename].each do |name|
457
+ full_path = File.join(path, name)
458
+ if File.readable?(full_path)
459
+ return full_path
460
+ end
461
+ end
462
+ end
463
+ nil
464
+ end
465
+ end
466
+ end