sass 3.1.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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