sass 3.2.7 → 3.3.0.rc.1

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 (184) hide show
  1. data/MIT-LICENSE +2 -2
  2. data/README.md +14 -2
  3. data/Rakefile +25 -1
  4. data/VERSION +1 -1
  5. data/VERSION_DATE +1 -1
  6. data/VERSION_NAME +1 -1
  7. data/lib/sass/cache_stores/base.rb +4 -2
  8. data/lib/sass/cache_stores/chain.rb +2 -1
  9. data/lib/sass/cache_stores/filesystem.rb +2 -6
  10. data/lib/sass/cache_stores/memory.rb +1 -1
  11. data/lib/sass/cache_stores/null.rb +2 -2
  12. data/lib/sass/callbacks.rb +1 -0
  13. data/lib/sass/css.rb +10 -10
  14. data/lib/sass/engine.rb +403 -150
  15. data/lib/sass/environment.rb +136 -57
  16. data/lib/sass/error.rb +7 -7
  17. data/lib/sass/exec.rb +123 -39
  18. data/lib/sass/features.rb +41 -0
  19. data/lib/sass/importers/base.rb +33 -2
  20. data/lib/sass/importers/deprecated_path.rb +45 -0
  21. data/lib/sass/importers/filesystem.rb +25 -14
  22. data/lib/sass/importers.rb +1 -0
  23. data/lib/sass/logger/base.rb +3 -3
  24. data/lib/sass/logger/log_level.rb +4 -6
  25. data/lib/sass/media.rb +19 -19
  26. data/lib/sass/plugin/compiler.rb +141 -101
  27. data/lib/sass/plugin/configuration.rb +18 -22
  28. data/lib/sass/plugin/merb.rb +1 -1
  29. data/lib/sass/plugin/staleness_checker.rb +24 -8
  30. data/lib/sass/plugin.rb +4 -2
  31. data/lib/sass/repl.rb +3 -3
  32. data/lib/sass/script/css_lexer.rb +9 -4
  33. data/lib/sass/script/css_parser.rb +6 -2
  34. data/lib/sass/script/functions.rb +1343 -590
  35. data/lib/sass/script/lexer.rb +84 -52
  36. data/lib/sass/script/parser.rb +217 -97
  37. data/lib/sass/script/tree/funcall.rb +290 -0
  38. data/lib/sass/script/{interpolation.rb → tree/interpolation.rb} +34 -13
  39. data/lib/sass/script/tree/list_literal.rb +80 -0
  40. data/lib/sass/script/tree/literal.rb +47 -0
  41. data/lib/sass/script/tree/map_literal.rb +64 -0
  42. data/lib/sass/script/{node.rb → tree/node.rb} +22 -12
  43. data/lib/sass/script/{operation.rb → tree/operation.rb} +17 -25
  44. data/lib/sass/script/tree/selector.rb +30 -0
  45. data/lib/sass/script/{string_interpolation.rb → tree/string_interpolation.rb} +5 -4
  46. data/lib/sass/script/{unary_operation.rb → tree/unary_operation.rb} +14 -9
  47. data/lib/sass/script/tree/variable.rb +57 -0
  48. data/lib/sass/script/tree.rb +16 -0
  49. data/lib/sass/script/{arg_list.rb → value/arg_list.rb} +4 -24
  50. data/lib/sass/script/value/base.rb +248 -0
  51. data/lib/sass/script/value/bool.rb +36 -0
  52. data/lib/sass/script/{color.rb → value/color.rb} +239 -195
  53. data/lib/sass/script/value/helpers.rb +155 -0
  54. data/lib/sass/script/value/list.rb +119 -0
  55. data/lib/sass/script/value/map.rb +70 -0
  56. data/lib/sass/script/value/null.rb +45 -0
  57. data/lib/sass/script/{number.rb → value/number.rb} +91 -65
  58. data/lib/sass/script/{string.rb → value/string.rb} +9 -11
  59. data/lib/sass/script/value.rb +11 -0
  60. data/lib/sass/script.rb +35 -8
  61. data/lib/sass/scss/css_parser.rb +2 -1
  62. data/lib/sass/scss/parser.rb +338 -170
  63. data/lib/sass/scss/rx.rb +5 -6
  64. data/lib/sass/scss/script_lexer.rb +1 -0
  65. data/lib/sass/scss/script_parser.rb +1 -0
  66. data/lib/sass/scss/static_parser.rb +23 -6
  67. data/lib/sass/selector/abstract_sequence.rb +2 -2
  68. data/lib/sass/selector/comma_sequence.rb +21 -16
  69. data/lib/sass/selector/sequence.rb +60 -34
  70. data/lib/sass/selector/simple.rb +11 -12
  71. data/lib/sass/selector/simple_sequence.rb +55 -33
  72. data/lib/sass/selector.rb +52 -48
  73. data/lib/sass/source/map.rb +211 -0
  74. data/lib/sass/source/position.rb +39 -0
  75. data/lib/sass/source/range.rb +41 -0
  76. data/lib/sass/stack.rb +120 -0
  77. data/lib/sass/supports.rb +12 -13
  78. data/lib/sass/tree/at_root_node.rb +82 -0
  79. data/lib/sass/tree/comment_node.rb +3 -3
  80. data/lib/sass/tree/css_import_node.rb +11 -11
  81. data/lib/sass/tree/debug_node.rb +2 -2
  82. data/lib/sass/tree/directive_node.rb +13 -2
  83. data/lib/sass/tree/each_node.rb +8 -8
  84. data/lib/sass/tree/extend_node.rb +13 -6
  85. data/lib/sass/tree/for_node.rb +4 -4
  86. data/lib/sass/tree/function_node.rb +5 -4
  87. data/lib/sass/tree/if_node.rb +1 -1
  88. data/lib/sass/tree/import_node.rb +4 -5
  89. data/lib/sass/tree/media_node.rb +4 -14
  90. data/lib/sass/tree/mixin_def_node.rb +4 -4
  91. data/lib/sass/tree/mixin_node.rb +21 -8
  92. data/lib/sass/tree/node.rb +29 -12
  93. data/lib/sass/tree/prop_node.rb +38 -18
  94. data/lib/sass/tree/return_node.rb +3 -2
  95. data/lib/sass/tree/root_node.rb +19 -3
  96. data/lib/sass/tree/rule_node.rb +25 -17
  97. data/lib/sass/tree/supports_node.rb +0 -13
  98. data/lib/sass/tree/trace_node.rb +2 -1
  99. data/lib/sass/tree/variable_node.rb +9 -3
  100. data/lib/sass/tree/visitors/base.rb +6 -6
  101. data/lib/sass/tree/visitors/check_nesting.rb +12 -9
  102. data/lib/sass/tree/visitors/convert.rb +63 -38
  103. data/lib/sass/tree/visitors/cssize.rb +63 -23
  104. data/lib/sass/tree/visitors/deep_copy.rb +6 -5
  105. data/lib/sass/tree/visitors/extend.rb +7 -7
  106. data/lib/sass/tree/visitors/perform.rb +256 -151
  107. data/lib/sass/tree/visitors/set_options.rb +6 -6
  108. data/lib/sass/tree/visitors/to_css.rb +231 -81
  109. data/lib/sass/tree/warn_node.rb +2 -2
  110. data/lib/sass/tree/while_node.rb +2 -2
  111. data/lib/sass/util/multibyte_string_scanner.rb +2 -0
  112. data/lib/sass/util/normalized_map.rb +65 -0
  113. data/lib/sass/util/ordered_hash.rb +188 -0
  114. data/lib/sass/util/subset_map.rb +3 -2
  115. data/lib/sass/util/test.rb +9 -0
  116. data/lib/sass/util.rb +220 -34
  117. data/lib/sass/version.rb +9 -9
  118. data/lib/sass.rb +14 -7
  119. data/test/sass/compiler_test.rb +213 -0
  120. data/test/sass/conversion_test.rb +235 -9
  121. data/test/sass/engine_test.rb +230 -60
  122. data/test/sass/exec_test.rb +86 -0
  123. data/test/sass/extend_test.rb +215 -147
  124. data/test/sass/functions_test.rb +584 -99
  125. data/test/sass/importer_test.rb +165 -17
  126. data/test/sass/plugin_test.rb +19 -13
  127. data/test/sass/script_conversion_test.rb +40 -0
  128. data/test/sass/script_test.rb +231 -21
  129. data/test/sass/scss/css_test.rb +14 -5
  130. data/test/sass/scss/scss_test.rb +1266 -66
  131. data/test/sass/source_map_test.rb +879 -0
  132. data/test/sass/templates/bork5.sass +3 -0
  133. data/test/sass/util/normalized_map_test.rb +30 -0
  134. data/test/sass/util_test.rb +90 -0
  135. data/test/sass/value_helpers_test.rb +181 -0
  136. data/test/test_helper.rb +7 -2
  137. metadata +316 -291
  138. data/lib/sass/script/bool.rb +0 -18
  139. data/lib/sass/script/funcall.rb +0 -231
  140. data/lib/sass/script/list.rb +0 -84
  141. data/lib/sass/script/literal.rb +0 -239
  142. data/lib/sass/script/null.rb +0 -34
  143. data/lib/sass/script/variable.rb +0 -58
  144. data/test/Gemfile +0 -3
  145. data/vendor/listen/CHANGELOG.md +0 -221
  146. data/vendor/listen/CONTRIBUTING.md +0 -38
  147. data/vendor/listen/Gemfile +0 -30
  148. data/vendor/listen/Guardfile +0 -8
  149. data/vendor/listen/LICENSE +0 -20
  150. data/vendor/listen/README.md +0 -315
  151. data/vendor/listen/Rakefile +0 -47
  152. data/vendor/listen/Vagrantfile +0 -96
  153. data/vendor/listen/lib/listen/adapter.rb +0 -214
  154. data/vendor/listen/lib/listen/adapters/bsd.rb +0 -112
  155. data/vendor/listen/lib/listen/adapters/darwin.rb +0 -85
  156. data/vendor/listen/lib/listen/adapters/linux.rb +0 -113
  157. data/vendor/listen/lib/listen/adapters/polling.rb +0 -67
  158. data/vendor/listen/lib/listen/adapters/windows.rb +0 -87
  159. data/vendor/listen/lib/listen/dependency_manager.rb +0 -126
  160. data/vendor/listen/lib/listen/directory_record.rb +0 -371
  161. data/vendor/listen/lib/listen/listener.rb +0 -225
  162. data/vendor/listen/lib/listen/multi_listener.rb +0 -143
  163. data/vendor/listen/lib/listen/turnstile.rb +0 -28
  164. data/vendor/listen/lib/listen/version.rb +0 -3
  165. data/vendor/listen/lib/listen.rb +0 -40
  166. data/vendor/listen/listen.gemspec +0 -22
  167. data/vendor/listen/spec/listen/adapter_spec.rb +0 -183
  168. data/vendor/listen/spec/listen/adapters/bsd_spec.rb +0 -36
  169. data/vendor/listen/spec/listen/adapters/darwin_spec.rb +0 -37
  170. data/vendor/listen/spec/listen/adapters/linux_spec.rb +0 -47
  171. data/vendor/listen/spec/listen/adapters/polling_spec.rb +0 -68
  172. data/vendor/listen/spec/listen/adapters/windows_spec.rb +0 -30
  173. data/vendor/listen/spec/listen/dependency_manager_spec.rb +0 -107
  174. data/vendor/listen/spec/listen/directory_record_spec.rb +0 -1225
  175. data/vendor/listen/spec/listen/listener_spec.rb +0 -169
  176. data/vendor/listen/spec/listen/multi_listener_spec.rb +0 -174
  177. data/vendor/listen/spec/listen/turnstile_spec.rb +0 -56
  178. data/vendor/listen/spec/listen_spec.rb +0 -73
  179. data/vendor/listen/spec/spec_helper.rb +0 -21
  180. data/vendor/listen/spec/support/adapter_helper.rb +0 -629
  181. data/vendor/listen/spec/support/directory_record_helper.rb +0 -55
  182. data/vendor/listen/spec/support/fixtures_helper.rb +0 -29
  183. data/vendor/listen/spec/support/listeners_helper.rb +0 -156
  184. data/vendor/listen/spec/support/platform_helper.rb +0 -15
data/lib/sass/engine.rb CHANGED
@@ -1,6 +1,9 @@
1
1
  require 'set'
2
2
  require 'digest/sha1'
3
3
  require 'sass/cache_stores'
4
+ require 'sass/source/position'
5
+ require 'sass/source/range'
6
+ require 'sass/source/map'
4
7
  require 'sass/tree/node'
5
8
  require 'sass/tree/root_node'
6
9
  require 'sass/tree/rule_node'
@@ -26,6 +29,7 @@ require 'sass/tree/debug_node'
26
29
  require 'sass/tree/warn_node'
27
30
  require 'sass/tree/import_node'
28
31
  require 'sass/tree/charset_node'
32
+ require 'sass/tree/at_root_node'
29
33
  require 'sass/tree/visitors/base'
30
34
  require 'sass/tree/visitors/perform'
31
35
  require 'sass/tree/visitors/cssize'
@@ -39,6 +43,7 @@ require 'sass/selector'
39
43
  require 'sass/environment'
40
44
  require 'sass/script'
41
45
  require 'sass/scss'
46
+ require 'sass/stack'
42
47
  require 'sass/error'
43
48
  require 'sass/importers'
44
49
  require 'sass/shared'
@@ -52,12 +57,12 @@ module Sass
52
57
  # `name`: `String`
53
58
  # : The name of the mixin/function.
54
59
  #
55
- # `args`: `Array<(Script::Node, Script::Node)>`
60
+ # `args`: `Array<(Script::Tree::Node, Script::Tree::Node)>`
56
61
  # : The arguments for the mixin/function.
57
62
  # Each element is a tuple containing the variable node of the argument
58
63
  # and the parse tree for the default value of the argument.
59
64
  #
60
- # `splat`: `Script::Node?`
65
+ # `splat`: `Script::Tree::Node?`
61
66
  # : The variable node of the splat argument for this callable, or null.
62
67
  #
63
68
  # `environment`: {Sass::Environment}
@@ -188,6 +193,16 @@ module Sass
188
193
  options[:filesystem_importer].new(p.to_s)
189
194
  end
190
195
 
196
+ # Remove any deprecated importers if the location is imported explicitly
197
+ options[:load_paths].reject! do |importer|
198
+ importer.is_a?(Sass::Importers::DeprecatedPath) &&
199
+ options[:load_paths].find do |other_importer|
200
+ other_importer.is_a?(Sass::Importers::Filesystem) &&
201
+ other_importer != importer &&
202
+ other_importer.root == importer.root
203
+ end
204
+ end
205
+
191
206
  # Backwards compatibility
192
207
  options[:property_syntax] ||= options[:attribute_syntax]
193
208
  case options[:property_syntax]
@@ -246,7 +261,7 @@ module Sass
246
261
  # See {file:SASS_REFERENCE.md#sass_options the Sass options documentation}.
247
262
  # @see {Sass::Engine.for_file}
248
263
  # @see {Sass::Plugin}
249
- def initialize(template, options={})
264
+ def initialize(template, options = {})
250
265
  @options = self.class.normalize_options(options)
251
266
  @template = template
252
267
  end
@@ -259,9 +274,27 @@ module Sass
259
274
  # cannot be converted to UTF-8
260
275
  # @raise [ArgumentError] if the document uses an unknown encoding with `@charset`
261
276
  def render
262
- return _render unless @options[:quiet]
263
- Sass::Util.silence_sass_warnings {_render}
277
+ return encode_and_set_charset(_to_tree.render) unless @options[:quiet]
278
+ Sass::Util.silence_sass_warnings {encode_and_set_charset(_to_tree.render)}
279
+ end
280
+
281
+ # Render the template to CSS and return the source map.
282
+ #
283
+ # @param sourcemap_uri [String] The sourcemap URI to use in the
284
+ # `@sourceMappingURL` comment. If this is relative, it should be relative
285
+ # to the location of the CSS file.
286
+ # @return [(String, Sass::Source::Map)] The rendered CSS and the associated
287
+ # source map
288
+ # @raise [Sass::SyntaxError] if there's an error in the document, or if the
289
+ # public URL for this document couldn't be determined.
290
+ # @raise [Encoding::UndefinedConversionError] if the source encoding
291
+ # cannot be converted to UTF-8
292
+ # @raise [ArgumentError] if the document uses an unknown encoding with `@charset`
293
+ def render_with_sourcemap(sourcemap_uri)
294
+ return _render_with_sourcemap(sourcemap_uri) unless @options[:quiet]
295
+ Sass::Util.silence_sass_warnings {_render_with_sourcemap(sourcemap_uri)}
264
296
  end
297
+
265
298
  alias_method :to_css, :render
266
299
 
267
300
  # Parses the document into its parse tree. Memoized.
@@ -269,9 +302,11 @@ module Sass
269
302
  # @return [Sass::Tree::Node] The root of the parse tree.
270
303
  # @raise [Sass::SyntaxError] if there's an error in the document
271
304
  def to_tree
272
- @tree ||= @options[:quiet] ?
273
- Sass::Util.silence_sass_warnings {_to_tree} :
274
- _to_tree
305
+ @tree ||= if @options[:quiet]
306
+ Sass::Util.silence_sass_warnings {_to_tree}
307
+ else
308
+ _to_tree
309
+ end
275
310
  end
276
311
 
277
312
  # Returns the original encoding of the document,
@@ -300,7 +335,8 @@ module Sass
300
335
  #
301
336
  # @private
302
337
  def _dependencies(seen, engines)
303
- return if seen.include?(key = [@options[:filename], @options[:importer]])
338
+ key = [@options[:filename], @options[:importer]]
339
+ return if seen.include?(key)
304
340
  seen << key
305
341
  engines << self
306
342
  to_tree.grep(Tree::ImportNode) do |n|
@@ -311,8 +347,34 @@ module Sass
311
347
 
312
348
  private
313
349
 
314
- def _render
315
- rendered = _to_tree.render
350
+ def _render_with_sourcemap(sourcemap_uri)
351
+ if @options[:filename].nil?
352
+ raise Sass::SyntaxError.new(<<ERR)
353
+ Error generating source map: couldn't determine public URL for the source stylesheet.
354
+ No filename is available so there's nothing for the source map to link to.
355
+ ERR
356
+ elsif @options[:importer].nil? ||
357
+ !(@options[:importer].public_url(@options[:filename]) ||
358
+ @options[:importer].is_a?(Sass::Importers::Filesystem))
359
+ raise Sass::SyntaxError.new(<<ERR)
360
+ Error generating source map: couldn't determine public URL for "#{@options[:filename]}".
361
+ Without a public URL, there's nothing for the source map to link to.
362
+ Custom importers should define the #public_url method.
363
+ ERR
364
+ end
365
+
366
+ rendered, sourcemap = _to_tree.render_with_sourcemap
367
+ compressed = @options[:style] == :compressed
368
+ rendered << "\n" if rendered[-1] != ?\n
369
+ rendered << "\n" unless compressed
370
+ rendered << "/*# sourceMappingURL="
371
+ rendered << Sass::Util.escape_uri(sourcemap_uri)
372
+ rendered << " */"
373
+ rendered = encode_and_set_charset(rendered)
374
+ return rendered, sourcemap
375
+ end
376
+
377
+ def encode_and_set_charset(rendered)
316
378
  return rendered if ruby1_8?
317
379
  begin
318
380
  # Try to convert the result to the original encoding,
@@ -330,7 +392,7 @@ module Sass
330
392
  key = sassc_key
331
393
  sha = Digest::SHA1.hexdigest(@template)
332
394
 
333
- if root = @options[:cache_store].retrieve(key, sha)
395
+ if (root = @options[:cache_store].retrieve(key, sha))
334
396
  root.options = @options
335
397
  return root
336
398
  end
@@ -339,7 +401,7 @@ module Sass
339
401
  check_encoding!
340
402
 
341
403
  if @options[:syntax] == :scss
342
- root = Sass::SCSS::Parser.new(@template, @options[:filename]).parse
404
+ root = Sass::SCSS::Parser.new(@template, @options[:filename], @options[:importer]).parse
343
405
  else
344
406
  root = Tree::RootNode.new(@template)
345
407
  append_children(root, tree(tabulate(@template)).first, true)
@@ -424,12 +486,15 @@ END
424
486
  raise SyntaxError.new(message, :line => index)
425
487
  end
426
488
 
427
- lines << Line.new(line.strip, line_tabs, index, tab_str.size, @options[:filename], [])
489
+ lines << Line.new(line.strip, line_tabs, index, line_tab_str.size, @options[:filename], [])
428
490
  end
429
491
  lines
430
492
  end
431
493
 
494
+ # @comment
495
+ # rubocop:disable ParameterLists
432
496
  def try_comment(line, last, tab_str, comment_tab_str, index)
497
+ # rubocop:enable ParameterLists
433
498
  return unless last && last.comment?
434
499
  # Nested comment stuff must be at least one whitespace char deeper
435
500
  # than the normal indentation
@@ -454,7 +519,8 @@ MSG
454
519
  nodes = []
455
520
  while (line = arr[i]) && line.tabs >= base
456
521
  if line.tabs > base
457
- raise SyntaxError.new("The line was indented #{line.tabs - base} levels deeper than the previous line.",
522
+ raise SyntaxError.new(
523
+ "The line was indented #{line.tabs - base} levels deeper than the previous line.",
458
524
  :line => line.index) if line.tabs > base + 1
459
525
 
460
526
  nodes.last.children, i = tree(arr, i)
@@ -468,6 +534,7 @@ MSG
468
534
 
469
535
  def build_tree(parent, line, root = false)
470
536
  @line = line.index
537
+ @offset = line.offset
471
538
  node_or_nodes = parse_line(parent, line, root)
472
539
 
473
540
  Array(node_or_nodes).each do |node|
@@ -551,12 +618,25 @@ WARNING
551
618
  # which begin with ::,
552
619
  # as well as pseudo-classes
553
620
  # if we're using the new property syntax
554
- Tree::RuleNode.new(parse_interp(line.text))
621
+ Tree::RuleNode.new(parse_interp(line.text), full_line_range(line))
555
622
  else
623
+ name_start_offset = line.offset + 1 # +1 for the leading ':'
556
624
  name, value = line.text.scan(PROPERTY_OLD)[0]
557
625
  raise SyntaxError.new("Invalid property: \"#{line.text}\".",
558
626
  :line => @line) if name.nil? || value.nil?
559
- parse_property(name, parse_interp(name), value, :old, line)
627
+
628
+ value_start_offset = name_end_offset = name_start_offset + name.length
629
+ unless value.empty?
630
+ # +1 and -1 both compensate for the leading ':', which is part of line.text
631
+ value_start_offset = name_start_offset + line.text.index(value, name.length + 1) - 1
632
+ end
633
+
634
+ property = parse_property(name, parse_interp(name), value, :old, line, value_start_offset)
635
+ property.name_source_range = Sass::Source::Range.new(
636
+ Sass::Source::Position.new(@line, to_parser_offset(name_start_offset)),
637
+ Sass::Source::Position.new(@line, to_parser_offset(name_end_offset)),
638
+ @options[:filename], @options[:importer])
639
+ property
560
640
  end
561
641
  when ?$
562
642
  parse_variable(line)
@@ -565,12 +645,12 @@ WARNING
565
645
  when DIRECTIVE_CHAR
566
646
  parse_directive(parent, line, root)
567
647
  when ESCAPE_CHAR
568
- Tree::RuleNode.new(parse_interp(line.text[1..-1]))
648
+ Tree::RuleNode.new(parse_interp(line.text[1..-1]), full_line_range(line))
569
649
  when MIXIN_DEFINITION_CHAR
570
650
  parse_mixin_definition(line)
571
651
  when MIXIN_INCLUDE_CHAR
572
652
  if line.text[1].nil? || line.text[1] == ?\s
573
- Tree::RuleNode.new(parse_interp(line.text))
653
+ Tree::RuleNode.new(parse_interp(line.text), full_line_range(line))
574
654
  else
575
655
  parse_mixin_include(line, root)
576
656
  end
@@ -582,32 +662,70 @@ WARNING
582
662
  def parse_property_or_rule(line)
583
663
  scanner = Sass::Util::MultibyteStringScanner.new(line.text)
584
664
  hack_char = scanner.scan(/[:\*\.]|\#(?!\{)/)
585
- parser = Sass::SCSS::Parser.new(scanner, @options[:filename], @line)
586
-
587
- unless res = parser.parse_interp_ident
588
- return Tree::RuleNode.new(parse_interp(line.text))
665
+ offset = line.offset
666
+ offset += hack_char.length if hack_char
667
+ parser = Sass::SCSS::Parser.new(scanner,
668
+ @options[:filename], @options[:importer],
669
+ @line, to_parser_offset(offset))
670
+
671
+ unless (res = parser.parse_interp_ident)
672
+ parsed = parse_interp(line.text, line.offset)
673
+ return Tree::RuleNode.new(parsed, full_line_range(line))
589
674
  end
675
+
676
+ ident_range = Sass::Source::Range.new(
677
+ Sass::Source::Position.new(@line, to_parser_offset(line.offset)),
678
+ Sass::Source::Position.new(@line, parser.offset),
679
+ @options[:filename], @options[:importer])
680
+ offset = parser.offset - 1
590
681
  res.unshift(hack_char) if hack_char
591
- if comment = scanner.scan(Sass::SCSS::RX::COMMENT)
682
+
683
+ # Handle comments after a property name but before the colon.
684
+ if (comment = scanner.scan(Sass::SCSS::RX::COMMENT))
592
685
  res << comment
686
+ offset += comment.length
593
687
  end
594
688
 
595
689
  name = line.text[0...scanner.pos]
596
- if scanner.scan(/\s*:(?:\s|$)/)
597
- parse_property(name, res, scanner.rest, :new, line)
690
+ if (scanned = scanner.scan(/\s*:(?:\s+|$)/)) # test for a property
691
+ offset += scanned.length
692
+ property = parse_property(name, res, scanner.rest, :new, line, offset)
693
+ property.name_source_range = ident_range
694
+ property
598
695
  else
599
696
  res.pop if comment
600
- Tree::RuleNode.new(res + parse_interp(scanner.rest))
697
+
698
+ if (trailing = (scanner.scan(/\s*#{Sass::SCSS::RX::COMMENT}/) ||
699
+ scanner.scan(/\s*#{Sass::SCSS::RX::SINGLE_LINE_COMMENT}/)))
700
+ trailing.strip!
701
+ end
702
+ interp_parsed = parse_interp(scanner.rest)
703
+ selector_range = Sass::Source::Range.new(
704
+ ident_range.start_pos,
705
+ Sass::Source::Position.new(@line, to_parser_offset(line.offset) + line.text.length),
706
+ @options[:filename], @options[:importer])
707
+ rule = Tree::RuleNode.new(res + interp_parsed, selector_range)
708
+ rule << Tree::CommentNode.new([trailing], :silent) if trailing
709
+ rule
601
710
  end
602
711
  end
603
712
 
604
- def parse_property(name, parsed_name, value, prop, line)
713
+ # @comment
714
+ # rubocop:disable ParameterLists
715
+ def parse_property(name, parsed_name, value, prop, line, start_offset)
716
+ # rubocop:enable ParameterLists
605
717
  if value.strip.empty?
606
- expr = Sass::Script::String.new("")
718
+ expr = Sass::Script::Tree::Literal.new(Sass::Script::Value::String.new(""))
719
+ end_offset = start_offset
607
720
  else
608
- expr = parse_script(value, :offset => line.offset + line.text.index(value))
721
+ expr = parse_script(value, :offset => to_parser_offset(start_offset))
722
+ end_offset = expr.source_range.end_pos.offset - 1
609
723
  end
610
724
  node = Tree::PropNode.new(parse_interp(name), expr, prop)
725
+ node.value_source_range = Sass::Source::Range.new(
726
+ Sass::Source::Position.new(line.index, to_parser_offset(start_offset)),
727
+ Sass::Source::Position.new(line.index, to_parser_offset(end_offset)),
728
+ @options[:filename], @options[:importer])
611
729
  if value.strip.empty? && line.children.empty?
612
730
  raise SyntaxError.new(
613
731
  "Invalid property: \"#{node.declaration}\" (no value)." +
@@ -618,15 +736,24 @@ WARNING
618
736
  end
619
737
 
620
738
  def parse_variable(line)
621
- name, value, default = line.text.scan(Script::MATCH)[0]
739
+ name, value, flags = line.text.scan(Script::MATCH)[0]
622
740
  raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath variable declarations.",
623
741
  :line => @line + 1) unless line.children.empty?
624
742
  raise SyntaxError.new("Invalid variable: \"#{line.text}\".",
625
743
  :line => @line) unless name && value
744
+ flags = flags ? flags.split(/\s+/) : []
745
+ if (invalid_flag = flags.find {|f| f != '!default' && f != '!global'})
746
+ raise SyntaxError.new("Invalid flag \"#{invalid_flag}\".", :line => @line)
747
+ end
626
748
 
627
- expr = parse_script(value, :offset => line.offset + line.text.index(value))
749
+ # This workaround is needed for the case when the variable value is part of the identifier,
750
+ # otherwise we end up with the offset equal to the value index inside the name:
751
+ # $red_color: red;
752
+ var_lhs_length = 1 + name.length # 1 stands for '$'
753
+ index = line.text.index(value, line.offset + var_lhs_length) || 0
754
+ expr = parse_script(value, :offset => to_parser_offset(line.offset + index))
628
755
 
629
- Tree::VariableNode.new(name, expr, default)
756
+ Tree::VariableNode.new(name, expr, flags.include?('!default'), flags.include?('!global'))
630
757
  end
631
758
 
632
759
  def parse_comment(line)
@@ -636,107 +763,162 @@ WARNING
636
763
  if silent
637
764
  value = [line.text]
638
765
  else
639
- value = self.class.parse_interp(line.text, line.index, line.offset, :filename => @filename)
640
- value[0].slice!(2) if loud # get rid of the "!"
766
+ value = self.class.parse_interp(
767
+ line.text, line.index, to_parser_offset(line.offset), :filename => @filename)
641
768
  end
642
769
  value = with_extracted_values(value) do |str|
643
770
  str = str.gsub(/^#{line.comment_tab_str}/m, '')[2..-1] # get rid of // or /*
644
771
  format_comment_text(str, silent)
645
772
  end
646
- type = if silent then :silent elsif loud then :loud else :normal end
773
+ type = if silent
774
+ :silent
775
+ elsif loud
776
+ :loud
777
+ else
778
+ :normal
779
+ end
647
780
  Tree::CommentNode.new(value, type)
648
781
  else
649
- Tree::RuleNode.new(parse_interp(line.text))
782
+ Tree::RuleNode.new(parse_interp(line.text), full_line_range(line))
650
783
  end
651
784
  end
652
785
 
786
+ DIRECTIVES = Set[:mixin, :include, :function, :return, :debug, :warn, :for,
787
+ :each, :while, :if, :else, :extend, :import, :media, :charset, :content,
788
+ :at_root]
789
+
790
+ # @comment
791
+ # rubocop:disable MethodLength
653
792
  def parse_directive(parent, line, root)
654
793
  directive, whitespace, value = line.text[1..-1].split(/(\s+)/, 2)
655
794
  offset = directive.size + whitespace.size + 1 if whitespace
656
795
 
657
- # If value begins with url( or ",
658
- # it's a CSS @import rule and we don't want to touch it.
659
- case directive
660
- when 'import'
661
- parse_import(line, value, offset)
662
- when 'mixin'
663
- parse_mixin_definition(line)
664
- when 'content'
665
- parse_content_directive(line)
666
- when 'include'
667
- parse_mixin_include(line, root)
668
- when 'function'
669
- parse_function(line, root)
670
- when 'for'
671
- parse_for(line, root, value)
672
- when 'each'
673
- parse_each(line, root, value)
674
- when 'else'
675
- parse_else(parent, line, value)
676
- when 'while'
677
- raise SyntaxError.new("Invalid while directive '@while': expected expression.") unless value
678
- Tree::WhileNode.new(parse_script(value, :offset => offset))
679
- when 'if'
680
- raise SyntaxError.new("Invalid if directive '@if': expected expression.") unless value
681
- Tree::IfNode.new(parse_script(value, :offset => offset))
682
- when 'debug'
683
- raise SyntaxError.new("Invalid debug directive '@debug': expected expression.") unless value
684
- raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath debug directives.",
685
- :line => @line + 1) unless line.children.empty?
686
- offset = line.offset + line.text.index(value).to_i
687
- Tree::DebugNode.new(parse_script(value, :offset => offset))
688
- when 'extend'
689
- raise SyntaxError.new("Invalid extend directive '@extend': expected expression.") unless value
690
- raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath extend directives.",
691
- :line => @line + 1) unless line.children.empty?
692
- optional = !!value.gsub!(/\s+#{Sass::SCSS::RX::OPTIONAL}$/, '')
693
- offset = line.offset + line.text.index(value).to_i
694
- Tree::ExtendNode.new(parse_interp(value, offset), optional)
695
- when 'warn'
696
- raise SyntaxError.new("Invalid warn directive '@warn': expected expression.") unless value
697
- raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath warn directives.",
698
- :line => @line + 1) unless line.children.empty?
699
- offset = line.offset + line.text.index(value).to_i
700
- Tree::WarnNode.new(parse_script(value, :offset => offset))
701
- when 'return'
702
- raise SyntaxError.new("Invalid @return: expected expression.") unless value
703
- raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath return directives.",
704
- :line => @line + 1) unless line.children.empty?
705
- offset = line.offset + line.text.index(value).to_i
706
- Tree::ReturnNode.new(parse_script(value, :offset => offset))
707
- when 'charset'
708
- name = value && value[/\A(["'])(.*)\1\Z/, 2] #"
709
- raise SyntaxError.new("Invalid charset directive '@charset': expected string.") unless name
710
- raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath charset directives.",
711
- :line => @line + 1) unless line.children.empty?
712
- Tree::CharsetNode.new(name)
713
- when 'media'
796
+ directive_name = directive.gsub('-', '_').to_sym
797
+ if DIRECTIVES.include?(directive_name)
798
+ return send("parse_#{directive_name}_directive", parent, line, root, value, offset)
799
+ end
800
+
801
+ unprefixed_directive = directive.gsub(/^-[a-z0-9]+-/i, '')
802
+ if unprefixed_directive == 'supports'
714
803
  parser = Sass::SCSS::Parser.new(value, @options[:filename], @line)
715
- Tree::MediaNode.new(parser.parse_media_query_list.to_a)
716
- else
717
- unprefixed_directive = directive.gsub(/^-[a-z0-9]+-/i, '')
718
- if unprefixed_directive == 'supports'
719
- parser = Sass::SCSS::Parser.new(value, @options[:filename], @line)
720
- return Tree::SupportsNode.new(directive, parser.parse_supports_condition)
721
- end
804
+ return Tree::SupportsNode.new(directive, parser.parse_supports_condition)
805
+ end
806
+
807
+ Tree::DirectiveNode.new(
808
+ value.nil? ? ["@#{directive}"] : ["@#{directive} "] + parse_interp(value, offset))
809
+ end
810
+
811
+ def parse_while_directive(parent, line, root, value, offset)
812
+ raise SyntaxError.new("Invalid while directive '@while': expected expression.") unless value
813
+ Tree::WhileNode.new(parse_script(value, :offset => offset))
814
+ end
815
+
816
+ def parse_if_directive(parent, line, root, value, offset)
817
+ raise SyntaxError.new("Invalid if directive '@if': expected expression.") unless value
818
+ Tree::IfNode.new(parse_script(value, :offset => offset))
819
+ end
820
+
821
+ def parse_debug_directive(parent, line, root, value, offset)
822
+ raise SyntaxError.new("Invalid debug directive '@debug': expected expression.") unless value
823
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath debug directives.",
824
+ :line => @line + 1) unless line.children.empty?
825
+ offset = line.offset + line.text.index(value).to_i
826
+ Tree::DebugNode.new(parse_script(value, :offset => offset))
827
+ end
722
828
 
723
- Tree::DirectiveNode.new(
724
- value.nil? ? ["@#{directive}"] : ["@#{directive} "] + parse_interp(value, offset))
829
+ def parse_extend_directive(parent, line, root, value, offset)
830
+ raise SyntaxError.new("Invalid extend directive '@extend': expected expression.") unless value
831
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath extend directives.",
832
+ :line => @line + 1) unless line.children.empty?
833
+ optional = !!value.gsub!(/\s+#{Sass::SCSS::RX::OPTIONAL}$/, '')
834
+ offset = line.offset + line.text.index(value).to_i
835
+ interp_parsed = parse_interp(value, offset)
836
+ selector_range = Sass::Source::Range.new(
837
+ Sass::Source::Position.new(@line, to_parser_offset(offset)),
838
+ Sass::Source::Position.new(@line, to_parser_offset(line.offset) + line.text.length),
839
+ @options[:filename], @options[:importer]
840
+ )
841
+ Tree::ExtendNode.new(interp_parsed, optional, selector_range)
842
+ end
843
+ # @comment
844
+ # rubocop:enable MethodLength
845
+
846
+ def parse_warn_directive(parent, line, root, value, offset)
847
+ raise SyntaxError.new("Invalid warn directive '@warn': expected expression.") unless value
848
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath warn directives.",
849
+ :line => @line + 1) unless line.children.empty?
850
+ offset = line.offset + line.text.index(value).to_i
851
+ Tree::WarnNode.new(parse_script(value, :offset => offset))
852
+ end
853
+
854
+ def parse_return_directive(parent, line, root, value, offset)
855
+ raise SyntaxError.new("Invalid @return: expected expression.") unless value
856
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath return directives.",
857
+ :line => @line + 1) unless line.children.empty?
858
+ offset = line.offset + line.text.index(value).to_i
859
+ Tree::ReturnNode.new(parse_script(value, :offset => offset))
860
+ end
861
+
862
+ def parse_charset_directive(parent, line, root, value, offset)
863
+ name = value && value[/\A(["'])(.*)\1\Z/, 2] # "
864
+ raise SyntaxError.new("Invalid charset directive '@charset': expected string.") unless name
865
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath charset directives.",
866
+ :line => @line + 1) unless line.children.empty?
867
+ Tree::CharsetNode.new(name)
868
+ end
869
+
870
+ def parse_media_directive(parent, line, root, value, offset)
871
+ parser = Sass::SCSS::Parser.new(value,
872
+ @options[:filename], @options[:importer],
873
+ @line, to_parser_offset(@offset))
874
+ offset = line.offset + line.text.index('media').to_i - 1
875
+ parsed_media_query_list = parser.parse_media_query_list.to_a
876
+ node = Tree::MediaNode.new(parsed_media_query_list)
877
+ node.source_range = Sass::Source::Range.new(
878
+ Sass::Source::Position.new(@line, to_parser_offset(offset)),
879
+ Sass::Source::Position.new(@line, to_parser_offset(line.offset) + line.text.length),
880
+ @options[:filename], @options[:importer])
881
+ node
882
+ end
883
+
884
+ def parse_at_root_directive(parent, line, root, value, offset)
885
+ return Sass::Tree::AtRootNode.new unless value
886
+
887
+ if value.start_with?('(')
888
+ parser = Sass::SCSS::Parser.new(value,
889
+ @options[:filename], @options[:importer],
890
+ @line, to_parser_offset(@offset))
891
+ offset = line.offset + line.text.index('at-root').to_i - 1
892
+ return Tree::AtRootNode.new(parser.parse_at_root_query)
725
893
  end
894
+
895
+ at_root_node = Tree::AtRootNode.new
896
+ parsed = parse_interp(value, offset)
897
+ rule_node = Tree::RuleNode.new(parsed, full_line_range(line))
898
+
899
+ # The caller expects to automatically add children to the returned node
900
+ # and we want it to add children to the rule node instead, so we
901
+ # manually handle the wiring here and return nil so the caller doesn't
902
+ # duplicate our efforts.
903
+ append_children(rule_node, line.children, false)
904
+ at_root_node << rule_node
905
+ parent << at_root_node
906
+ nil
726
907
  end
727
908
 
728
- def parse_for(line, root, text)
729
- var, from_expr, to_name, to_expr = text.scan(/^([^\s]+)\s+from\s+(.+)\s+(to|through)\s+(.+)$/).first
909
+ def parse_for_directive(parent, line, root, value, offset)
910
+ var, from_expr, to_name, to_expr =
911
+ value.scan(/^([^\s]+)\s+from\s+(.+)\s+(to|through)\s+(.+)$/).first
730
912
 
731
913
  if var.nil? # scan failed, try to figure out why for error message
732
- if text !~ /^[^\s]+/
914
+ if value !~ /^[^\s]+/
733
915
  expected = "variable name"
734
- elsif text !~ /^[^\s]+\s+from\s+.+/
916
+ elsif value !~ /^[^\s]+\s+from\s+.+/
735
917
  expected = "'from <expr>'"
736
918
  else
737
919
  expected = "'to <expr>' or 'through <expr>'"
738
920
  end
739
- raise SyntaxError.new("Invalid for directive '@for #{text}': expected #{expected}.")
921
+ raise SyntaxError.new("Invalid for directive '@for #{value}': expected #{expected}.")
740
922
  end
741
923
  raise SyntaxError.new("Invalid variable \"#{var}\".") unless var =~ Script::VALIDATE
742
924
 
@@ -746,31 +928,35 @@ WARNING
746
928
  Tree::ForNode.new(var, parsed_from, parsed_to, to_name == 'to')
747
929
  end
748
930
 
749
- def parse_each(line, root, text)
750
- var, list_expr = text.scan(/^([^\s]+)\s+in\s+(.+)$/).first
931
+ def parse_each_directive(parent, line, root, value, offset)
932
+ vars, list_expr = value.scan(/^([^\s]+(?:\s*,\s*[^\s]+)*)\s+in\s+(.+)$/).first
751
933
 
752
- if var.nil? # scan failed, try to figure out why for error message
753
- if text !~ /^[^\s]+/
934
+ if vars.nil? # scan failed, try to figure out why for error message
935
+ if value !~ /^[^\s]+/
754
936
  expected = "variable name"
755
- elsif text !~ /^[^\s]+\s+from\s+.+/
937
+ elsif value !~ /^[^\s]+(?:\s*,\s*[^\s]+)*[^\s]+\s+from\s+.+/
756
938
  expected = "'in <expr>'"
757
939
  end
758
- raise SyntaxError.new("Invalid for directive '@each #{text}': expected #{expected}.")
940
+ raise SyntaxError.new("Invalid each directive '@each #{value}': expected #{expected}.")
941
+ end
942
+
943
+ vars = vars.split(',').map do |var|
944
+ var.strip!
945
+ raise SyntaxError.new("Invalid variable \"#{var}\".") unless var =~ Script::VALIDATE
946
+ var[1..-1]
759
947
  end
760
- raise SyntaxError.new("Invalid variable \"#{var}\".") unless var =~ Script::VALIDATE
761
948
 
762
- var = var[1..-1]
763
949
  parsed_list = parse_script(list_expr, :offset => line.offset + line.text.index(list_expr))
764
- Tree::EachNode.new(var, parsed_list)
950
+ Tree::EachNode.new(vars, parsed_list)
765
951
  end
766
952
 
767
- def parse_else(parent, line, text)
953
+ def parse_else_directive(parent, line, root, value, offset)
768
954
  previous = parent.children.last
769
955
  raise SyntaxError.new("@else must come after @if.") unless previous.is_a?(Tree::IfNode)
770
956
 
771
- if text
772
- if text !~ /^if\s+(.+)/
773
- raise SyntaxError.new("Invalid else directive '@else #{text}': expected 'if <expr>'.")
957
+ if value
958
+ if value !~ /^if\s+(.+)/
959
+ raise SyntaxError.new("Invalid else directive '@else #{value}': expected 'if <expr>'.")
774
960
  end
775
961
  expr = parse_script($1, :offset => line.offset + line.text.index($1))
776
962
  end
@@ -781,7 +967,7 @@ WARNING
781
967
  nil
782
968
  end
783
969
 
784
- def parse_import(line, value, offset)
970
+ def parse_import_directive(parent, line, root, value, offset)
785
971
  raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath import directives.",
786
972
  :line => @line + 1) unless line.children.empty?
787
973
 
@@ -789,8 +975,9 @@ WARNING
789
975
  values = []
790
976
 
791
977
  loop do
792
- unless node = parse_import_arg(scanner, offset + scanner.pos)
793
- raise SyntaxError.new("Invalid @import: expected file to import, was #{scanner.rest.inspect}",
978
+ unless (node = parse_import_arg(scanner, offset + scanner.pos))
979
+ raise SyntaxError.new(
980
+ "Invalid @import: expected file to import, was #{scanner.rest.inspect}",
794
981
  :line => @line)
795
982
  end
796
983
  values << node
@@ -802,35 +989,80 @@ WARNING
802
989
  :line => @line)
803
990
  end
804
991
 
805
- return values
992
+ values
806
993
  end
807
994
 
995
+ # @comment
996
+ # rubocop:disable MethodLength
808
997
  def parse_import_arg(scanner, offset)
809
998
  return if scanner.eos?
810
999
 
811
1000
  if scanner.match?(/url\(/i)
812
- script_parser = Sass::Script::Parser.new(scanner, @line, offset, @options)
1001
+ script_parser = Sass::Script::Parser.new(scanner, @line, to_parser_offset(offset), @options)
813
1002
  str = script_parser.parse_string
814
- media_parser = Sass::SCSS::Parser.new(scanner, @options[:filename], @line)
815
- media = media_parser.parse_media_query_list
816
- return Tree::CssImportNode.new(str, media.to_a)
1003
+
1004
+ media_parser = Sass::SCSS::Parser.new(scanner,
1005
+ @options[:filename], @options[:importer],
1006
+ @line, str.source_range.end_pos.offset)
1007
+ if (media = media_parser.parse_media_query_list)
1008
+ end_pos = Sass::Source::Position.new(@line, media_parser.offset + 1)
1009
+ node = Tree::CssImportNode.new(str, media.to_a)
1010
+ else
1011
+ end_pos = str.source_range.end_pos
1012
+ node = Tree::CssImportNode.new(str)
1013
+ end
1014
+
1015
+ node.source_range = Sass::Source::Range.new(
1016
+ str.source_range.start_pos, end_pos,
1017
+ @options[:filename], @options[:importer])
1018
+ return node
817
1019
  end
818
1020
 
819
- unless str = scanner.scan(Sass::SCSS::RX::STRING)
820
- return Tree::ImportNode.new(scanner.scan(/[^,;]+/))
1021
+ unless (str = scanner.scan(Sass::SCSS::RX::STRING))
1022
+ scanned = scanner.scan(/[^,;]+/)
1023
+ node = Tree::ImportNode.new(scanned)
1024
+ start_parser_offset = to_parser_offset(offset)
1025
+ node.source_range = Sass::Source::Range.new(
1026
+ Sass::Source::Position.new(@line, start_parser_offset),
1027
+ Sass::Source::Position.new(@line, start_parser_offset + scanned.length),
1028
+ @options[:filename], @options[:importer])
1029
+ return node
821
1030
  end
822
1031
 
1032
+ start_offset = offset
1033
+ offset += str.length
823
1034
  val = scanner[1] || scanner[2]
824
- scanner.scan(/\s*/)
1035
+ scanned = scanner.scan(/\s*/)
825
1036
  if !scanner.match?(/[,;]|$/)
826
- media_parser = Sass::SCSS::Parser.new(scanner, @options[:filename], @line)
1037
+ offset += scanned.length if scanned
1038
+ media_parser = Sass::SCSS::Parser.new(scanner,
1039
+ @options[:filename], @options[:importer], @line, offset)
827
1040
  media = media_parser.parse_media_query_list
828
- Tree::CssImportNode.new(str || uri, media.to_a)
829
- elsif val =~ /^(https?:)?\/\//
830
- Tree::CssImportNode.new("url(#{val})")
1041
+ node = Tree::CssImportNode.new(str || uri, media.to_a)
1042
+ node.source_range = Sass::Source::Range.new(
1043
+ Sass::Source::Position.new(@line, to_parser_offset(start_offset)),
1044
+ Sass::Source::Position.new(@line, media_parser.offset),
1045
+ @options[:filename], @options[:importer])
1046
+ elsif val =~ %r{^(https?:)?//}
1047
+ node = Tree::CssImportNode.new("url(#{val})")
1048
+ node.source_range = Sass::Source::Range.new(
1049
+ Sass::Source::Position.new(@line, to_parser_offset(start_offset)),
1050
+ Sass::Source::Position.new(@line, to_parser_offset(offset)),
1051
+ @options[:filename], @options[:importer])
831
1052
  else
832
- Tree::ImportNode.new(val)
1053
+ node = Tree::ImportNode.new(val)
1054
+ node.source_range = Sass::Source::Range.new(
1055
+ Sass::Source::Position.new(@line, to_parser_offset(start_offset)),
1056
+ Sass::Source::Position.new(@line, to_parser_offset(offset)),
1057
+ @options[:filename], @options[:importer])
833
1058
  end
1059
+ node
1060
+ end
1061
+ # @comment
1062
+ # rubocop:enable MethodLength
1063
+
1064
+ def parse_mixin_directive(parent, line, root, value, offset)
1065
+ parse_mixin_definition(line)
834
1066
  end
835
1067
 
836
1068
  MIXIN_DEF_RE = /^(?:=|@mixin)\s*(#{Sass::SCSS::RX::IDENT})(.*)$/
@@ -839,45 +1071,53 @@ WARNING
839
1071
  raise SyntaxError.new("Invalid mixin \"#{line.text[1..-1]}\".") if name.nil?
840
1072
 
841
1073
  offset = line.offset + line.text.size - arg_string.size
842
- args, splat = Script::Parser.new(arg_string.strip, @line, offset, @options).
1074
+ args, splat = Script::Parser.new(arg_string.strip, @line, to_parser_offset(offset), @options).
843
1075
  parse_mixin_definition_arglist
844
1076
  Tree::MixinDefNode.new(name, args, splat)
845
1077
  end
846
1078
 
847
1079
  CONTENT_RE = /^@content\s*(.+)?$/
848
- def parse_content_directive(line)
1080
+ def parse_content_directive(parent, line, root, value, offset)
849
1081
  trailing = line.text.scan(CONTENT_RE).first.first
850
- raise SyntaxError.new("Invalid content directive. Trailing characters found: \"#{trailing}\".") unless trailing.nil?
1082
+ unless trailing.nil?
1083
+ raise SyntaxError.new(
1084
+ "Invalid content directive. Trailing characters found: \"#{trailing}\".")
1085
+ end
851
1086
  raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath @content directives.",
852
1087
  :line => line.index + 1) unless line.children.empty?
853
1088
  Tree::ContentNode.new
854
1089
  end
855
1090
 
1091
+ def parse_include_directive(parent, line, root, value, offset)
1092
+ parse_mixin_include(line, root)
1093
+ end
1094
+
856
1095
  MIXIN_INCLUDE_RE = /^(?:\+|@include)\s*(#{Sass::SCSS::RX::IDENT})(.*)$/
857
1096
  def parse_mixin_include(line, root)
858
1097
  name, arg_string = line.text.scan(MIXIN_INCLUDE_RE).first
859
1098
  raise SyntaxError.new("Invalid mixin include \"#{line.text}\".") if name.nil?
860
1099
 
861
1100
  offset = line.offset + line.text.size - arg_string.size
862
- args, keywords, splat = Script::Parser.new(arg_string.strip, @line, offset, @options).
863
- parse_mixin_include_arglist
864
- Tree::MixinNode.new(name, args, keywords, splat)
1101
+ args, keywords, splat, kwarg_splat =
1102
+ Script::Parser.new(arg_string.strip, @line, to_parser_offset(offset), @options).
1103
+ parse_mixin_include_arglist
1104
+ Tree::MixinNode.new(name, args, keywords, splat, kwarg_splat)
865
1105
  end
866
1106
 
867
1107
  FUNCTION_RE = /^@function\s*(#{Sass::SCSS::RX::IDENT})(.*)$/
868
- def parse_function(line, root)
1108
+ def parse_function_directive(parent, line, root, value, offset)
869
1109
  name, arg_string = line.text.scan(FUNCTION_RE).first
870
1110
  raise SyntaxError.new("Invalid function definition \"#{line.text}\".") if name.nil?
871
1111
 
872
1112
  offset = line.offset + line.text.size - arg_string.size
873
- args, splat = Script::Parser.new(arg_string.strip, @line, offset, @options).
1113
+ args, splat = Script::Parser.new(arg_string.strip, @line, to_parser_offset(offset), @options).
874
1114
  parse_function_definition_arglist
875
1115
  Tree::FunctionNode.new(name, args, splat)
876
1116
  end
877
1117
 
878
1118
  def parse_script(script, options = {})
879
1119
  line = options[:line] || @line
880
- offset = options[:offset] || 0
1120
+ offset = options[:offset] || @offset + 1
881
1121
  Script.parse(script, line, offset, @options)
882
1122
  end
883
1123
 
@@ -890,7 +1130,7 @@ WARNING
890
1130
  end
891
1131
 
892
1132
  return silent ? "//" : "/* */" if content.empty?
893
- content.last.gsub!(%r{ ?\*/ *$}, '')
1133
+ content.last.gsub!(/ ?\*\/ *$/, '')
894
1134
  content.map! {|l| l.gsub!(/^\*( ?)/, '\1') || (l.empty? ? "" : " ") + l}
895
1135
  content.first.gsub!(/^ /, '') unless removed_first
896
1136
  if silent
@@ -905,8 +1145,20 @@ WARNING
905
1145
  self.class.parse_interp(text, @line, offset, :filename => @filename)
906
1146
  end
907
1147
 
1148
+ # Parser tracks 1-based line and offset, so our offset should be converted.
1149
+ def to_parser_offset(offset)
1150
+ offset + 1
1151
+ end
1152
+
1153
+ def full_line_range(line)
1154
+ Sass::Source::Range.new(
1155
+ Sass::Source::Position.new(@line, to_parser_offset(line.offset)),
1156
+ Sass::Source::Position.new(@line, to_parser_offset(line.offset) + line.text.length),
1157
+ @options[:filename], @options[:importer])
1158
+ end
1159
+
908
1160
  # It's important that this have strings (at least)
909
- # at the beginning, the end, and between each Script::Node.
1161
+ # at the beginning, the end, and between each Script::Tree::Node.
910
1162
  #
911
1163
  # @private
912
1164
  def self.parse_interp(text, line, offset, options)
@@ -914,12 +1166,13 @@ WARNING
914
1166
  rest = Sass::Shared.handle_interpolation text do |scan|
915
1167
  escapes = scan[2].size
916
1168
  res << scan.matched[0...-2 - escapes]
917
- if escapes % 2 == 1
1169
+ if escapes.odd?
918
1170
  res << "\\" * (escapes - 1) << '#{'
919
1171
  else
920
1172
  res << "\\" * [0, escapes - 1].max
1173
+ # Add 1 to emulate to_parser_offset.
921
1174
  res << Script::Parser.new(
922
- scan, line, offset + scan.pos - scan.matched_size, options).
1175
+ scan, line, offset + scan.pos - scan.matched_size + 1, options).
923
1176
  parse_interpolated
924
1177
  end
925
1178
  end