haml 2.0.10 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of haml might be problematic. Click here for more details.

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