mack-haml 0.8.1 → 0.8.2

Sign up to get free protection for your applications and to get access to all the features.
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