drnic-haml 2.3.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 (190) hide show
  1. data/.yardopts +5 -0
  2. data/CONTRIBUTING +4 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +347 -0
  5. data/REVISION +1 -0
  6. data/Rakefile +371 -0
  7. data/VERSION +1 -0
  8. data/VERSION_NAME +1 -0
  9. data/bin/css2sass +7 -0
  10. data/bin/haml +9 -0
  11. data/bin/html2haml +7 -0
  12. data/bin/sass +8 -0
  13. data/extra/haml-mode.el +663 -0
  14. data/extra/sass-mode.el +205 -0
  15. data/extra/update_watch.rb +13 -0
  16. data/init.rb +8 -0
  17. data/lib/haml.rb +40 -0
  18. data/lib/haml/buffer.rb +307 -0
  19. data/lib/haml/engine.rb +301 -0
  20. data/lib/haml/error.rb +22 -0
  21. data/lib/haml/exec.rb +470 -0
  22. data/lib/haml/filters.rb +341 -0
  23. data/lib/haml/helpers.rb +560 -0
  24. data/lib/haml/helpers/action_view_extensions.rb +40 -0
  25. data/lib/haml/helpers/action_view_mods.rb +176 -0
  26. data/lib/haml/herb.rb +96 -0
  27. data/lib/haml/html.rb +308 -0
  28. data/lib/haml/precompiler.rb +997 -0
  29. data/lib/haml/shared.rb +78 -0
  30. data/lib/haml/template.rb +51 -0
  31. data/lib/haml/template/patch.rb +58 -0
  32. data/lib/haml/template/plugin.rb +71 -0
  33. data/lib/haml/util.rb +244 -0
  34. data/lib/haml/version.rb +64 -0
  35. data/lib/sass.rb +24 -0
  36. data/lib/sass/css.rb +423 -0
  37. data/lib/sass/engine.rb +491 -0
  38. data/lib/sass/environment.rb +79 -0
  39. data/lib/sass/error.rb +162 -0
  40. data/lib/sass/files.rb +133 -0
  41. data/lib/sass/plugin.rb +170 -0
  42. data/lib/sass/plugin/merb.rb +57 -0
  43. data/lib/sass/plugin/rails.rb +23 -0
  44. data/lib/sass/repl.rb +58 -0
  45. data/lib/sass/script.rb +55 -0
  46. data/lib/sass/script/bool.rb +17 -0
  47. data/lib/sass/script/color.rb +183 -0
  48. data/lib/sass/script/funcall.rb +50 -0
  49. data/lib/sass/script/functions.rb +199 -0
  50. data/lib/sass/script/lexer.rb +191 -0
  51. data/lib/sass/script/literal.rb +177 -0
  52. data/lib/sass/script/node.rb +14 -0
  53. data/lib/sass/script/number.rb +381 -0
  54. data/lib/sass/script/operation.rb +45 -0
  55. data/lib/sass/script/parser.rb +222 -0
  56. data/lib/sass/script/string.rb +12 -0
  57. data/lib/sass/script/unary_operation.rb +34 -0
  58. data/lib/sass/script/variable.rb +31 -0
  59. data/lib/sass/tree/comment_node.rb +84 -0
  60. data/lib/sass/tree/debug_node.rb +30 -0
  61. data/lib/sass/tree/directive_node.rb +70 -0
  62. data/lib/sass/tree/for_node.rb +48 -0
  63. data/lib/sass/tree/if_node.rb +54 -0
  64. data/lib/sass/tree/import_node.rb +69 -0
  65. data/lib/sass/tree/mixin_def_node.rb +29 -0
  66. data/lib/sass/tree/mixin_node.rb +48 -0
  67. data/lib/sass/tree/node.rb +252 -0
  68. data/lib/sass/tree/prop_node.rb +106 -0
  69. data/lib/sass/tree/root_node.rb +56 -0
  70. data/lib/sass/tree/rule_node.rb +220 -0
  71. data/lib/sass/tree/variable_node.rb +34 -0
  72. data/lib/sass/tree/while_node.rb +31 -0
  73. data/rails/init.rb +1 -0
  74. data/test/benchmark.rb +99 -0
  75. data/test/haml/engine_test.rb +1129 -0
  76. data/test/haml/helper_test.rb +282 -0
  77. data/test/haml/html2haml_test.rb +258 -0
  78. data/test/haml/markaby/standard.mab +52 -0
  79. data/test/haml/mocks/article.rb +6 -0
  80. data/test/haml/results/content_for_layout.xhtml +12 -0
  81. data/test/haml/results/eval_suppressed.xhtml +9 -0
  82. data/test/haml/results/filters.xhtml +62 -0
  83. data/test/haml/results/helpers.xhtml +93 -0
  84. data/test/haml/results/helpful.xhtml +10 -0
  85. data/test/haml/results/just_stuff.xhtml +68 -0
  86. data/test/haml/results/list.xhtml +12 -0
  87. data/test/haml/results/nuke_inner_whitespace.xhtml +40 -0
  88. data/test/haml/results/nuke_outer_whitespace.xhtml +148 -0
  89. data/test/haml/results/original_engine.xhtml +20 -0
  90. data/test/haml/results/partial_layout.xhtml +5 -0
  91. data/test/haml/results/partials.xhtml +21 -0
  92. data/test/haml/results/render_layout.xhtml +3 -0
  93. data/test/haml/results/silent_script.xhtml +74 -0
  94. data/test/haml/results/standard.xhtml +162 -0
  95. data/test/haml/results/tag_parsing.xhtml +23 -0
  96. data/test/haml/results/very_basic.xhtml +5 -0
  97. data/test/haml/results/whitespace_handling.xhtml +89 -0
  98. data/test/haml/rhtml/_av_partial_1.rhtml +12 -0
  99. data/test/haml/rhtml/_av_partial_2.rhtml +8 -0
  100. data/test/haml/rhtml/action_view.rhtml +62 -0
  101. data/test/haml/rhtml/standard.rhtml +54 -0
  102. data/test/haml/spec_test.rb +44 -0
  103. data/test/haml/template_test.rb +217 -0
  104. data/test/haml/templates/_av_partial_1.haml +9 -0
  105. data/test/haml/templates/_av_partial_1_ugly.haml +9 -0
  106. data/test/haml/templates/_av_partial_2.haml +5 -0
  107. data/test/haml/templates/_av_partial_2_ugly.haml +5 -0
  108. data/test/haml/templates/_layout.erb +3 -0
  109. data/test/haml/templates/_layout_for_partial.haml +3 -0
  110. data/test/haml/templates/_partial.haml +8 -0
  111. data/test/haml/templates/_text_area.haml +3 -0
  112. data/test/haml/templates/action_view.haml +47 -0
  113. data/test/haml/templates/action_view_ugly.haml +47 -0
  114. data/test/haml/templates/breakage.haml +8 -0
  115. data/test/haml/templates/content_for_layout.haml +8 -0
  116. data/test/haml/templates/eval_suppressed.haml +11 -0
  117. data/test/haml/templates/filters.haml +66 -0
  118. data/test/haml/templates/helpers.haml +95 -0
  119. data/test/haml/templates/helpful.haml +11 -0
  120. data/test/haml/templates/just_stuff.haml +83 -0
  121. data/test/haml/templates/list.haml +12 -0
  122. data/test/haml/templates/nuke_inner_whitespace.haml +32 -0
  123. data/test/haml/templates/nuke_outer_whitespace.haml +144 -0
  124. data/test/haml/templates/original_engine.haml +17 -0
  125. data/test/haml/templates/partial_layout.haml +3 -0
  126. data/test/haml/templates/partialize.haml +1 -0
  127. data/test/haml/templates/partials.haml +12 -0
  128. data/test/haml/templates/render_layout.haml +2 -0
  129. data/test/haml/templates/silent_script.haml +40 -0
  130. data/test/haml/templates/standard.haml +42 -0
  131. data/test/haml/templates/standard_ugly.haml +42 -0
  132. data/test/haml/templates/tag_parsing.haml +21 -0
  133. data/test/haml/templates/very_basic.haml +4 -0
  134. data/test/haml/templates/whitespace_handling.haml +87 -0
  135. data/test/haml/util_test.rb +92 -0
  136. data/test/linked_rails.rb +12 -0
  137. data/test/sass/css2sass_test.rb +294 -0
  138. data/test/sass/engine_test.rb +956 -0
  139. data/test/sass/functions_test.rb +126 -0
  140. data/test/sass/more_results/more1.css +9 -0
  141. data/test/sass/more_results/more1_with_line_comments.css +26 -0
  142. data/test/sass/more_results/more_import.css +29 -0
  143. data/test/sass/more_templates/_more_partial.sass +2 -0
  144. data/test/sass/more_templates/more1.sass +23 -0
  145. data/test/sass/more_templates/more_import.sass +11 -0
  146. data/test/sass/plugin_test.rb +229 -0
  147. data/test/sass/results/alt.css +4 -0
  148. data/test/sass/results/basic.css +9 -0
  149. data/test/sass/results/compact.css +5 -0
  150. data/test/sass/results/complex.css +87 -0
  151. data/test/sass/results/compressed.css +1 -0
  152. data/test/sass/results/expanded.css +19 -0
  153. data/test/sass/results/import.css +29 -0
  154. data/test/sass/results/line_numbers.css +49 -0
  155. data/test/sass/results/mixins.css +95 -0
  156. data/test/sass/results/multiline.css +24 -0
  157. data/test/sass/results/nested.css +22 -0
  158. data/test/sass/results/parent_ref.css +13 -0
  159. data/test/sass/results/script.css +16 -0
  160. data/test/sass/results/subdir/nested_subdir/nested_subdir.css +1 -0
  161. data/test/sass/results/subdir/subdir.css +3 -0
  162. data/test/sass/results/units.css +11 -0
  163. data/test/sass/script_test.rb +261 -0
  164. data/test/sass/templates/_partial.sass +2 -0
  165. data/test/sass/templates/alt.sass +16 -0
  166. data/test/sass/templates/basic.sass +23 -0
  167. data/test/sass/templates/bork1.sass +2 -0
  168. data/test/sass/templates/bork2.sass +2 -0
  169. data/test/sass/templates/bork3.sass +2 -0
  170. data/test/sass/templates/compact.sass +17 -0
  171. data/test/sass/templates/complex.sass +307 -0
  172. data/test/sass/templates/compressed.sass +15 -0
  173. data/test/sass/templates/expanded.sass +17 -0
  174. data/test/sass/templates/import.sass +11 -0
  175. data/test/sass/templates/importee.sass +19 -0
  176. data/test/sass/templates/line_numbers.sass +13 -0
  177. data/test/sass/templates/mixins.sass +76 -0
  178. data/test/sass/templates/multiline.sass +20 -0
  179. data/test/sass/templates/nested.sass +25 -0
  180. data/test/sass/templates/nested_bork1.sass +2 -0
  181. data/test/sass/templates/nested_bork2.sass +2 -0
  182. data/test/sass/templates/nested_bork3.sass +2 -0
  183. data/test/sass/templates/parent_ref.sass +25 -0
  184. data/test/sass/templates/script.sass +101 -0
  185. data/test/sass/templates/subdir/nested_subdir/_nested_partial.sass +2 -0
  186. data/test/sass/templates/subdir/nested_subdir/nested_subdir.sass +3 -0
  187. data/test/sass/templates/subdir/subdir.sass +6 -0
  188. data/test/sass/templates/units.sass +11 -0
  189. data/test/test_helper.rb +44 -0
  190. metadata +298 -0
@@ -0,0 +1,491 @@
1
+ require 'strscan'
2
+ require 'digest/sha1'
3
+ require 'sass/tree/node'
4
+ require 'sass/tree/root_node'
5
+ require 'sass/tree/rule_node'
6
+ require 'sass/tree/comment_node'
7
+ require 'sass/tree/prop_node'
8
+ require 'sass/tree/directive_node'
9
+ require 'sass/tree/variable_node'
10
+ require 'sass/tree/mixin_def_node'
11
+ require 'sass/tree/mixin_node'
12
+ require 'sass/tree/if_node'
13
+ require 'sass/tree/while_node'
14
+ require 'sass/tree/for_node'
15
+ require 'sass/tree/debug_node'
16
+ require 'sass/tree/import_node'
17
+ require 'sass/environment'
18
+ require 'sass/script'
19
+ require 'sass/error'
20
+ require 'sass/files'
21
+ require 'haml/shared'
22
+
23
+ module Sass
24
+ # A Sass mixin.
25
+ #
26
+ # `name`: `String`
27
+ # : The name of the mixin.
28
+ #
29
+ # `args`: `Array<(String, Script::Node)>`
30
+ # : The arguments for the mixin.
31
+ # Each element is a tuple containing the name of the argument
32
+ # and the parse tree for the default value of the argument.
33
+ #
34
+ # `environment`: {Sass::Environment}
35
+ # : The environment in which the mixin was defined.
36
+ # This is captured so that the mixin can have access
37
+ # to local variables defined in its scope.
38
+ #
39
+ # `tree`: {Sass::Tree::Node}
40
+ # : The parse tree for the mixin.
41
+ Mixin = Struct.new(:name, :args, :environment, :tree)
42
+
43
+ # This class handles the parsing and compilation of the Sass template.
44
+ # Example usage:
45
+ #
46
+ # template = File.load('stylesheets/sassy.sass')
47
+ # sass_engine = Sass::Engine.new(template)
48
+ # output = sass_engine.render
49
+ # puts output
50
+ class Engine
51
+ include Haml::Util
52
+
53
+ # A line of Sass code.
54
+ #
55
+ # `text`: `String`
56
+ # : The text in the line, without any whitespace at the beginning or end.
57
+ #
58
+ # `tabs`: `Fixnum`
59
+ # : The level of indentation of the line.
60
+ #
61
+ # `index`: `Fixnum`
62
+ # : The line number in the original document.
63
+ #
64
+ # `offset`: `Fixnum`
65
+ # : The number of bytes in on the line that the text begins.
66
+ # This ends up being the number of bytes of leading whitespace.
67
+ #
68
+ # `filename`: `String`
69
+ # : The name of the file in which this line appeared.
70
+ #
71
+ # `children`: `Array<Line>`
72
+ # : The lines nested below this one.
73
+ class Line < Struct.new(:text, :tabs, :index, :offset, :filename, :children)
74
+ def comment?
75
+ text[0] == COMMENT_CHAR && (text[1] == SASS_COMMENT_CHAR || text[1] == CSS_COMMENT_CHAR)
76
+ end
77
+ end
78
+
79
+ # The character that begins a CSS property.
80
+ PROPERTY_CHAR = ?:
81
+
82
+ # The character that designates that
83
+ # a property should be assigned to a SassScript expression.
84
+ SCRIPT_CHAR = ?=
85
+
86
+ # The character that designates the beginning of a comment,
87
+ # either Sass or CSS.
88
+ COMMENT_CHAR = ?/
89
+
90
+ # The character that follows the general COMMENT_CHAR and designates a Sass comment,
91
+ # which is not output as a CSS comment.
92
+ SASS_COMMENT_CHAR = ?/
93
+
94
+ # The character that follows the general COMMENT_CHAR and designates a CSS comment,
95
+ # which is embedded in the CSS document.
96
+ CSS_COMMENT_CHAR = ?*
97
+
98
+ # The character used to denote a compiler directive.
99
+ DIRECTIVE_CHAR = ?@
100
+
101
+ # Designates a non-parsed rule.
102
+ ESCAPE_CHAR = ?\\
103
+
104
+ # Designates block as mixin definition rather than CSS rules to output
105
+ MIXIN_DEFINITION_CHAR = ?=
106
+
107
+ # Includes named mixin declared using MIXIN_DEFINITION_CHAR
108
+ MIXIN_INCLUDE_CHAR = ?+
109
+
110
+ # The regex that matches properties of the form <tt>name: prop</tt>.
111
+ PROPERTY_NEW_MATCHER = /^[^\s:"]+\s*[=:](\s|$)/
112
+
113
+ # The regex that matches and extracts data from
114
+ # properties of the form <tt>name: prop</tt>.
115
+ PROPERTY_NEW = /^([^\s=:"]+)(\s*=|:)(?:\s+|$)(.*)/
116
+
117
+ # The regex that matches and extracts data from
118
+ # properties of the form <tt>:name prop</tt>.
119
+ PROPERTY_OLD = /^:([^\s=:"]+)\s*(=?)(?:\s+|$)(.*)/
120
+
121
+ # The default options for Sass::Engine.
122
+ DEFAULT_OPTIONS = {
123
+ :style => :nested,
124
+ :load_paths => ['.'],
125
+ :cache => true,
126
+ :cache_location => './.sass-cache',
127
+ }.freeze
128
+
129
+ # @param template [String] The Sass template.
130
+ # @param options [Hash<Symbol, Object>] An options hash;
131
+ # see {file:SASS_REFERENCE.md#sass_options the Sass options documentation}
132
+ def initialize(template, options={})
133
+ @options = DEFAULT_OPTIONS.merge(options)
134
+ @template = template
135
+
136
+ # Backwards compatibility
137
+ @options[:property_syntax] ||= @options[:attribute_syntax]
138
+ case @options[:property_syntax]
139
+ when :alternate; @options[:property_syntax] = :new
140
+ when :normal; @options[:property_syntax] = :old
141
+ end
142
+ end
143
+
144
+ # Render the template to CSS.
145
+ #
146
+ # @return [String] The CSS
147
+ # @raise [Sass::SyntaxError] if there's an error in the document
148
+ def render
149
+ to_tree.render
150
+ end
151
+
152
+ alias_method :to_css, :render
153
+
154
+ # Parses the document into its parse tree.
155
+ #
156
+ # @return [Sass::Tree::Node] The root of the parse tree.
157
+ # @raise [Sass::SyntaxError] if there's an error in the document
158
+ def to_tree
159
+ check_encoding(@template) {|msg, line| raise Sass::SyntaxError.new(msg, :line => line)}
160
+
161
+ root = Tree::RootNode.new(@template)
162
+ append_children(root, tree(tabulate(@template)).first, true)
163
+ root.options = @options
164
+ root
165
+ rescue SyntaxError => e
166
+ e.modify_backtrace(:filename => @options[:filename], :line => @line)
167
+ e.sass_template = @template
168
+ raise e
169
+ end
170
+
171
+ private
172
+
173
+ def tabulate(string)
174
+ tab_str = nil
175
+ first = true
176
+ lines = []
177
+ string.gsub(/\r|\n|\r\n|\r\n/, "\n").scan(/^.*?$/).each_with_index do |line, index|
178
+ index += (@options[:line] || 1)
179
+ if line.strip.empty?
180
+ lines.last.text << "\n" if lines.last && lines.last.comment?
181
+ next
182
+ end
183
+
184
+ line_tab_str = line[/^\s*/]
185
+ unless line_tab_str.empty?
186
+ tab_str ||= line_tab_str
187
+
188
+ raise SyntaxError.new("Indenting at the beginning of the document is illegal.",
189
+ :line => index) if first
190
+
191
+ raise SyntaxError.new("Indentation can't use both tabs and spaces.",
192
+ :line => index) if tab_str.include?(?\s) && tab_str.include?(?\t)
193
+ end
194
+ first &&= !tab_str.nil?
195
+ if tab_str.nil?
196
+ lines << Line.new(line.strip, 0, index, 0, @options[:filename], [])
197
+ next
198
+ end
199
+
200
+ if lines.last && lines.last.comment? && line =~ /^(?:#{tab_str}){#{lines.last.tabs + 1}}(.*)$/
201
+ lines.last.text << "\n" << $1
202
+ next
203
+ end
204
+
205
+ line_tabs = line_tab_str.scan(tab_str).size
206
+ if tab_str * line_tabs != line_tab_str
207
+ message = <<END.strip.gsub("\n", ' ')
208
+ Inconsistent indentation: #{Haml::Shared.human_indentation line_tab_str, true} used for indentation,
209
+ but the rest of the document was indented using #{Haml::Shared.human_indentation tab_str}.
210
+ END
211
+ raise SyntaxError.new(message, :line => index)
212
+ end
213
+
214
+ lines << Line.new(line.strip, line_tabs, index, tab_str.size, @options[:filename], [])
215
+ end
216
+ lines
217
+ end
218
+
219
+ def tree(arr, i = 0)
220
+ return [], i if arr[i].nil?
221
+
222
+ base = arr[i].tabs
223
+ nodes = []
224
+ while (line = arr[i]) && line.tabs >= base
225
+ if line.tabs > base
226
+ raise SyntaxError.new("The line was indented #{line.tabs - base} levels deeper than the previous line.",
227
+ :line => line.index) if line.tabs > base + 1
228
+
229
+ nodes.last.children, i = tree(arr, i)
230
+ else
231
+ nodes << line
232
+ i += 1
233
+ end
234
+ end
235
+ return nodes, i
236
+ end
237
+
238
+ def build_tree(parent, line, root = false)
239
+ @line = line.index
240
+ node_or_nodes = parse_line(parent, line, root)
241
+
242
+ Array(node_or_nodes).each do |node|
243
+ # Node is a symbol if it's non-outputting, like a variable assignment
244
+ next unless node.is_a? Tree::Node
245
+
246
+ node.line = line.index
247
+ node.filename = line.filename
248
+
249
+ if node.is_a?(Tree::CommentNode)
250
+ node.lines = line.children
251
+ else
252
+ append_children(node, line.children, false)
253
+ end
254
+ end
255
+
256
+ node_or_nodes
257
+ end
258
+
259
+ def append_children(parent, children, root)
260
+ continued_rule = nil
261
+ children.each do |line|
262
+ child = build_tree(parent, line, root)
263
+
264
+ if child.is_a?(Tree::RuleNode) && child.continued?
265
+ raise SyntaxError.new("Rules can't end in commas.",
266
+ :line => child.line) unless child.children.empty?
267
+ if continued_rule
268
+ continued_rule.add_rules child
269
+ else
270
+ continued_rule = child
271
+ end
272
+ next
273
+ end
274
+
275
+ if continued_rule
276
+ raise SyntaxError.new("Rules can't end in commas.",
277
+ :line => continued_rule.line) unless child.is_a?(Tree::RuleNode)
278
+ continued_rule.add_rules child
279
+ continued_rule.children = child.children
280
+ continued_rule, child = nil, continued_rule
281
+ end
282
+
283
+ check_for_no_children(child)
284
+ validate_and_append_child(parent, child, line, root)
285
+ end
286
+
287
+ raise SyntaxError.new("Rules can't end in commas.",
288
+ :line => continued_rule.line) if continued_rule
289
+
290
+ parent
291
+ end
292
+
293
+ def validate_and_append_child(parent, child, line, root)
294
+ unless root
295
+ case child
296
+ when Tree::MixinDefNode
297
+ raise SyntaxError.new("Mixins may only be defined at the root of a document.",
298
+ :line => line.index)
299
+ when Tree::ImportNode
300
+ raise SyntaxError.new("Import directives may only be used at the root of a document.",
301
+ :line => line.index)
302
+ end
303
+ end
304
+
305
+ case child
306
+ when Array
307
+ child.each {|c| validate_and_append_child(parent, c, line, root)}
308
+ when Tree::Node
309
+ parent << child
310
+ end
311
+ end
312
+
313
+ def check_for_no_children(node)
314
+ return unless node.is_a?(Tree::RuleNode) && node.children.empty?
315
+ warning = (node.rules.size == 1) ? <<SHORT : <<LONG
316
+ WARNING on line #{node.line}:
317
+ Selector #{node.rules.first.inspect} doesn't have any properties and will not be rendered.
318
+ SHORT
319
+
320
+ WARNING on line #{node.line}:
321
+ Selector
322
+ #{node.rules.join("\n ")}
323
+ doesn't have any properties and will not be rendered.
324
+ LONG
325
+
326
+ warn(warning.strip)
327
+ end
328
+
329
+ def parse_line(parent, line, root)
330
+ case line.text[0]
331
+ when PROPERTY_CHAR
332
+ if line.text[1] != PROPERTY_CHAR
333
+ parse_property(line, PROPERTY_OLD)
334
+ else
335
+ # Support CSS3-style pseudo-elements,
336
+ # which begin with ::
337
+ Tree::RuleNode.new(line.text)
338
+ end
339
+ when Script::VARIABLE_CHAR
340
+ parse_variable(line)
341
+ when COMMENT_CHAR
342
+ parse_comment(line.text)
343
+ when DIRECTIVE_CHAR
344
+ parse_directive(parent, line, root)
345
+ when ESCAPE_CHAR
346
+ Tree::RuleNode.new(line.text[1..-1])
347
+ when MIXIN_DEFINITION_CHAR
348
+ parse_mixin_definition(line)
349
+ when MIXIN_INCLUDE_CHAR
350
+ if line.text[1].nil? || line.text[1] == ?\s
351
+ Tree::RuleNode.new(line.text)
352
+ else
353
+ parse_mixin_include(line, root)
354
+ end
355
+ else
356
+ if line.text =~ PROPERTY_NEW_MATCHER
357
+ parse_property(line, PROPERTY_NEW)
358
+ else
359
+ Tree::RuleNode.new(line.text)
360
+ end
361
+ end
362
+ end
363
+
364
+ def parse_property(line, property_regx)
365
+ name, eq, value = line.text.scan(property_regx)[0]
366
+
367
+ raise SyntaxError.new("Invalid property: \"#{line.text}\".",
368
+ :line => @line) if name.nil? || value.nil?
369
+
370
+ expr = if (eq.strip[0] == SCRIPT_CHAR)
371
+ parse_script(value, :offset => line.offset + line.text.index(value))
372
+ else
373
+ value
374
+ end
375
+ Tree::PropNode.new(name, expr, property_regx == PROPERTY_OLD ? :old : :new)
376
+ end
377
+
378
+ def parse_variable(line)
379
+ name, op, value = line.text.scan(Script::MATCH)[0]
380
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath variable declarations.",
381
+ :line => @line + 1) unless line.children.empty?
382
+ raise SyntaxError.new("Invalid variable: \"#{line.text}\".",
383
+ :line => @line) unless name && value
384
+
385
+ Tree::VariableNode.new(name, parse_script(value, :offset => line.offset + line.text.index(value)), op == '||=')
386
+ end
387
+
388
+ def parse_comment(line)
389
+ if line[1] == CSS_COMMENT_CHAR || line[1] == SASS_COMMENT_CHAR
390
+ Tree::CommentNode.new(line, line[1] == SASS_COMMENT_CHAR)
391
+ else
392
+ Tree::RuleNode.new(line)
393
+ end
394
+ end
395
+
396
+ def parse_directive(parent, line, root)
397
+ directive, whitespace, value = line.text[1..-1].split(/(\s+)/, 2)
398
+ offset = directive.size + whitespace.size + 1 if whitespace
399
+
400
+ # If value begins with url( or ",
401
+ # it's a CSS @import rule and we don't want to touch it.
402
+ if directive == "import" && value !~ /^(url\(|")/
403
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath import directives.",
404
+ :line => @line + 1) unless line.children.empty?
405
+ value.split(/,\s*/).map {|f| Tree::ImportNode.new(f)}
406
+ elsif directive == "for"
407
+ parse_for(line, root, value)
408
+ elsif directive == "else"
409
+ parse_else(parent, line, value)
410
+ elsif directive == "while"
411
+ raise SyntaxError.new("Invalid while directive '@while': expected expression.") unless value
412
+ Tree::WhileNode.new(parse_script(value, :offset => offset))
413
+ elsif directive == "if"
414
+ raise SyntaxError.new("Invalid if directive '@if': expected expression.") unless value
415
+ Tree::IfNode.new(parse_script(value, :offset => offset))
416
+ elsif directive == "debug"
417
+ raise SyntaxError.new("Invalid debug directive '@debug': expected expression.") unless value
418
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath debug directives.",
419
+ :line => @line + 1) unless line.children.empty?
420
+ offset = line.offset + line.text.index(value).to_i
421
+ Tree::DebugNode.new(parse_script(value, :offset => offset))
422
+ else
423
+ Tree::DirectiveNode.new(line.text)
424
+ end
425
+ end
426
+
427
+ def parse_for(line, root, text)
428
+ var, from_expr, to_name, to_expr = text.scan(/^([^\s]+)\s+from\s+(.+)\s+(to|through)\s+(.+)$/).first
429
+
430
+ if var.nil? # scan failed, try to figure out why for error message
431
+ if text !~ /^[^\s]+/
432
+ expected = "variable name"
433
+ elsif text !~ /^[^\s]+\s+from\s+.+/
434
+ expected = "'from <expr>'"
435
+ else
436
+ expected = "'to <expr>' or 'through <expr>'"
437
+ end
438
+ raise SyntaxError.new("Invalid for directive '@for #{text}': expected #{expected}.")
439
+ end
440
+ raise SyntaxError.new("Invalid variable \"#{var}\".") unless var =~ Script::VALIDATE
441
+
442
+ parsed_from = parse_script(from_expr, :offset => line.offset + line.text.index(from_expr))
443
+ parsed_to = parse_script(to_expr, :offset => line.offset + line.text.index(to_expr))
444
+ Tree::ForNode.new(var[1..-1], parsed_from, parsed_to, to_name == 'to')
445
+ end
446
+
447
+ def parse_else(parent, line, text)
448
+ previous = parent.last
449
+ raise SyntaxError.new("@else must come after @if.") unless previous.is_a?(Tree::IfNode)
450
+
451
+ if text
452
+ if text !~ /^if\s+(.+)/
453
+ raise SyntaxError.new("Invalid else directive '@else #{text}': expected 'if <expr>'.")
454
+ end
455
+ expr = parse_script($1, :offset => line.offset + line.text.index($1))
456
+ end
457
+
458
+ node = Tree::IfNode.new(expr)
459
+ append_children(node, line.children, false)
460
+ previous.add_else node
461
+ nil
462
+ end
463
+
464
+ def parse_mixin_definition(line)
465
+ name, arg_string = line.text.scan(/^=\s*([^(]+)(.*)$/).first
466
+ raise SyntaxError.new("Invalid mixin \"#{line.text[1..-1]}\".") if name.nil?
467
+
468
+ offset = line.offset + line.text.size - arg_string.size
469
+ args = Script::Parser.new(arg_string.strip, @line, offset).parse_mixin_definition_arglist
470
+ default_arg_found = false
471
+ Tree::MixinDefNode.new(name, args)
472
+ end
473
+
474
+ def parse_mixin_include(line, root)
475
+ name, arg_string = line.text.scan(/^\+\s*([^(]+)(.*)$/).first
476
+ raise SyntaxError.new("Invalid mixin include \"#{line.text}\".") if name.nil?
477
+
478
+ offset = line.offset + line.text.size - arg_string.size
479
+ args = Script::Parser.new(arg_string.strip, @line, offset).parse_mixin_include_arglist
480
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath mixin directives.",
481
+ :line => @line + 1) unless line.children.empty?
482
+ Tree::MixinNode.new(name, args)
483
+ end
484
+
485
+ def parse_script(script, options = {})
486
+ line = options[:line] || @line
487
+ offset = options[:offset] || 0
488
+ Script.parse(script, line, offset, @options[:filename])
489
+ end
490
+ end
491
+ end