sass 3.1.0 → 3.3.0

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