merbjedi-haml 2.1.0

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 (177) hide show
  1. data/FAQ +138 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +332 -0
  4. data/REVISION +1 -0
  5. data/Rakefile +184 -0
  6. data/VERSION +1 -0
  7. data/bin/css2sass +7 -0
  8. data/bin/haml +9 -0
  9. data/bin/html2haml +7 -0
  10. data/bin/sass +8 -0
  11. data/extra/haml-mode.el +434 -0
  12. data/extra/sass-mode.el +98 -0
  13. data/init.rb +8 -0
  14. data/lib/haml.rb +1025 -0
  15. data/lib/haml/buffer.rb +255 -0
  16. data/lib/haml/engine.rb +268 -0
  17. data/lib/haml/error.rb +22 -0
  18. data/lib/haml/exec.rb +395 -0
  19. data/lib/haml/filters.rb +276 -0
  20. data/lib/haml/helpers.rb +465 -0
  21. data/lib/haml/helpers/action_view_extensions.rb +45 -0
  22. data/lib/haml/helpers/action_view_mods.rb +181 -0
  23. data/lib/haml/html.rb +218 -0
  24. data/lib/haml/precompiler.rb +896 -0
  25. data/lib/haml/shared.rb +45 -0
  26. data/lib/haml/template.rb +51 -0
  27. data/lib/haml/template/patch.rb +58 -0
  28. data/lib/haml/template/plugin.rb +72 -0
  29. data/lib/haml/util.rb +77 -0
  30. data/lib/haml/version.rb +47 -0
  31. data/lib/sass.rb +1062 -0
  32. data/lib/sass/css.rb +388 -0
  33. data/lib/sass/engine.rb +501 -0
  34. data/lib/sass/environment.rb +33 -0
  35. data/lib/sass/error.rb +35 -0
  36. data/lib/sass/plugin.rb +203 -0
  37. data/lib/sass/plugin/merb.rb +56 -0
  38. data/lib/sass/plugin/rails.rb +24 -0
  39. data/lib/sass/repl.rb +44 -0
  40. data/lib/sass/script.rb +38 -0
  41. data/lib/sass/script/bool.rb +13 -0
  42. data/lib/sass/script/color.rb +97 -0
  43. data/lib/sass/script/funcall.rb +28 -0
  44. data/lib/sass/script/functions.rb +122 -0
  45. data/lib/sass/script/lexer.rb +144 -0
  46. data/lib/sass/script/literal.rb +60 -0
  47. data/lib/sass/script/number.rb +231 -0
  48. data/lib/sass/script/operation.rb +30 -0
  49. data/lib/sass/script/parser.rb +142 -0
  50. data/lib/sass/script/string.rb +42 -0
  51. data/lib/sass/script/unary_operation.rb +21 -0
  52. data/lib/sass/script/variable.rb +20 -0
  53. data/lib/sass/tree/attr_node.rb +64 -0
  54. data/lib/sass/tree/comment_node.rb +30 -0
  55. data/lib/sass/tree/debug_node.rb +22 -0
  56. data/lib/sass/tree/directive_node.rb +50 -0
  57. data/lib/sass/tree/file_node.rb +27 -0
  58. data/lib/sass/tree/for_node.rb +29 -0
  59. data/lib/sass/tree/if_node.rb +27 -0
  60. data/lib/sass/tree/mixin_def_node.rb +18 -0
  61. data/lib/sass/tree/mixin_node.rb +34 -0
  62. data/lib/sass/tree/node.rb +97 -0
  63. data/lib/sass/tree/rule_node.rb +120 -0
  64. data/lib/sass/tree/variable_node.rb +24 -0
  65. data/lib/sass/tree/while_node.rb +20 -0
  66. data/rails/init.rb +1 -0
  67. data/test/benchmark.rb +99 -0
  68. data/test/haml/engine_test.rb +852 -0
  69. data/test/haml/helper_test.rb +224 -0
  70. data/test/haml/html2haml_test.rb +92 -0
  71. data/test/haml/markaby/standard.mab +52 -0
  72. data/test/haml/mocks/article.rb +6 -0
  73. data/test/haml/results/content_for_layout.xhtml +15 -0
  74. data/test/haml/results/eval_suppressed.xhtml +9 -0
  75. data/test/haml/results/filters.xhtml +62 -0
  76. data/test/haml/results/helpers.xhtml +93 -0
  77. data/test/haml/results/helpful.xhtml +10 -0
  78. data/test/haml/results/just_stuff.xhtml +68 -0
  79. data/test/haml/results/list.xhtml +12 -0
  80. data/test/haml/results/nuke_inner_whitespace.xhtml +40 -0
  81. data/test/haml/results/nuke_outer_whitespace.xhtml +148 -0
  82. data/test/haml/results/original_engine.xhtml +20 -0
  83. data/test/haml/results/partial_layout.xhtml +5 -0
  84. data/test/haml/results/partials.xhtml +21 -0
  85. data/test/haml/results/render_layout.xhtml +3 -0
  86. data/test/haml/results/silent_script.xhtml +74 -0
  87. data/test/haml/results/standard.xhtml +42 -0
  88. data/test/haml/results/tag_parsing.xhtml +23 -0
  89. data/test/haml/results/very_basic.xhtml +5 -0
  90. data/test/haml/results/whitespace_handling.xhtml +89 -0
  91. data/test/haml/rhtml/_av_partial_1.rhtml +12 -0
  92. data/test/haml/rhtml/_av_partial_2.rhtml +8 -0
  93. data/test/haml/rhtml/action_view.rhtml +62 -0
  94. data/test/haml/rhtml/standard.rhtml +54 -0
  95. data/test/haml/template_test.rb +204 -0
  96. data/test/haml/templates/_av_partial_1.haml +9 -0
  97. data/test/haml/templates/_av_partial_1_ugly.haml +9 -0
  98. data/test/haml/templates/_av_partial_2.haml +5 -0
  99. data/test/haml/templates/_av_partial_2_ugly.haml +5 -0
  100. data/test/haml/templates/_layout.erb +3 -0
  101. data/test/haml/templates/_layout_for_partial.haml +3 -0
  102. data/test/haml/templates/_partial.haml +8 -0
  103. data/test/haml/templates/_text_area.haml +3 -0
  104. data/test/haml/templates/action_view.haml +47 -0
  105. data/test/haml/templates/action_view_ugly.haml +47 -0
  106. data/test/haml/templates/breakage.haml +8 -0
  107. data/test/haml/templates/content_for_layout.haml +10 -0
  108. data/test/haml/templates/eval_suppressed.haml +11 -0
  109. data/test/haml/templates/filters.haml +66 -0
  110. data/test/haml/templates/helpers.haml +95 -0
  111. data/test/haml/templates/helpful.haml +11 -0
  112. data/test/haml/templates/just_stuff.haml +83 -0
  113. data/test/haml/templates/list.haml +12 -0
  114. data/test/haml/templates/nuke_inner_whitespace.haml +32 -0
  115. data/test/haml/templates/nuke_outer_whitespace.haml +144 -0
  116. data/test/haml/templates/original_engine.haml +17 -0
  117. data/test/haml/templates/partial_layout.haml +3 -0
  118. data/test/haml/templates/partialize.haml +1 -0
  119. data/test/haml/templates/partials.haml +12 -0
  120. data/test/haml/templates/render_layout.haml +2 -0
  121. data/test/haml/templates/silent_script.haml +40 -0
  122. data/test/haml/templates/standard.haml +42 -0
  123. data/test/haml/templates/standard_ugly.haml +42 -0
  124. data/test/haml/templates/tag_parsing.haml +21 -0
  125. data/test/haml/templates/very_basic.haml +4 -0
  126. data/test/haml/templates/whitespace_handling.haml +87 -0
  127. data/test/linked_rails.rb +12 -0
  128. data/test/sass/css2sass_test.rb +193 -0
  129. data/test/sass/engine_test.rb +752 -0
  130. data/test/sass/functions_test.rb +96 -0
  131. data/test/sass/more_results/more1.css +9 -0
  132. data/test/sass/more_results/more1_with_line_comments.css +26 -0
  133. data/test/sass/more_results/more_import.css +29 -0
  134. data/test/sass/more_templates/_more_partial.sass +2 -0
  135. data/test/sass/more_templates/more1.sass +23 -0
  136. data/test/sass/more_templates/more_import.sass +11 -0
  137. data/test/sass/plugin_test.rb +208 -0
  138. data/test/sass/results/alt.css +4 -0
  139. data/test/sass/results/basic.css +9 -0
  140. data/test/sass/results/compact.css +5 -0
  141. data/test/sass/results/complex.css +87 -0
  142. data/test/sass/results/compressed.css +1 -0
  143. data/test/sass/results/expanded.css +19 -0
  144. data/test/sass/results/import.css +29 -0
  145. data/test/sass/results/line_numbers.css +49 -0
  146. data/test/sass/results/mixins.css +95 -0
  147. data/test/sass/results/multiline.css +24 -0
  148. data/test/sass/results/nested.css +22 -0
  149. data/test/sass/results/parent_ref.css +13 -0
  150. data/test/sass/results/script.css +16 -0
  151. data/test/sass/results/subdir/nested_subdir/nested_subdir.css +1 -0
  152. data/test/sass/results/subdir/subdir.css +3 -0
  153. data/test/sass/results/units.css +11 -0
  154. data/test/sass/script_test.rb +152 -0
  155. data/test/sass/templates/_partial.sass +2 -0
  156. data/test/sass/templates/alt.sass +16 -0
  157. data/test/sass/templates/basic.sass +23 -0
  158. data/test/sass/templates/bork.sass +2 -0
  159. data/test/sass/templates/bork2.sass +2 -0
  160. data/test/sass/templates/compact.sass +17 -0
  161. data/test/sass/templates/complex.sass +309 -0
  162. data/test/sass/templates/compressed.sass +15 -0
  163. data/test/sass/templates/expanded.sass +17 -0
  164. data/test/sass/templates/import.sass +11 -0
  165. data/test/sass/templates/importee.sass +19 -0
  166. data/test/sass/templates/line_numbers.sass +13 -0
  167. data/test/sass/templates/mixins.sass +76 -0
  168. data/test/sass/templates/multiline.sass +20 -0
  169. data/test/sass/templates/nested.sass +25 -0
  170. data/test/sass/templates/parent_ref.sass +25 -0
  171. data/test/sass/templates/script.sass +101 -0
  172. data/test/sass/templates/subdir/nested_subdir/_nested_partial.sass +2 -0
  173. data/test/sass/templates/subdir/nested_subdir/nested_subdir.sass +3 -0
  174. data/test/sass/templates/subdir/subdir.sass +6 -0
  175. data/test/sass/templates/units.sass +11 -0
  176. data/test/test_helper.rb +21 -0
  177. metadata +273 -0
@@ -0,0 +1,388 @@
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 RuleNode
21
+ def to_sass(tabs, opts = {})
22
+ str = "\n#{' ' * tabs}#{rule}#{children.any? { |c| c.is_a? AttrNode } ? "\n" : ''}"
23
+
24
+ children.each do |child|
25
+ str << "#{child.to_sass(tabs + 1, opts)}"
26
+ end
27
+
28
+ str
29
+ end
30
+ end
31
+
32
+ class AttrNode
33
+ def to_sass(tabs, opts = {})
34
+ "#{' ' * tabs}#{opts[:alternate] ? '' : ':'}#{name}#{opts[:alternate] ? ':' : ''} #{value}\n"
35
+ end
36
+ end
37
+
38
+ class DirectiveNode
39
+ def to_sass(tabs, opts = {})
40
+ "#{' ' * tabs}#{value}#{children.map {|c| c.to_sass(tabs + 1, opts)}}\n"
41
+ end
42
+ end
43
+ end
44
+
45
+ # This class is based on the Ruby 1.9 ordered hashes.
46
+ # It keeps the semantics and most of the efficiency of normal hashes
47
+ # while also keeping track of the order in which elements were set.
48
+ class OrderedHash
49
+ Node = Struct.new(:key, :value, :next, :prev)
50
+ include Enumerable
51
+
52
+ def initialize
53
+ @hash = {}
54
+ end
55
+
56
+ def initialize_copy(other)
57
+ @hash = other.instance_variable_get('@hash').clone
58
+ end
59
+
60
+ def [](key)
61
+ @hash[key] && @hash[key].value
62
+ end
63
+
64
+ def []=(key, value)
65
+ node = Node.new(key, value)
66
+
67
+ if old = @hash[key]
68
+ if old.prev
69
+ old.prev.next = old.next
70
+ else # old is @first and @last
71
+ @first = @last = nil
72
+ end
73
+ end
74
+
75
+ if @first.nil?
76
+ @first = @last = node
77
+ else
78
+ node.prev = @last
79
+ @last.next = node
80
+ @last = node
81
+ end
82
+
83
+ @hash[key] = node
84
+ value
85
+ end
86
+
87
+ def each
88
+ return unless @first
89
+ yield [@first.key, @first.value]
90
+ node = @first
91
+ yield [node.key, node.value] while node = node.next
92
+ self
93
+ end
94
+
95
+ def values
96
+ self.map { |k, v| v }
97
+ end
98
+ end
99
+
100
+ # :startdoc:
101
+
102
+ # This class contains the functionality used in the +css2sass+ utility,
103
+ # namely converting CSS documents to Sass templates.
104
+ class CSS
105
+
106
+ # Creates a new instance of Sass::CSS that will compile the given document
107
+ # to a Sass string when +render+ is called.
108
+ def initialize(template, options = {})
109
+ if template.is_a? IO
110
+ template = template.read
111
+ end
112
+
113
+ @options = options
114
+ @template = StringScanner.new(template)
115
+ end
116
+
117
+ # Processes the document and returns the result as a string
118
+ # containing the CSS template.
119
+ def render
120
+ begin
121
+ build_tree.to_sass(@options).strip + "\n"
122
+ rescue Exception => err
123
+ line = @template.string[0...@template.pos].split("\n").size
124
+
125
+ err.backtrace.unshift "(css):#{line}"
126
+ raise err
127
+ end
128
+ end
129
+
130
+ private
131
+
132
+ def build_tree
133
+ root = Tree::Node.new({})
134
+ whitespace
135
+ rules root
136
+ expand_commas root
137
+ parent_ref_rules root
138
+ remove_parent_refs root
139
+ flatten_rules root
140
+ fold_commas root
141
+ root
142
+ end
143
+
144
+ def rules(root)
145
+ while r = rule
146
+ root << r
147
+ whitespace
148
+ end
149
+ end
150
+
151
+ def rule
152
+ return unless rule = @template.scan(/[^\{\};]+/)
153
+ rule.strip!
154
+ directive = rule[0] == ?@
155
+
156
+ if directive
157
+ node = Tree::DirectiveNode.new(rule, {})
158
+ return node if @template.scan(/;/)
159
+
160
+ assert_match /\{/
161
+ whitespace
162
+
163
+ rules(node)
164
+ return node
165
+ end
166
+
167
+ assert_match /\{/
168
+ node = Tree::RuleNode.new(rule, {})
169
+ attributes(node)
170
+ return node
171
+ end
172
+
173
+ def attributes(rule)
174
+ while @template.scan(/[^:\}\s]+/)
175
+ name = @template[0]
176
+ whitespace
177
+
178
+ assert_match /:/
179
+
180
+ value = ''
181
+ while @template.scan(/[^;\s\}]+/)
182
+ value << @template[0] << whitespace
183
+ end
184
+
185
+ assert_match /(;|(?=\}))/
186
+ rule << Tree::AttrNode.new(name, value, {})
187
+ end
188
+
189
+ assert_match /\}/
190
+ end
191
+
192
+ def whitespace
193
+ space = @template.scan(/\s*/) || ''
194
+
195
+ # If we've hit a comment,
196
+ # go past it and look for more whitespace
197
+ if @template.scan(/\/\*/)
198
+ @template.scan_until(/\*\//)
199
+ return space + whitespace
200
+ end
201
+ return space
202
+ end
203
+
204
+ def assert_match(re)
205
+ if !@template.scan(re)
206
+ line = @template.string[0..@template.pos].count "\n"
207
+ # Display basic regexps as plain old strings
208
+ expected = re.source == Regexp.escape(re.source) ? "\"#{re.source}\"" : re.inspect
209
+ raise Exception.new("Invalid CSS on line #{line}: expected #{expected}")
210
+ end
211
+ whitespace
212
+ end
213
+
214
+ # Transform
215
+ #
216
+ # foo, bar, baz
217
+ # color: blue
218
+ #
219
+ # into
220
+ #
221
+ # foo
222
+ # color: blue
223
+ # bar
224
+ # color: blue
225
+ # baz
226
+ # color: blue
227
+ #
228
+ # Yes, this expands the amount of code,
229
+ # but it's necessary to get nesting to work properly.
230
+ def expand_commas(root)
231
+ root.children.map! do |child|
232
+ next child unless Tree::RuleNode === child && child.rule.include?(',')
233
+ child.rule.split(',').map do |rule|
234
+ node = Tree::RuleNode.new(rule.strip, {})
235
+ node.children = child.children
236
+ node
237
+ end
238
+ end
239
+ root.children.flatten!
240
+ end
241
+
242
+ # Make rules use parent refs so that
243
+ #
244
+ # foo
245
+ # color: green
246
+ # foo.bar
247
+ # color: blue
248
+ #
249
+ # becomes
250
+ #
251
+ # foo
252
+ # color: green
253
+ # &.bar
254
+ # color: blue
255
+ #
256
+ # This has the side effect of nesting rules,
257
+ # so that
258
+ #
259
+ # foo
260
+ # color: green
261
+ # foo bar
262
+ # color: red
263
+ # foo baz
264
+ # color: blue
265
+ #
266
+ # becomes
267
+ #
268
+ # foo
269
+ # color: green
270
+ # & bar
271
+ # color: red
272
+ # & baz
273
+ # color: blue
274
+ #
275
+ def parent_ref_rules(root)
276
+ rules = OrderedHash.new
277
+ root.children.select { |c| Tree::RuleNode === c }.each do |child|
278
+ root.children.delete child
279
+ first, rest = child.rule.scan(/^(&?(?: .|[^ ])[^.#: \[]*)([.#: \[].*)?$/).first
280
+ rules[first] ||= Tree::RuleNode.new(first, {})
281
+ if rest
282
+ child.rule = "&" + rest
283
+ rules[first] << child
284
+ else
285
+ rules[first].children += child.children
286
+ end
287
+ end
288
+
289
+ rules.values.each { |v| parent_ref_rules(v) }
290
+ root.children += rules.values
291
+ end
292
+
293
+ # Remove useless parent refs so that
294
+ #
295
+ # foo
296
+ # & bar
297
+ # color: blue
298
+ #
299
+ # becomes
300
+ #
301
+ # foo
302
+ # bar
303
+ # color: blue
304
+ #
305
+ def remove_parent_refs(root)
306
+ root.children.each do |child|
307
+ if child.is_a?(Tree::RuleNode)
308
+ child.rule.gsub! /^& +/, ''
309
+ remove_parent_refs child
310
+ end
311
+ end
312
+ end
313
+
314
+ # Flatten rules so that
315
+ #
316
+ # foo
317
+ # bar
318
+ # baz
319
+ # color: red
320
+ #
321
+ # becomes
322
+ #
323
+ # foo bar baz
324
+ # color: red
325
+ #
326
+ # and
327
+ #
328
+ # foo
329
+ # &.bar
330
+ # color: blue
331
+ #
332
+ # becomes
333
+ #
334
+ # foo.bar
335
+ # color: blue
336
+ #
337
+ def flatten_rules(root)
338
+ root.children.each { |child| flatten_rule(child) if child.is_a?(Tree::RuleNode) }
339
+ end
340
+
341
+ def flatten_rule(rule)
342
+ while rule.children.size == 1 && rule.children.first.is_a?(Tree::RuleNode)
343
+ child = rule.children.first
344
+
345
+ if child.rule[0] == ?&
346
+ rule.rule = child.rule.gsub /^&/, rule.rule
347
+ else
348
+ rule.rule = "#{rule.rule} #{child.rule}"
349
+ end
350
+
351
+ rule.children = child.children
352
+ end
353
+
354
+ flatten_rules(rule)
355
+ end
356
+
357
+ # Transform
358
+ #
359
+ # foo
360
+ # bar
361
+ # color: blue
362
+ # baz
363
+ # color: blue
364
+ #
365
+ # into
366
+ #
367
+ # foo
368
+ # bar, baz
369
+ # color: blue
370
+ #
371
+ def fold_commas(root)
372
+ prev_rule = nil
373
+ root.children.map! do |child|
374
+ next child unless child.is_a?(Tree::RuleNode)
375
+
376
+ if prev_rule && prev_rule.children == child.children
377
+ prev_rule.rule << ", #{child.rule}"
378
+ next nil
379
+ end
380
+
381
+ fold_commas(child)
382
+ prev_rule = child
383
+ child
384
+ end
385
+ root.children.compact!
386
+ end
387
+ end
388
+ end
@@ -0,0 +1,501 @@
1
+ require 'strscan'
2
+ require 'sass/tree/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/tree/variable_node'
8
+ require 'sass/tree/mixin_def_node'
9
+ require 'sass/tree/mixin_node'
10
+ require 'sass/tree/if_node'
11
+ require 'sass/tree/while_node'
12
+ require 'sass/tree/for_node'
13
+ require 'sass/tree/debug_node'
14
+ require 'sass/tree/file_node'
15
+ require 'sass/environment'
16
+ require 'sass/script'
17
+ require 'sass/error'
18
+ require 'haml/shared'
19
+
20
+ module Sass
21
+ # :stopdoc:
22
+ Mixin = Struct.new(:name, :args, :environment, :tree)
23
+ # :startdoc:
24
+
25
+ # This is the class where all the parsing and processing of the Sass
26
+ # template is done. It can be directly used by the user by creating a
27
+ # new instance and calling <tt>render</tt> to render the template. For example:
28
+ #
29
+ # template = File.load('stylesheets/sassy.sass')
30
+ # sass_engine = Sass::Engine.new(template)
31
+ # output = sass_engine.render
32
+ # puts output
33
+ class Engine
34
+ include Haml::Util
35
+ Line = Struct.new(:text, :tabs, :index, :offset, :filename, :children)
36
+
37
+ # The character that begins a CSS attribute.
38
+ ATTRIBUTE_CHAR = ?:
39
+
40
+ # The character that designates that
41
+ # an attribute should be assigned to a SassScript expression.
42
+ SCRIPT_CHAR = ?=
43
+
44
+ # The character that designates the beginning of a comment,
45
+ # either Sass or CSS.
46
+ COMMENT_CHAR = ?/
47
+
48
+ # The character that follows the general COMMENT_CHAR and designates a Sass comment,
49
+ # which is not output as a CSS comment.
50
+ SASS_COMMENT_CHAR = ?/
51
+
52
+ # The character that follows the general COMMENT_CHAR and designates a CSS comment,
53
+ # which is embedded in the CSS document.
54
+ CSS_COMMENT_CHAR = ?*
55
+
56
+ # The character used to denote a compiler directive.
57
+ DIRECTIVE_CHAR = ?@
58
+
59
+ # Designates a non-parsed rule.
60
+ ESCAPE_CHAR = ?\\
61
+
62
+ # Designates block as mixin definition rather than CSS rules to output
63
+ MIXIN_DEFINITION_CHAR = ?=
64
+
65
+ # Includes named mixin declared using MIXIN_DEFINITION_CHAR
66
+ MIXIN_INCLUDE_CHAR = ?+
67
+
68
+ # The regex that matches and extracts data from
69
+ # attributes of the form <tt>:name attr</tt>.
70
+ ATTRIBUTE = /^:([^\s=:]+)\s*(=?)(?:\s+|$)(.*)/
71
+
72
+ # The regex that matches attributes of the form <tt>name: attr</tt>.
73
+ ATTRIBUTE_ALTERNATE_MATCHER = /^[^\s:]+\s*[=:](\s|$)/
74
+
75
+ # The regex that matches and extracts data from
76
+ # attributes of the form <tt>name: attr</tt>.
77
+ ATTRIBUTE_ALTERNATE = /^([^\s=:]+)(\s*=|:)(?:\s+|$)(.*)/
78
+
79
+ # Creates a new instace of Sass::Engine that will compile the given
80
+ # template string when <tt>render</tt> is called.
81
+ # See README.rdoc for available options.
82
+ #
83
+ #--
84
+ #
85
+ # TODO: Add current options to REFRENCE. Remember :filename!
86
+ #
87
+ # When adding options, remember to add information about them
88
+ # to README.rdoc!
89
+ #++
90
+ #
91
+ def initialize(template, options={})
92
+ @options = {
93
+ :style => :nested,
94
+ :load_paths => ['.']
95
+ }.merge! options
96
+ @template = template
97
+ @environment = Environment.new
98
+ @environment.set_var("important", Script::String.new("!important"))
99
+ end
100
+
101
+ # Processes the template and returns the result as a string.
102
+ def render
103
+ begin
104
+ render_to_tree.perform(@environment).to_s
105
+ rescue SyntaxError => err
106
+ err.sass_line = @line unless err.sass_line
107
+ unless err.sass_filename
108
+ err.add_backtrace_entry(@options[:filename])
109
+ end
110
+ raise err
111
+ end
112
+ end
113
+
114
+ alias_method :to_css, :render
115
+
116
+ protected
117
+
118
+ def environment
119
+ @environment
120
+ end
121
+
122
+ def render_to_tree
123
+ root = Tree::Node.new(@options)
124
+ append_children(root, tree(tabulate(@template)).first, true)
125
+ root
126
+ end
127
+
128
+ private
129
+
130
+ def tabulate(string)
131
+ tab_str = nil
132
+ first = true
133
+ enum_with_index(string.gsub(/\r|\n|\r\n|\r\n/, "\n").scan(/^.*?$/)).map do |line, index|
134
+ index += 1
135
+ next if line.strip.empty? || line =~ /^\/\//
136
+
137
+ line_tab_str = line[/^\s*/]
138
+ unless line_tab_str.empty?
139
+ tab_str ||= line_tab_str
140
+
141
+ raise SyntaxError.new("Indenting at the beginning of the document is illegal.", index) if first
142
+ if tab_str.include?(?\s) && tab_str.include?(?\t)
143
+ raise SyntaxError.new("Indentation can't use both tabs and spaces.", index)
144
+ end
145
+ end
146
+ first &&= !tab_str.nil?
147
+ next Line.new(line.strip, 0, index, 0, @options[:filename], []) if tab_str.nil?
148
+
149
+ line_tabs = line_tab_str.scan(tab_str).size
150
+ raise SyntaxError.new(<<END.strip.gsub("\n", ' '), index) if tab_str * line_tabs != line_tab_str
151
+ Inconsistent indentation: #{Haml::Shared.human_indentation line_tab_str, true} used for indentation,
152
+ but the rest of the document was indented using #{Haml::Shared.human_indentation tab_str}.
153
+ END
154
+ Line.new(line.strip, line_tabs, index, tab_str.size, @options[:filename], [])
155
+ end.compact
156
+ end
157
+
158
+ def tree(arr, i = 0)
159
+ return [], i if arr[i].nil?
160
+
161
+ base = arr[i].tabs
162
+ nodes = []
163
+ while (line = arr[i]) && line.tabs >= base
164
+ if line.tabs > base
165
+ if line.tabs > base + 1
166
+ raise SyntaxError.new("The line was indented #{line.tabs - base} levels deeper than the previous line.", line.index)
167
+ end
168
+
169
+ nodes.last.children, i = tree(arr, i)
170
+ else
171
+ nodes << line
172
+ i += 1
173
+ end
174
+ end
175
+ return nodes, i
176
+ end
177
+
178
+ def build_tree(parent, line, root = false)
179
+ @line = line.index
180
+ node = parse_line(parent, line, root)
181
+
182
+ # Node is a symbol if it's non-outputting, like a variable assignment,
183
+ # or an array if it's a group of nodes to add
184
+ return node unless node.is_a? Tree::Node
185
+
186
+ node.line = line.index
187
+ node.filename = line.filename
188
+
189
+ unless node.is_a?(Tree::CommentNode)
190
+ append_children(node, line.children, false)
191
+ else
192
+ node.children = line.children
193
+ end
194
+ return node
195
+ end
196
+
197
+ def append_children(parent, children, root)
198
+ continued_rule = nil
199
+ children.each do |line|
200
+ child = build_tree(parent, line, root)
201
+
202
+ if child.is_a?(Tree::RuleNode) && child.continued?
203
+ raise SyntaxError.new("Rules can't end in commas.", child.line) unless child.children.empty?
204
+ if continued_rule
205
+ continued_rule.add_rules child
206
+ else
207
+ continued_rule = child
208
+ end
209
+ next
210
+ end
211
+
212
+ if continued_rule
213
+ raise SyntaxError.new("Rules can't end in commas.", continued_rule.line) unless child.is_a?(Tree::RuleNode)
214
+ continued_rule.add_rules child
215
+ continued_rule.children = child.children
216
+ continued_rule, child = nil, continued_rule
217
+ end
218
+
219
+ validate_and_append_child(parent, child, line, root)
220
+ end
221
+
222
+ raise SyntaxError.new("Rules can't end in commas.", continued_rule.line) if continued_rule
223
+
224
+ parent
225
+ end
226
+
227
+ def validate_and_append_child(parent, child, line, root)
228
+ unless root
229
+ case child
230
+ when Tree::MixinDefNode
231
+ raise SyntaxError.new("Mixins may only be defined at the root of a document.", line.index)
232
+ when Tree::DirectiveNode
233
+ raise SyntaxError.new("Import directives may only be used at the root of a document.", line.index)
234
+ end
235
+ end
236
+
237
+ case child
238
+ when Array
239
+ child.each {|c| validate_and_append_child(parent, c, line, root)}
240
+ when Tree::Node
241
+ parent << child
242
+ end
243
+ end
244
+
245
+ def parse_line(parent, line, root)
246
+ case line.text[0]
247
+ when ATTRIBUTE_CHAR
248
+ if line.text[1] != ATTRIBUTE_CHAR
249
+ parse_attribute(line, ATTRIBUTE)
250
+ else
251
+ # Support CSS3-style pseudo-elements,
252
+ # which begin with ::
253
+ Tree::RuleNode.new(line.text, @options)
254
+ end
255
+ when Script::VARIABLE_CHAR
256
+ parse_variable(line)
257
+ when COMMENT_CHAR
258
+ parse_comment(line.text)
259
+ when DIRECTIVE_CHAR
260
+ parse_directive(parent, line, root)
261
+ when ESCAPE_CHAR
262
+ Tree::RuleNode.new(line.text[1..-1], @options)
263
+ when MIXIN_DEFINITION_CHAR
264
+ parse_mixin_definition(line)
265
+ when MIXIN_INCLUDE_CHAR
266
+ if line.text[1].nil?
267
+ Tree::RuleNode.new(line.text, @options)
268
+ else
269
+ parse_mixin_include(line, root)
270
+ end
271
+ else
272
+ if line.text =~ ATTRIBUTE_ALTERNATE_MATCHER
273
+ parse_attribute(line, ATTRIBUTE_ALTERNATE)
274
+ else
275
+ Tree::RuleNode.new(line.text, @options)
276
+ end
277
+ end
278
+ end
279
+
280
+ def parse_attribute(line, attribute_regx)
281
+ if @options[:attribute_syntax] == :normal &&
282
+ attribute_regx == ATTRIBUTE_ALTERNATE
283
+ raise SyntaxError.new("Illegal attribute syntax: can't use alternate syntax when :attribute_syntax => :normal is set.")
284
+ elsif @options[:attribute_syntax] == :alternate &&
285
+ attribute_regx == ATTRIBUTE
286
+ raise SyntaxError.new("Illegal attribute syntax: can't use normal syntax when :attribute_syntax => :alternate is set.")
287
+ end
288
+
289
+ name, eq, value = line.text.scan(attribute_regx)[0]
290
+
291
+ if name.nil? || value.nil?
292
+ raise SyntaxError.new("Invalid attribute: \"#{line.text}\".", @line)
293
+ end
294
+ expr = if (eq.strip[0] == SCRIPT_CHAR)
295
+ parse_script(value, :offset => line.offset + line.text.index(value))
296
+ else
297
+ value
298
+ end
299
+ Tree::AttrNode.new(name, expr, @options)
300
+ end
301
+
302
+ def parse_variable(line)
303
+ name, op, value = line.text.scan(Script::MATCH)[0]
304
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath variable declarations.", @line + 1) unless line.children.empty?
305
+ raise SyntaxError.new("Invalid variable: \"#{line.text}\".", @line) unless name && value
306
+
307
+ Tree::VariableNode.new(name, parse_script(value, :offset => line.offset + line.text.index(value)), op == '||=', @options)
308
+ end
309
+
310
+ def parse_comment(line)
311
+ if line[1] == SASS_COMMENT_CHAR
312
+ :comment
313
+ elsif line[1] == CSS_COMMENT_CHAR
314
+ Tree::CommentNode.new(line, @options)
315
+ else
316
+ Tree::RuleNode.new(line, @options)
317
+ end
318
+ end
319
+
320
+ def parse_directive(parent, line, root)
321
+ directive, whitespace, value = line.text[1..-1].split(/(\s+)/, 2)
322
+ offset = directive.size + whitespace.size + 1 if whitespace
323
+
324
+ # If value begins with url( or ",
325
+ # it's a CSS @import rule and we don't want to touch it.
326
+ if directive == "import" && value !~ /^(url\(|")/
327
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath import directives.", @line + 1) unless line.children.empty?
328
+ import(value)
329
+ elsif directive == "for"
330
+ parse_for(line, root, value)
331
+ elsif directive == "else"
332
+ parse_else(parent, line, value)
333
+ elsif directive == "while"
334
+ raise SyntaxError.new("Invalid while directive '@while': expected expression.") unless value
335
+ Tree::WhileNode.new(parse_script(value, :offset => offset), @options)
336
+ elsif directive == "if"
337
+ raise SyntaxError.new("Invalid if directive '@if': expected expression.") unless value
338
+ Tree::IfNode.new(parse_script(value, :offset => offset), @options)
339
+ elsif directive == "debug"
340
+ raise SyntaxError.new("Invalid debug directive '@debug': expected expression.") unless value
341
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath debug directives.", @line + 1) unless line.children.empty?
342
+ offset = line.offset + line.text.index(value).to_i
343
+ Tree::DebugNode.new(parse_script(value, :offset => offset), @options)
344
+ else
345
+ Tree::DirectiveNode.new(line.text, @options)
346
+ end
347
+ end
348
+
349
+ def parse_for(line, root, text)
350
+ var, from_expr, to_name, to_expr = text.scan(/^([^\s]+)\s+from\s+(.+)\s+(to|through)\s+(.+)$/).first
351
+
352
+ if var.nil? # scan failed, try to figure out why for error message
353
+ if text !~ /^[^\s]+/
354
+ expected = "variable name"
355
+ elsif text !~ /^[^\s]+\s+from\s+.+/
356
+ expected = "'from <expr>'"
357
+ else
358
+ expected = "'to <expr>' or 'through <expr>'"
359
+ end
360
+ raise SyntaxError.new("Invalid for directive '@for #{text}': expected #{expected}.", @line)
361
+ end
362
+ raise SyntaxError.new("Invalid variable \"#{var}\".", @line) unless var =~ Script::VALIDATE
363
+
364
+ parsed_from = parse_script(from_expr, :offset => line.offset + line.text.index(from_expr))
365
+ parsed_to = parse_script(to_expr, :offset => line.offset + line.text.index(to_expr))
366
+ Tree::ForNode.new(var[1..-1], parsed_from, parsed_to, to_name == 'to', @options)
367
+ end
368
+
369
+ def parse_else(parent, line, text)
370
+ previous = parent.last
371
+ raise SyntaxError.new("@else must come after @if.") unless previous.is_a?(Tree::IfNode)
372
+
373
+ if text
374
+ if text !~ /^if\s+(.+)/
375
+ raise SyntaxError.new("Invalid else directive '@else #{text}': expected 'if <expr>'.", @line)
376
+ end
377
+ expr = parse_script($1, :offset => line.offset + line.text.index($1))
378
+ end
379
+
380
+ node = Tree::IfNode.new(expr, @options)
381
+ append_children(node, line.children, false)
382
+ previous.add_else node
383
+ nil
384
+ end
385
+
386
+ # parses out the arguments between the commas and cleans up the mixin arguments
387
+ # returns nil if it fails to parse, otherwise an array.
388
+ def parse_mixin_arguments(arg_string)
389
+ arg_string = arg_string.strip
390
+ return [] if arg_string.empty?
391
+ return nil unless (arg_string[0] == ?( && arg_string[-1] == ?))
392
+ arg_string = arg_string[1...-1]
393
+ arg_string.split(",", -1).map {|a| a.strip}
394
+ end
395
+
396
+ def parse_mixin_definition(line)
397
+ name, arg_string = line.text.scan(/^=\s*([^(]+)(.*)$/).first
398
+ args = parse_mixin_arguments(arg_string)
399
+ raise SyntaxError.new("Invalid mixin \"#{line.text[1..-1]}\".", @line) if name.nil? || args.nil?
400
+ default_arg_found = false
401
+ required_arg_count = 0
402
+ args.map! do |arg|
403
+ raise SyntaxError.new("Mixin arguments can't be empty.", @line) if arg.empty? || arg == "!"
404
+ unless arg[0] == Script::VARIABLE_CHAR
405
+ raise SyntaxError.new("Mixin argument \"#{arg}\" must begin with an exclamation point (!).", @line)
406
+ end
407
+ arg, default = arg.split(/\s*=\s*/, 2)
408
+ required_arg_count += 1 unless default
409
+ default_arg_found ||= default
410
+ raise SyntaxError.new("Invalid variable \"#{arg}\".", @line) unless arg =~ Script::VALIDATE
411
+ raise SyntaxError.new("Required arguments must not follow optional arguments \"#{arg}\".", @line) if default_arg_found && !default
412
+ default = parse_script(default, :offset => line.offset + line.text.index(default)) if default
413
+ { :name => arg[1..-1], :default_value => default }
414
+ end
415
+ Tree::MixinDefNode.new(name, args, @options)
416
+ end
417
+
418
+ def parse_mixin_include(line, root)
419
+ name, arg_string = line.text.scan(/^\+\s*([^(]+)(.*)$/).first
420
+ args = parse_mixin_arguments(arg_string)
421
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath mixin directives.", @line + 1) unless line.children.empty?
422
+ raise SyntaxError.new("Invalid mixin include \"#{line.text}\".", @line) if name.nil? || args.nil?
423
+ args.each {|a| raise SyntaxError.new("Mixin arguments can't be empty.", @line) if a.empty?}
424
+
425
+ Tree::MixinNode.new(name, args.map {|s| parse_script(s, :offset => line.offset + line.text.index(s))}, @options)
426
+ end
427
+
428
+ def parse_script(script, options = {})
429
+ line = options[:line] || @line
430
+ offset = options[:offset] || 0
431
+ Script.parse(script, line, offset, @options[:filename])
432
+ end
433
+
434
+ def import_paths
435
+ paths = @options[:load_paths] || []
436
+ paths.unshift(File.dirname(@options[:filename])) if @options[:filename]
437
+ paths
438
+ end
439
+
440
+ def import(files)
441
+ files.split(/,\s*/).map do |filename|
442
+ engine = nil
443
+
444
+ begin
445
+ filename = self.class.find_file_to_import(filename, import_paths)
446
+ rescue Exception => e
447
+ raise SyntaxError.new(e.message, @line)
448
+ end
449
+
450
+ next Tree::DirectiveNode.new("@import url(#{filename})", @options) if filename =~ /\.css$/
451
+
452
+ File.open(filename) do |file|
453
+ new_options = @options.dup
454
+ new_options[:filename] = filename
455
+ engine = Sass::Engine.new(file.read, new_options)
456
+ end
457
+
458
+ begin
459
+ root = engine.render_to_tree
460
+ rescue Sass::SyntaxError => err
461
+ err.add_backtrace_entry(filename)
462
+ raise err
463
+ end
464
+ Tree::FileNode.new(filename, root.children, @options)
465
+ end.flatten
466
+ end
467
+
468
+ def self.find_file_to_import(filename, load_paths)
469
+ was_sass = false
470
+ original_filename = filename
471
+
472
+ if filename[-5..-1] == ".sass"
473
+ filename = filename[0...-5]
474
+ was_sass = true
475
+ elsif filename[-4..-1] == ".css"
476
+ return filename
477
+ end
478
+
479
+ new_filename = find_full_path("#{filename}.sass", load_paths)
480
+
481
+ return new_filename if new_filename
482
+ return filename + '.css' unless was_sass
483
+ raise SyntaxError.new("File to import not found or unreadable: #{original_filename}.", @line)
484
+ end
485
+
486
+ def self.find_full_path(filename, load_paths)
487
+ segments = filename.split(File::SEPARATOR)
488
+ segments.push "_#{segments.pop}"
489
+ partial_name = segments.join(File::SEPARATOR)
490
+ load_paths.each do |path|
491
+ [partial_name, filename].each do |name|
492
+ full_path = File.join(path, name)
493
+ if File.readable?(full_path)
494
+ return full_path
495
+ end
496
+ end
497
+ end
498
+ nil
499
+ end
500
+ end
501
+ end