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
@@ -1,4 +1,3 @@
1
- require 'strscan'
2
1
  require 'set'
3
2
 
4
3
  module Sass
@@ -6,14 +5,30 @@ module Sass
6
5
  # The parser for SCSS.
7
6
  # It parses a string of code into a tree of {Sass::Tree::Node}s.
8
7
  class Parser
8
+ # Expose for the SASS parser.
9
+ attr_accessor :offset
10
+
9
11
  # @param str [String, StringScanner] The source document to parse.
10
12
  # Note that `Parser` *won't* raise a nice error message if this isn't properly parsed;
11
13
  # for that, you should use the higher-level {Sass::Engine} or {Sass::CSS}.
12
- # @param line [Fixnum] The line on which the source string appeared,
13
- # if it's part of another document
14
- def initialize(str, line = 1)
14
+ # @param filename [String] The name of the file being parsed. Used for
15
+ # warnings and source maps.
16
+ # @param importer [Sass::Importers::Base] The importer used to import the
17
+ # file being parsed. Used for source maps.
18
+ # @param line [Fixnum] The 1-based line on which the source string appeared,
19
+ # if it's part of another document.
20
+ # @param offset [Fixnum] The 1-based character (not byte) offset in the line on
21
+ # which the source string starts. Used for error reporting and sourcemap
22
+ # building.
23
+ # @comment
24
+ # rubocop:disable ParameterLists
25
+ def initialize(str, filename, importer, line = 1, offset = 1)
26
+ # rubocop:enable ParameterLists
15
27
  @template = str
28
+ @filename = filename
29
+ @importer = importer
16
30
  @line = line
31
+ @offset = offset
17
32
  @strs = []
18
33
  end
19
34
 
@@ -32,28 +47,72 @@ module Sass
32
47
  # Note that this won't assert that the identifier takes up the entire input string;
33
48
  # it's meant to be used with `StringScanner`s as part of other parsers.
34
49
  #
35
- # @return [Array<String, Sass::Script::Node>, nil]
50
+ # @return [Array<String, Sass::Script::Tree::Node>, nil]
36
51
  # The interpolated identifier, or nil if none could be parsed
37
52
  def parse_interp_ident
38
53
  init_scanner!
39
54
  interp_ident
40
55
  end
41
56
 
57
+ # Parses a media query list.
58
+ #
59
+ # @return [Sass::Media::QueryList] The parsed query list
60
+ # @raise [Sass::SyntaxError] if there's a syntax error in the query list,
61
+ # or if it doesn't take up the entire input string.
62
+ def parse_media_query_list
63
+ init_scanner!
64
+ ql = media_query_list
65
+ expected("media query list") unless @scanner.eos?
66
+ ql
67
+ end
68
+
69
+ # Parses an at-root query.
70
+ #
71
+ # @return [Array<String, Sass::Script;:Tree::Node>] The interpolated query.
72
+ # @raise [Sass::SyntaxError] if there's a syntax error in the query,
73
+ # or if it doesn't take up the entire input string.
74
+ def parse_at_root_query
75
+ init_scanner!
76
+ query = at_root_query
77
+ expected("@at-root query list") unless @scanner.eos?
78
+ query
79
+ end
80
+
81
+ # Parses a supports query condition.
82
+ #
83
+ # @return [Sass::Supports::Condition] The parsed condition
84
+ # @raise [Sass::SyntaxError] if there's a syntax error in the condition,
85
+ # or if it doesn't take up the entire input string.
86
+ def parse_supports_condition
87
+ init_scanner!
88
+ condition = supports_condition
89
+ expected("supports condition") unless @scanner.eos?
90
+ condition
91
+ end
92
+
42
93
  private
43
94
 
44
95
  include Sass::SCSS::RX
45
96
 
97
+ def source_position
98
+ Sass::Source::Position.new(@line, @offset)
99
+ end
100
+
101
+ def range(start_pos, end_pos = source_position)
102
+ Sass::Source::Range.new(start_pos, end_pos, @filename, @importer)
103
+ end
104
+
46
105
  def init_scanner!
47
106
  @scanner =
48
107
  if @template.is_a?(StringScanner)
49
108
  @template
50
109
  else
51
- StringScanner.new(@template.gsub("\r", ""))
110
+ Sass::Util::MultibyteStringScanner.new(@template.gsub("\r", ""))
52
111
  end
53
112
  end
54
113
 
55
114
  def stylesheet
56
- node = node(Sass::Tree::RootNode.new(@scanner.string))
115
+ node = node(Sass::Tree::RootNode.new(@scanner.string), source_position)
57
116
  block_contents(node, :stylesheet) {s(node)}
58
117
  end
59
118
 
@@ -87,34 +146,61 @@ module Sass
87
146
  end
88
147
 
89
148
  def process_comment(text, node)
90
- single_line = text =~ /^\/\//
91
- pre_str = single_line ? "" : @scanner.
92
- string[0...@scanner.pos].
93
- reverse[/.*?\*\/(.*?)($|\Z)/, 1].
94
- reverse.gsub(/[^\s]/, ' ')
95
- text = text.sub(/^\s*\/\//, '/*').gsub(/^\s*\/\//, ' *') + ' */' if single_line
96
- comment = Sass::Tree::CommentNode.new(pre_str + text, single_line)
97
- comment.line = @line - text.count("\n")
149
+ silent = text =~ %r{\A//}
150
+ loud = !silent && text =~ %r{\A/[/*]!}
151
+ line = @line - text.count("\n")
152
+
153
+ if silent
154
+ value = [text.sub(%r{\A\s*//}, '/*').gsub(%r{^\s*//}, ' *') + ' */']
155
+ else
156
+ value = Sass::Engine.parse_interp(
157
+ text, line, @scanner.pos - text.size, :filename => @filename)
158
+ value.unshift(@scanner.
159
+ string[0...@scanner.pos].
160
+ reverse[/.*?\*\/(.*?)($|\Z)/, 1].
161
+ reverse.gsub(/[^\s]/, ' '))
162
+ end
163
+
164
+ type = if silent
165
+ :silent
166
+ elsif loud
167
+ :loud
168
+ else
169
+ :normal
170
+ end
171
+ comment = Sass::Tree::CommentNode.new(value, type)
172
+ comment.line = line
98
173
  node << comment
99
174
  end
100
175
 
101
176
  DIRECTIVES = Set[:mixin, :include, :function, :return, :debug, :warn, :for,
102
- :each, :while, :if, :else, :extend, :import, :media, :charset]
177
+ :each, :while, :if, :else, :extend, :import, :media, :charset, :content,
178
+ :_moz_document, :at_root]
179
+
180
+ PREFIXED_DIRECTIVES = Set[:supports]
103
181
 
104
182
  def directive
183
+ start_pos = source_position
105
184
  return unless tok(/@/)
106
185
  name = tok!(IDENT)
107
186
  ss
108
187
 
109
- if dir = special_directive(name)
188
+ if (dir = special_directive(name, start_pos))
189
+ return dir
190
+ elsif (dir = prefixed_directive(name, start_pos))
110
191
  return dir
111
192
  end
112
193
 
113
194
  # Most at-rules take expressions (e.g. @import),
114
- # but some (e.g. @page) take selector-like arguments
115
- val = str {break unless expr}
116
- val ||= CssParser.new(@scanner, @line).parse_selector_string
117
- node = node(Sass::Tree::DirectiveNode.new("@#{name} #{val}".strip))
195
+ # but some (e.g. @page) take selector-like arguments.
196
+ # Some take no arguments at all.
197
+ val = expr || selector
198
+ val = val ? ["@#{name} "] + Sass::Util.strip_string_array(val) : ["@#{name}"]
199
+ directive_body(val, start_pos)
200
+ end
201
+
202
+ def directive_body(value, start_pos)
203
+ node = Sass::Tree::DirectiveNode.new(value)
118
204
 
119
205
  if tok(/\{/)
120
206
  node.has_children = true
@@ -122,48 +208,65 @@ module Sass
122
208
  tok!(/\}/)
123
209
  end
124
210
 
125
- node
211
+ node(node, start_pos)
126
212
  end
127
213
 
128
- def special_directive(name)
214
+ def special_directive(name, start_pos)
129
215
  sym = name.gsub('-', '_').to_sym
130
- DIRECTIVES.include?(sym) && send("#{sym}_directive")
216
+ DIRECTIVES.include?(sym) && send("#{sym}_directive", start_pos)
217
+ end
218
+
219
+ def prefixed_directive(name, start_pos)
220
+ sym = name.gsub(/^-[a-z0-9]+-/i, '').gsub('-', '_').to_sym
221
+ PREFIXED_DIRECTIVES.include?(sym) && send("#{sym}_directive", name, start_pos)
131
222
  end
132
223
 
133
- def mixin_directive
224
+ def mixin_directive(start_pos)
134
225
  name = tok! IDENT
135
- args = sass_script(:parse_mixin_definition_arglist)
226
+ args, splat = sass_script(:parse_mixin_definition_arglist)
136
227
  ss
137
- block(node(Sass::Tree::MixinDefNode.new(name, args)), :directive)
228
+ block(node(Sass::Tree::MixinDefNode.new(name, args, splat), start_pos), :directive)
138
229
  end
139
230
 
140
- def include_directive
231
+ def include_directive(start_pos)
141
232
  name = tok! IDENT
142
- args, keywords = sass_script(:parse_mixin_include_arglist)
233
+ args, keywords, splat, kwarg_splat = sass_script(:parse_mixin_include_arglist)
234
+ ss
235
+ include_node = node(
236
+ Sass::Tree::MixinNode.new(name, args, keywords, splat, kwarg_splat), start_pos)
237
+ if tok?(/\{/)
238
+ include_node.has_children = true
239
+ block(include_node, :directive)
240
+ else
241
+ include_node
242
+ end
243
+ end
244
+
245
+ def content_directive(start_pos)
143
246
  ss
144
- node(Sass::Tree::MixinNode.new(name, args, keywords))
247
+ node(Sass::Tree::ContentNode.new, start_pos)
145
248
  end
146
249
 
147
- def function_directive
250
+ def function_directive(start_pos)
148
251
  name = tok! IDENT
149
- args = sass_script(:parse_function_definition_arglist)
252
+ args, splat = sass_script(:parse_function_definition_arglist)
150
253
  ss
151
- block(node(Sass::Tree::FunctionNode.new(name, args)), :function)
254
+ block(node(Sass::Tree::FunctionNode.new(name, args, splat), start_pos), :function)
152
255
  end
153
256
 
154
- def return_directive
155
- node(Sass::Tree::ReturnNode.new(sass_script(:parse)))
257
+ def return_directive(start_pos)
258
+ node(Sass::Tree::ReturnNode.new(sass_script(:parse)), start_pos)
156
259
  end
157
260
 
158
- def debug_directive
159
- node(Sass::Tree::DebugNode.new(sass_script(:parse)))
261
+ def debug_directive(start_pos)
262
+ node(Sass::Tree::DebugNode.new(sass_script(:parse)), start_pos)
160
263
  end
161
264
 
162
- def warn_directive
163
- node(Sass::Tree::WarnNode.new(sass_script(:parse)))
265
+ def warn_directive(start_pos)
266
+ node(Sass::Tree::WarnNode.new(sass_script(:parse)), start_pos)
164
267
  end
165
268
 
166
- def for_directive
269
+ def for_directive(start_pos)
167
270
  tok!(/\$/)
168
271
  var = tok! IDENT
169
272
  ss
@@ -177,31 +280,37 @@ module Sass
177
280
  to = sass_script(:parse)
178
281
  ss
179
282
 
180
- block(node(Sass::Tree::ForNode.new(var, from, to, exclusive)), :directive)
283
+ block(node(Sass::Tree::ForNode.new(var, from, to, exclusive), start_pos), :directive)
181
284
  end
182
285
 
183
- def each_directive
286
+ def each_directive(start_pos)
184
287
  tok!(/\$/)
185
- var = tok! IDENT
288
+ vars = [tok!(IDENT)]
186
289
  ss
290
+ while tok(/,/)
291
+ ss
292
+ tok!(/\$/)
293
+ vars << tok!(IDENT)
294
+ ss
295
+ end
187
296
 
188
297
  tok!(/in/)
189
298
  list = sass_script(:parse)
190
299
  ss
191
300
 
192
- block(node(Sass::Tree::EachNode.new(var, list)), :directive)
301
+ block(node(Sass::Tree::EachNode.new(vars, list), start_pos), :directive)
193
302
  end
194
303
 
195
- def while_directive
304
+ def while_directive(start_pos)
196
305
  expr = sass_script(:parse)
197
306
  ss
198
- block(node(Sass::Tree::WhileNode.new(expr)), :directive)
307
+ block(node(Sass::Tree::WhileNode.new(expr), start_pos), :directive)
199
308
  end
200
309
 
201
- def if_directive
310
+ def if_directive(start_pos)
202
311
  expr = sass_script(:parse)
203
312
  ss
204
- node = block(node(Sass::Tree::IfNode.new(expr)), :directive)
313
+ node = block(node(Sass::Tree::IfNode.new(expr), start_pos), :directive)
205
314
  pos = @scanner.pos
206
315
  line = @line
207
316
  ss
@@ -216,10 +325,11 @@ module Sass
216
325
  end
217
326
 
218
327
  def else_block(node)
328
+ start_pos = source_position
219
329
  return unless tok(/@else/)
220
330
  ss
221
331
  else_node = block(
222
- Sass::Tree::IfNode.new((sass_script(:parse) if tok(/if/))),
332
+ node(Sass::Tree::IfNode.new((sass_script(:parse) if tok(/if/))), start_pos),
223
333
  :directive)
224
334
  node.add_else(else_node)
225
335
  pos = @scanner.pos
@@ -235,107 +345,273 @@ module Sass
235
345
  end
236
346
  end
237
347
 
238
- def else_directive
348
+ def else_directive(start_pos)
239
349
  err("Invalid CSS: @else must come after @if")
240
350
  end
241
351
 
242
- def extend_directive
243
- node(Sass::Tree::ExtendNode.new(expr!(:selector)))
352
+ def extend_directive(start_pos)
353
+ selector, selector_range = expr!(:selector_sequence)
354
+ optional = tok(OPTIONAL)
355
+ ss
356
+ node(Sass::Tree::ExtendNode.new(selector, !!optional, selector_range), start_pos)
244
357
  end
245
358
 
246
- def import_directive
359
+ def import_directive(start_pos)
247
360
  values = []
248
361
 
249
362
  loop do
250
363
  values << expr!(:import_arg)
251
- break if use_css_import? || !tok(/,\s*/)
364
+ break if use_css_import?
365
+ break unless tok(/,/)
366
+ ss
252
367
  end
253
368
 
254
- return values
369
+ values
255
370
  end
256
371
 
257
372
  def import_arg
258
- return unless arg = tok(STRING) || (uri = tok!(URI))
259
- path = @scanner[1] || @scanner[2] || @scanner[3]
260
- ss
373
+ start_pos = source_position
374
+ return unless (str = tok(STRING)) || (uri = tok?(/url\(/i))
375
+ if uri
376
+ str = sass_script(:parse_string)
377
+ ss
378
+ media = media_query_list
379
+ ss
380
+ return node(Tree::CssImportNode.new(str, media.to_a), start_pos)
381
+ end
261
382
 
262
- media = str {media_query_list}.strip
383
+ path = @scanner[1] || @scanner[2]
384
+ ss
263
385
 
264
- if uri || path =~ /^http:\/\// || !media.strip.empty? || use_css_import?
265
- return node(Sass::Tree::DirectiveNode.new("@import #{arg} #{media}".strip))
386
+ media = media_query_list
387
+ if path =~ %r{^(https?:)?//} || media || use_css_import?
388
+ return node(Sass::Tree::CssImportNode.new(str, media.to_a), start_pos)
266
389
  end
267
390
 
268
- node(Sass::Tree::ImportNode.new(path.strip))
391
+ node(Sass::Tree::ImportNode.new(path.strip), start_pos)
269
392
  end
270
393
 
271
394
  def use_css_import?; false; end
272
395
 
273
- def media_directive
274
- val = str {media_query_list}.strip
275
- block(node(Sass::Tree::MediaNode.new(val)), :directive)
396
+ def media_directive(start_pos)
397
+ block(node(Sass::Tree::MediaNode.new(expr!(:media_query_list).to_a), start_pos), :directive)
276
398
  end
277
399
 
278
400
  # http://www.w3.org/TR/css3-mediaqueries/#syntax
279
401
  def media_query_list
280
- return unless media_query
402
+ query = media_query
403
+ return unless query
404
+ queries = [query]
281
405
 
282
406
  ss
283
407
  while tok(/,/)
284
- ss; expr!(:media_query); ss
408
+ ss; queries << expr!(:media_query)
285
409
  end
410
+ ss
286
411
 
287
- true
412
+ Sass::Media::QueryList.new(queries)
288
413
  end
289
414
 
290
415
  def media_query
291
- if tok(/only|not/i)
416
+ if (ident1 = interp_ident)
292
417
  ss
293
- @expected = "media type (e.g. print, screen)"
294
- tok!(IDENT)
418
+ ident2 = interp_ident
295
419
  ss
296
- elsif !tok(IDENT) && !media_expr
297
- return
420
+ if ident2 && ident2.length == 1 && ident2[0].is_a?(String) && ident2[0].downcase == 'and'
421
+ query = Sass::Media::Query.new([], ident1, [])
422
+ else
423
+ if ident2
424
+ query = Sass::Media::Query.new(ident1, ident2, [])
425
+ else
426
+ query = Sass::Media::Query.new([], ident1, [])
427
+ end
428
+ return query unless tok(/and/i)
429
+ ss
430
+ end
298
431
  end
299
432
 
433
+ if query
434
+ expr = expr!(:media_expr)
435
+ else
436
+ expr = media_expr
437
+ return unless expr
438
+ end
439
+ query ||= Sass::Media::Query.new([], [], [])
440
+ query.expressions << expr
441
+
300
442
  ss
301
443
  while tok(/and/i)
302
- ss; expr!(:media_expr); ss
444
+ ss; query.expressions << expr!(:media_expr)
303
445
  end
304
446
 
305
- true
447
+ query
306
448
  end
307
449
 
308
- def media_expr
450
+ def query_expr
451
+ interp = interpolation
452
+ return interp if interp
309
453
  return unless tok(/\(/)
454
+ res = ['(']
310
455
  ss
311
- @expected = "media feature (e.g. min-device-width, color)"
312
- tok!(IDENT)
313
- ss
456
+ res << sass_script(:parse)
314
457
 
315
458
  if tok(/:/)
316
- ss; expr!(:expr)
459
+ res << ': '
460
+ ss
461
+ res << sass_script(:parse)
317
462
  end
318
- tok!(/\)/)
463
+ res << tok!(/\)/)
319
464
  ss
320
-
321
- true
465
+ res
322
466
  end
323
467
 
324
- def charset_directive
468
+ # Aliases allow us to use different descriptions if the same
469
+ # expression fails in different contexts.
470
+ alias_method :media_expr, :query_expr
471
+ alias_method :at_root_query, :query_expr
472
+
473
+ def charset_directive(start_pos)
325
474
  tok! STRING
326
475
  name = @scanner[1] || @scanner[2]
327
476
  ss
328
- node(Sass::Tree::CharsetNode.new(name))
477
+ node(Sass::Tree::CharsetNode.new(name), start_pos)
478
+ end
479
+
480
+ # The document directive is specified in
481
+ # http://www.w3.org/TR/css3-conditional/, but Gecko allows the
482
+ # `url-prefix` and `domain` functions to omit quotation marks, contrary to
483
+ # the standard.
484
+ #
485
+ # We could parse all document directives according to Mozilla's syntax,
486
+ # but if someone's using e.g. @-webkit-document we don't want them to
487
+ # think WebKit works sans quotes.
488
+ def _moz_document_directive(start_pos)
489
+ res = ["@-moz-document "]
490
+ loop do
491
+ res << str {ss} << expr!(:moz_document_function)
492
+ if (c = tok(/,/))
493
+ res << c
494
+ else
495
+ break
496
+ end
497
+ end
498
+ directive_body(res.flatten, start_pos)
499
+ end
500
+
501
+ def moz_document_function
502
+ val = interp_uri || _interp_string(:url_prefix) ||
503
+ _interp_string(:domain) || function(!:allow_var) || interpolation
504
+ return unless val
505
+ ss
506
+ val
507
+ end
508
+
509
+ def at_root_directive(start_pos)
510
+ if tok?(/\(/) && (expr = at_root_query)
511
+ return block(node(Sass::Tree::AtRootNode.new(expr), start_pos), :directive)
512
+ end
513
+
514
+ at_root_node = node(Sass::Tree::AtRootNode.new, start_pos)
515
+ rule_node = ruleset
516
+ return block(at_root_node, :stylesheet) unless rule_node
517
+ at_root_node << rule_node
518
+ at_root_node
519
+ end
520
+
521
+ def at_root_directive_list
522
+ return unless (first = tok(IDENT))
523
+ arr = [first]
524
+ ss
525
+ while (e = tok(IDENT))
526
+ arr << e
527
+ ss
528
+ end
529
+ arr
530
+ end
531
+
532
+ # http://www.w3.org/TR/css3-conditional/
533
+ def supports_directive(name, start_pos)
534
+ condition = expr!(:supports_condition)
535
+ node = Sass::Tree::SupportsNode.new(name, condition)
536
+
537
+ tok!(/\{/)
538
+ node.has_children = true
539
+ block_contents(node, :directive)
540
+ tok!(/\}/)
541
+
542
+ node(node, start_pos)
543
+ end
544
+
545
+ def supports_condition
546
+ supports_negation || supports_operator || supports_interpolation
547
+ end
548
+
549
+ def supports_negation
550
+ return unless tok(/not/i)
551
+ ss
552
+ Sass::Supports::Negation.new(expr!(:supports_condition_in_parens))
553
+ end
554
+
555
+ def supports_operator
556
+ cond = supports_condition_in_parens
557
+ return unless cond
558
+ while (op = tok(/and|or/i))
559
+ ss
560
+ cond = Sass::Supports::Operator.new(
561
+ cond, expr!(:supports_condition_in_parens), op)
562
+ end
563
+ cond
564
+ end
565
+
566
+ def supports_condition_in_parens
567
+ interp = supports_interpolation
568
+ return interp if interp
569
+ return unless tok(/\(/); ss
570
+ if (cond = supports_condition)
571
+ tok!(/\)/); ss
572
+ cond
573
+ else
574
+ name = sass_script(:parse)
575
+ tok!(/:/); ss
576
+ value = sass_script(:parse)
577
+ tok!(/\)/); ss
578
+ Sass::Supports::Declaration.new(name, value)
579
+ end
580
+ end
581
+
582
+ def supports_declaration_condition
583
+ return unless tok(/\(/); ss
584
+ supports_declaration_body
585
+ end
586
+
587
+ def supports_interpolation
588
+ interp = interpolation
589
+ return unless interp
590
+ ss
591
+ Sass::Supports::Interpolation.new(interp)
329
592
  end
330
593
 
331
594
  def variable
332
595
  return unless tok(/\$/)
596
+ start_pos = source_position
333
597
  name = tok!(IDENT)
334
598
  ss; tok!(/:/); ss
335
599
 
336
600
  expr = sass_script(:parse)
337
- guarded = tok(DEFAULT)
338
- node(Sass::Tree::VariableNode.new(name, expr, guarded))
601
+ while tok(/!/)
602
+ flag_name = tok!(IDENT)
603
+ if flag_name == 'default'
604
+ guarded ||= true
605
+ elsif flag_name == 'global'
606
+ global ||= true
607
+ else
608
+ raise Sass::SyntaxError.new("Invalid flag \"!#{flag_name}\".", :line => @line)
609
+ end
610
+ ss
611
+ end
612
+
613
+ result = Sass::Tree::VariableNode.new(name, expr, guarded, global)
614
+ node(result, start_pos)
339
615
  end
340
616
 
341
617
  def operator
@@ -346,13 +622,12 @@ module Sass
346
622
  str {ss if tok(/[\/,:.=]/)}
347
623
  end
348
624
 
349
- def unary_operator
350
- tok(/[+-]/)
351
- end
352
-
353
625
  def ruleset
354
- return unless rules = selector_sequence
355
- block(node(Sass::Tree::RuleNode.new(rules.flatten.compact)), :ruleset)
626
+ start_pos = source_position
627
+ rules, source_range = selector_sequence
628
+ return unless rules
629
+ block(node(
630
+ Sass::Tree::RuleNode.new(rules.flatten.compact, source_range), start_pos), :ruleset)
356
631
  end
357
632
 
358
633
  def block(node, context)
@@ -383,11 +658,11 @@ module Sass
383
658
  def has_children?(child_or_array)
384
659
  return false unless child_or_array
385
660
  return child_or_array.last.has_children if child_or_array.is_a?(Array)
386
- return child_or_array.has_children
661
+ child_or_array.has_children
387
662
  end
388
663
 
389
664
  # This is a nasty hack, and the only place in the parser
390
- # that requires backtracking.
665
+ # that requires a large amount of backtracking.
391
666
  # The reason is that we can't figure out if certain strings
392
667
  # are declarations or rulesets with fixed finite lookahead.
393
668
  # For example, "foo:bar baz baz baz..." could be either a property
@@ -421,40 +696,46 @@ module Sass
421
696
  end
422
697
 
423
698
  def selector_sequence
424
- if sel = tok(STATIC_SELECTOR)
425
- return [sel]
699
+ start_pos = source_position
700
+ if (sel = tok(STATIC_SELECTOR, true))
701
+ return [sel], range(start_pos)
426
702
  end
427
703
 
428
704
  rules = []
429
- return unless v = selector
705
+ v = selector
706
+ return unless v
430
707
  rules.concat v
431
708
 
432
709
  ws = ''
433
710
  while tok(/,/)
434
711
  ws << str {ss}
435
- if v = selector
712
+ if (v = selector)
436
713
  rules << ',' << ws
437
714
  rules.concat v
438
715
  ws = ''
439
716
  end
440
717
  end
441
- rules
718
+ return rules, range(start_pos)
442
719
  end
443
720
 
444
721
  def selector
445
- return unless sel = _selector
722
+ sel = _selector
723
+ return unless sel
446
724
  sel.to_a
447
725
  end
448
726
 
449
727
  def selector_comma_sequence
450
- return unless sel = _selector
728
+ sel = _selector
729
+ return unless sel
451
730
  selectors = [sel]
452
731
  ws = ''
453
732
  while tok(/,/)
454
- ws << str{ss}
455
- if sel = _selector
733
+ ws << str {ss}
734
+ if (sel = _selector)
456
735
  selectors << sel
457
- selectors[-1] = Selector::Sequence.new(["\n"] + selectors.last.members) if ws.include?("\n")
736
+ if ws.include?("\n")
737
+ selectors[-1] = Selector::Sequence.new(["\n"] + selectors.last.members)
738
+ end
458
739
  ws = ''
459
740
  end
460
741
  end
@@ -463,54 +744,75 @@ module Sass
463
744
 
464
745
  def _selector
465
746
  # The combinator here allows the "> E" hack
466
- return unless val = combinator || simple_selector_sequence
467
- nl = str{ss}.include?("\n")
747
+ val = combinator || simple_selector_sequence
748
+ return unless val
749
+ nl = str {ss}.include?("\n")
468
750
  res = []
469
751
  res << val
470
752
  res << "\n" if nl
471
753
 
472
- while val = combinator || simple_selector_sequence
754
+ while (val = combinator || simple_selector_sequence)
473
755
  res << val
474
- res << "\n" if str{ss}.include?("\n")
756
+ res << "\n" if str {ss}.include?("\n")
475
757
  end
476
758
  Selector::Sequence.new(res.compact)
477
759
  end
478
760
 
479
761
  def combinator
480
- tok(PLUS) || tok(GREATER) || tok(TILDE)
762
+ tok(PLUS) || tok(GREATER) || tok(TILDE) || reference_combinator
763
+ end
764
+
765
+ def reference_combinator
766
+ return unless tok(/\//)
767
+ res = ['/']
768
+ ns, name = expr!(:qualified_name)
769
+ res << ns << '|' if ns
770
+ res << name << tok!(/\//)
771
+ res = res.flatten
772
+ res = res.join '' if res.all? {|e| e.is_a?(String)}
773
+ res
481
774
  end
482
775
 
483
776
  def simple_selector_sequence
484
- # This allows for stuff like http://www.w3.org/TR/css3-animations/#keyframes-
485
- return expr unless e = element_name || id_selector || class_selector ||
486
- attrib || negation || pseudo || parent_selector || interpolation_selector
777
+ # Returning expr by default allows for stuff like
778
+ # http://www.w3.org/TR/css3-animations/#keyframes-
779
+
780
+ start_pos = source_position
781
+ e = element_name || id_selector ||
782
+ class_selector || placeholder_selector || attrib || pseudo ||
783
+ parent_selector || interpolation_selector
784
+ return expr(!:allow_var) unless e
487
785
  res = [e]
488
786
 
489
787
  # The tok(/\*/) allows the "E*" hack
490
- while v = element_name || id_selector || class_selector ||
491
- attrib || negation || pseudo || interpolation_selector ||
492
- (tok(/\*/) && Selector::Universal.new(nil))
788
+ while (v = id_selector || class_selector || placeholder_selector ||
789
+ attrib || pseudo || interpolation_selector ||
790
+ (tok(/\*/) && Selector::Universal.new(nil)))
493
791
  res << v
494
792
  end
495
793
 
496
- if tok?(/&/)
794
+ pos = @scanner.pos
795
+ line = @line
796
+ if (sel = str? {simple_selector_sequence})
797
+ @scanner.pos = pos
798
+ @line = line
497
799
  begin
498
- expected('"{"')
800
+ # If we see "*E", don't force a throw because this could be the
801
+ # "*prop: val" hack.
802
+ expected('"{"') if res.length == 1 && res[0].is_a?(Selector::Universal)
803
+ throw_error {expected('"{"')}
499
804
  rescue Sass::SyntaxError => e
500
- e.message << "\n\n" << <<MESSAGE
501
- In Sass 3, the parent selector & can only be used where element names are valid,
502
- since it could potentially be replaced by an element name.
503
- MESSAGE
805
+ e.message << "\n\n\"#{sel}\" may only be used at the beginning of a compound selector."
504
806
  raise e
505
807
  end
506
808
  end
507
809
 
508
- Selector::SimpleSequence.new(res)
810
+ Selector::SimpleSequence.new(res, tok(/!/), range(start_pos))
509
811
  end
510
812
 
511
813
  def parent_selector
512
814
  return unless tok(/&/)
513
- Selector::Parent.new
815
+ Selector::Parent.new(interp_ident(NAME) || [])
514
816
  end
515
817
 
516
818
  def class_selector
@@ -525,13 +827,15 @@ MESSAGE
525
827
  Selector::Id.new(merge(expr!(:interp_name)))
526
828
  end
527
829
 
830
+ def placeholder_selector
831
+ return unless tok(/%/)
832
+ @expected = "placeholder name"
833
+ Selector::Placeholder.new(merge(expr!(:interp_ident)))
834
+ end
835
+
528
836
  def element_name
529
- return unless name = interp_ident || tok(/\*/) || (tok?(/\|/) && "")
530
- if tok(/\|/)
531
- @expected = "element name or *"
532
- ns = name
533
- name = interp_ident || tok!(/\*/)
534
- end
837
+ ns, name = Sass::Util.destructure(qualified_name(:allow_star_name))
838
+ return unless ns || name
535
839
 
536
840
  if name == '*'
537
841
  Selector::Universal.new(merge(ns))
@@ -540,9 +844,20 @@ MESSAGE
540
844
  end
541
845
  end
542
846
 
847
+ def qualified_name(allow_star_name = false)
848
+ name = interp_ident || tok(/\*/) || (tok?(/\|/) && "")
849
+ return unless name
850
+ return nil, name unless tok(/\|/)
851
+
852
+ return name, expr!(:interp_ident) unless allow_star_name
853
+ @expected = "identifier or *"
854
+ return name, interp_ident || tok!(/\*/)
855
+ end
856
+
543
857
  def interpolation_selector
544
- return unless script = interpolation
545
- Selector::Interpolation.new(script)
858
+ if (script = interpolation)
859
+ Selector::Interpolation.new(script)
860
+ end
546
861
  end
547
862
 
548
863
  def attrib
@@ -551,28 +866,26 @@ MESSAGE
551
866
  ns, name = attrib_name!
552
867
  ss
553
868
 
554
- if op = tok(/=/) ||
555
- tok(INCLUDES) ||
556
- tok(DASHMATCH) ||
557
- tok(PREFIXMATCH) ||
558
- tok(SUFFIXMATCH) ||
559
- tok(SUBSTRINGMATCH)
869
+ op = tok(/=/) ||
870
+ tok(INCLUDES) ||
871
+ tok(DASHMATCH) ||
872
+ tok(PREFIXMATCH) ||
873
+ tok(SUFFIXMATCH) ||
874
+ tok(SUBSTRINGMATCH)
875
+ if op
560
876
  @expected = "identifier or string"
561
877
  ss
562
- if val = tok(IDENT)
563
- val = [val]
564
- else
565
- val = expr!(:interp_string)
566
- end
878
+ val = interp_ident || expr!(:interp_string)
567
879
  ss
568
880
  end
569
- tok(/\]/)
881
+ flags = interp_ident || interp_string
882
+ tok!(/\]/)
570
883
 
571
- Selector::Attribute.new(merge(name), merge(ns), op, merge(val))
884
+ Selector::Attribute.new(merge(name), merge(ns), op, merge(val), merge(flags))
572
885
  end
573
886
 
574
887
  def attrib_name!
575
- if name_or_ns = interp_ident
888
+ if (name_or_ns = interp_ident)
576
889
  # E, E|E
577
890
  if tok(/\|(?!=)/)
578
891
  ns = name_or_ns
@@ -590,59 +903,89 @@ MESSAGE
590
903
  end
591
904
 
592
905
  def pseudo
593
- return unless s = tok(/::?/)
906
+ s = tok(/::?/)
907
+ return unless s
594
908
  @expected = "pseudoclass or pseudoelement"
595
909
  name = expr!(:interp_ident)
596
910
  if tok(/\(/)
597
911
  ss
598
- arg = expr!(:pseudo_expr)
912
+ arg = expr!(:pseudo_arg)
913
+ while tok(/,/)
914
+ arg << ',' << str {ss}
915
+ arg.concat expr!(:pseudo_arg)
916
+ end
599
917
  tok!(/\)/)
600
918
  end
601
919
  Selector::Pseudo.new(s == ':' ? :class : :element, merge(name), merge(arg))
602
920
  end
603
921
 
604
- def pseudo_expr
605
- return unless e = tok(PLUS) || tok(/-/) || tok(NUMBER) ||
606
- interp_string || tok(IDENT) || interpolation
607
- res = [e, str{ss}]
608
- while e = tok(PLUS) || tok(/-/) || tok(NUMBER) ||
609
- interp_string || tok(IDENT) || interpolation
610
- res << e << str{ss}
922
+ def pseudo_arg
923
+ # In the CSS spec, every pseudo-class/element either takes a pseudo
924
+ # expression or a selector comma sequence as an argument. However, we
925
+ # don't want to have to know which takes which, so we handle both at
926
+ # once.
927
+ #
928
+ # However, there are some ambiguities between the two. For instance, "n"
929
+ # could start a pseudo expression like "n+1", or it could start a
930
+ # selector like "n|m". In order to handle this, we must regrettably
931
+ # backtrack.
932
+ expr, sel = nil, nil
933
+ pseudo_err = catch_error do
934
+ expr = pseudo_expr
935
+ next if tok?(/[,)]/)
936
+ expr = nil
937
+ expected '")"'
611
938
  end
612
- res
939
+
940
+ return expr if expr
941
+ sel_err = catch_error {sel = selector}
942
+ return sel if sel
943
+ rethrow pseudo_err if pseudo_err
944
+ rethrow sel_err if sel_err
945
+ nil
613
946
  end
614
947
 
615
- def negation
616
- return unless name = tok(NOT) || tok(MOZ_ANY)
617
- ss
618
- @expected = "selector"
619
- sel = selector_comma_sequence
620
- tok!(/\)/)
621
- Selector::SelectorPseudoClass.new(name[1...-1], sel)
948
+ def pseudo_expr_token
949
+ tok(PLUS) || tok(/[-*]/) || tok(NUMBER) || interp_string || tok(IDENT) || interpolation
950
+ end
951
+
952
+ def pseudo_expr
953
+ e = pseudo_expr_token
954
+ return unless e
955
+ res = [e, str {ss}]
956
+ while (e = pseudo_expr_token)
957
+ res << e << str {ss}
958
+ end
959
+ res
622
960
  end
623
961
 
624
962
  def declaration
625
963
  # This allows the "*prop: val", ":prop: val", and ".prop: val" hacks
626
- if s = tok(/[:\*\.]|\#(?!\{)/)
964
+ name_start_pos = source_position
965
+ if (s = tok(/[:\*\.]|\#(?!\{)/))
627
966
  @use_property_exception = s !~ /[\.\#]/
628
- name = [s, str{ss}, *expr!(:interp_ident)]
967
+ name = [s, str {ss}, *expr!(:interp_ident)]
629
968
  else
630
- return unless name = interp_ident
969
+ name = interp_ident
970
+ return unless name
631
971
  name = [name] if name.is_a?(String)
632
972
  end
633
- if comment = tok(COMMENT)
973
+ if (comment = tok(COMMENT))
634
974
  name << comment
635
975
  end
976
+ name_end_pos = source_position
636
977
  ss
637
978
 
638
979
  tok!(/:/)
639
- space, value = value!
640
- ss
641
- important = tok(IMPORTANT)
980
+ value_start_pos, space, value = value!
981
+ value_end_pos = source_position
642
982
  ss
643
983
  require_block = tok?(/\{/)
644
984
 
645
- node = node(Sass::Tree::PropNode.new(name.flatten.compact, value, !!important, :new))
985
+ node = node(Sass::Tree::PropNode.new(name.flatten.compact, value, :new),
986
+ name_start_pos, value_end_pos)
987
+ node.name_source_range = range(name_start_pos, name_end_pos)
988
+ node.value_source_range = range(value_start_pos, value_end_pos)
646
989
 
647
990
  return node unless require_block
648
991
  nested_properties! node, space
@@ -650,29 +993,29 @@ MESSAGE
650
993
 
651
994
  def value!
652
995
  space = !str {ss}.empty?
996
+ value_start_pos = source_position
653
997
  @use_property_exception ||= space || !tok?(IDENT)
654
998
 
655
- return true, Sass::Script::String.new("") if tok?(/\{/)
999
+ if tok?(/\{/)
1000
+ str = Sass::Script::Tree::Literal.new(Sass::Script::Value::String.new(""))
1001
+ str.line = source_position.line
1002
+ str.source_range = range(source_position)
1003
+ return value_start_pos, true, str
1004
+ end
1005
+
1006
+ start_pos = source_position
656
1007
  # This is a bit of a dirty trick:
657
1008
  # if the value is completely static,
658
1009
  # we don't parse it at all, and instead return a plain old string
659
1010
  # containing the value.
660
1011
  # This results in a dramatic speed increase.
661
- if val = tok(STATIC_VALUE)
662
- return space, Sass::Script::String.new(val.strip)
1012
+ if (val = tok(STATIC_VALUE, true))
1013
+ str = Sass::Script::Tree::Literal.new(Sass::Script::Value::String.new(val.strip))
1014
+ str.line = start_pos.line
1015
+ str.source_range = range(start_pos)
1016
+ return value_start_pos, space, str
663
1017
  end
664
- return space, sass_script(:parse)
665
- end
666
-
667
- def plain_value
668
- return unless tok(/:/)
669
- space = !str {ss}.empty?
670
- @use_property_exception ||= space || !tok?(IDENT)
671
-
672
- expression = expr
673
- expression << tok(IMPORTANT) if expression
674
- # expression, space, value
675
- return expression, space, expression || [""]
1018
+ return value_start_pos, space, sass_script(:parse)
676
1019
  end
677
1020
 
678
1021
  def nested_properties!(node, space)
@@ -686,43 +1029,55 @@ MESSAGE
686
1029
  block(node, :property)
687
1030
  end
688
1031
 
689
- def expr
690
- return unless t = term
691
- res = [t, str{ss}]
1032
+ def expr(allow_var = true)
1033
+ t = term(allow_var)
1034
+ return unless t
1035
+ res = [t, str {ss}]
692
1036
 
693
- while (o = operator) && (t = term)
694
- res << o << t << str{ss}
1037
+ while (o = operator) && (t = term(allow_var))
1038
+ res << o << t << str {ss}
695
1039
  end
696
1040
 
697
- res
1041
+ res.flatten
698
1042
  end
699
1043
 
700
- def term
701
- unless e = tok(NUMBER) ||
702
- tok(URI) ||
703
- function ||
704
- tok(STRING) ||
1044
+ def term(allow_var)
1045
+ e = tok(NUMBER) ||
1046
+ interp_uri ||
1047
+ function(allow_var) ||
1048
+ interp_string ||
705
1049
  tok(UNICODERANGE) ||
706
- tok(IDENT) ||
707
- tok(HEXCOLOR)
1050
+ interp_ident ||
1051
+ tok(HEXCOLOR) ||
1052
+ (allow_var && var_expr)
1053
+ return e if e
708
1054
 
709
- return unless op = unary_operator
710
- @expected = "number or function"
711
- return [op, tok(NUMBER) || expr!(:function)]
712
- end
713
- e
1055
+ op = tok(/[+-]/)
1056
+ return unless op
1057
+ @expected = "number or function"
1058
+ [op,
1059
+ tok(NUMBER) || function(allow_var) || (allow_var && var_expr) || expr!(:interpolation)]
714
1060
  end
715
1061
 
716
- def function
717
- return unless name = tok(FUNCTION)
1062
+ def function(allow_var)
1063
+ name = tok(FUNCTION)
1064
+ return unless name
718
1065
  if name == "expression(" || name == "calc("
719
1066
  str, _ = Sass::Shared.balance(@scanner, ?(, ?), 1)
720
1067
  [name, str]
721
1068
  else
722
- [name, str{ss}, expr, tok!(/\)/)]
1069
+ [name, str {ss}, expr(allow_var), tok!(/\)/)]
723
1070
  end
724
1071
  end
725
1072
 
1073
+ def var_expr
1074
+ return unless tok(/\$/)
1075
+ line = @line
1076
+ var = Sass::Script::Tree::Variable.new(tok!(IDENT))
1077
+ var.line = line
1078
+ var
1079
+ end
1080
+
726
1081
  def interpolation
727
1082
  return unless tok(INTERP_START)
728
1083
  sass_script(:parse_interpolated)
@@ -732,11 +1087,16 @@ MESSAGE
732
1087
  _interp_string(:double) || _interp_string(:single)
733
1088
  end
734
1089
 
1090
+ def interp_uri
1091
+ _interp_string(:uri)
1092
+ end
1093
+
735
1094
  def _interp_string(type)
736
- return unless start = tok(Sass::Script::Lexer::STRING_REGULAR_EXPRESSIONS[[type, false]])
1095
+ start = tok(Sass::Script::Lexer::STRING_REGULAR_EXPRESSIONS[type][false])
1096
+ return unless start
737
1097
  res = [start]
738
1098
 
739
- mid_re = Sass::Script::Lexer::STRING_REGULAR_EXPRESSIONS[[type, true]]
1099
+ mid_re = Sass::Script::Lexer::STRING_REGULAR_EXPRESSIONS[type][true]
740
1100
  # @scanner[2].empty? means we've started an interpolated section
741
1101
  while @scanner[2] == '#{'
742
1102
  @scanner.pos -= 2 # Don't consume the #{
@@ -747,14 +1107,22 @@ MESSAGE
747
1107
  end
748
1108
 
749
1109
  def interp_ident(start = IDENT)
750
- return unless val = tok(start) || interpolation
1110
+ val = tok(start) || interpolation || tok(IDENT_HYPHEN_INTERP, true)
1111
+ return unless val
751
1112
  res = [val]
752
- while val = tok(NAME) || interpolation
1113
+ while (val = tok(NAME) || interpolation)
753
1114
  res << val
754
1115
  end
755
1116
  res
756
1117
  end
757
1118
 
1119
+ def interp_ident_or_var
1120
+ id = interp_ident
1121
+ return id if id
1122
+ var = var_expr
1123
+ return [var] if var
1124
+ end
1125
+
758
1126
  def interp_name
759
1127
  interp_ident NAME
760
1128
  end
@@ -768,27 +1136,45 @@ MESSAGE
768
1136
  end
769
1137
 
770
1138
  def str?
1139
+ pos = @scanner.pos
1140
+ line = @line
1141
+ offset = @offset
771
1142
  @strs.push ""
772
- yield && @strs.last
1143
+ throw_error {yield} && @strs.last
1144
+ rescue Sass::SyntaxError
1145
+ @scanner.pos = pos
1146
+ @line = line
1147
+ @offset = offset
1148
+ nil
773
1149
  ensure
774
1150
  @strs.pop
775
1151
  end
776
1152
 
777
- def node(node)
778
- node.line = @line
1153
+ def node(node, start_pos, end_pos = source_position)
1154
+ node.line = start_pos.line
1155
+ node.source_range = range(start_pos, end_pos)
779
1156
  node
780
1157
  end
781
1158
 
782
1159
  @sass_script_parser = Class.new(Sass::Script::Parser)
783
1160
  @sass_script_parser.send(:include, ScriptParser)
784
- # @private
785
- def self.sass_script_parser; @sass_script_parser; end
1161
+
1162
+ class << self
1163
+ # @private
1164
+ attr_accessor :sass_script_parser
1165
+ end
786
1166
 
787
1167
  def sass_script(*args)
788
- parser = self.class.sass_script_parser.new(@scanner, @line,
789
- @scanner.pos - (@scanner.string[0...@scanner.pos].rindex("\n") || 0))
1168
+ parser = self.class.sass_script_parser.new(@scanner, @line, @offset,
1169
+ :filename => @filename, :importer => @importer)
790
1170
  result = parser.send(*args)
1171
+ unless @strs.empty?
1172
+ # Convert to CSS manually so that comments are ignored.
1173
+ src = result.to_sass
1174
+ @strs.each {|s| s << src}
1175
+ end
791
1176
  @line = parser.line
1177
+ @offset = parser.offset
792
1178
  result
793
1179
  rescue Sass::SyntaxError => e
794
1180
  throw(:_sass_parser_error, true) if @throw_error
@@ -801,32 +1187,45 @@ MESSAGE
801
1187
 
802
1188
  EXPR_NAMES = {
803
1189
  :media_query => "media query (e.g. print, screen, print and screen)",
804
- :media_expr => "media expression (e.g. (min-device-width: 800px)))",
805
- :pseudo_expr => "expression (e.g. fr, 2n+1)",
1190
+ :media_query_list => "media query (e.g. print, screen, print and screen)",
1191
+ :media_expr => "media expression (e.g. (min-device-width: 800px))",
1192
+ :at_root_query => "@at-root query (e.g. (without: media))",
1193
+ :at_root_directive_list => '* or identifier',
1194
+ :pseudo_arg => "expression (e.g. fr, 2n+1)",
806
1195
  :interp_ident => "identifier",
807
1196
  :interp_name => "identifier",
1197
+ :qualified_name => "identifier",
808
1198
  :expr => "expression (e.g. 1px, bold)",
809
1199
  :_selector => "selector",
810
1200
  :selector_comma_sequence => "selector",
811
1201
  :simple_selector_sequence => "selector",
812
1202
  :import_arg => "file to import (string or url())",
1203
+ :moz_document_function => "matching function (e.g. url-prefix(), domain())",
1204
+ :supports_condition => "@supports condition (e.g. (display: flexbox))",
1205
+ :supports_condition_in_parens => "@supports condition (e.g. (display: flexbox))",
813
1206
  }
814
1207
 
815
- TOK_NAMES = Sass::Util.to_hash(
816
- Sass::SCSS::RX.constants.map {|c| [Sass::SCSS::RX.const_get(c), c.downcase]}).
817
- merge(IDENT => "identifier", /[;}]/ => '";"')
1208
+ TOK_NAMES = Sass::Util.to_hash(Sass::SCSS::RX.constants.map do |c|
1209
+ [Sass::SCSS::RX.const_get(c), c.downcase]
1210
+ end).merge(
1211
+ IDENT => "identifier",
1212
+ /[;}]/ => '";"',
1213
+ /\b(without|with)\b/ => '"with" or "without"'
1214
+ )
818
1215
 
819
1216
  def tok?(rx)
820
1217
  @scanner.match?(rx)
821
1218
  end
822
1219
 
823
1220
  def expr!(name)
824
- (e = send(name)) && (return e)
1221
+ e = send(name)
1222
+ return e if e
825
1223
  expected(EXPR_NAMES[name] || name.to_s)
826
1224
  end
827
1225
 
828
1226
  def tok!(rx)
829
- (t = tok(rx)) && (return t)
1227
+ t = tok(rx)
1228
+ return t if t
830
1229
  name = TOK_NAMES[rx]
831
1230
 
832
1231
  unless name
@@ -848,14 +1247,23 @@ MESSAGE
848
1247
  raise Sass::SyntaxError.new(msg, :line => @line)
849
1248
  end
850
1249
 
1250
+ def throw_error
1251
+ old_throw_error, @throw_error = @throw_error, false
1252
+ yield
1253
+ ensure
1254
+ @throw_error = old_throw_error
1255
+ end
1256
+
851
1257
  def catch_error(&block)
852
1258
  old_throw_error, @throw_error = @throw_error, true
853
1259
  pos = @scanner.pos
854
1260
  line = @line
1261
+ offset = @offset
855
1262
  expected = @expected
856
- if catch(:_sass_parser_error, &block)
1263
+ if catch(:_sass_parser_error) {yield; false}
857
1264
  @scanner.pos = pos
858
1265
  @line = line
1266
+ @offset = offset
859
1267
  @expected = expected
860
1268
  {:pos => pos, :line => line, :expected => @expected, :block => block}
861
1269
  end
@@ -864,10 +1272,10 @@ MESSAGE
864
1272
  end
865
1273
 
866
1274
  def rethrow(err)
867
- if @throw_err
1275
+ if @throw_error
868
1276
  throw :_sass_parser_error, err
869
1277
  else
870
- @scanner = StringScanner.new(@scanner.string)
1278
+ @scanner = Sass::Util::MultibyteStringScanner.new(@scanner.string)
871
1279
  @scanner.pos = err[:pos]
872
1280
  @line = err[:line]
873
1281
  @expected = err[:expected]
@@ -904,10 +1312,29 @@ MESSAGE
904
1312
  # This is important because `#tok` is called all the time.
905
1313
  NEWLINE = "\n"
906
1314
 
907
- def tok(rx)
1315
+ def tok(rx, last_group_lookahead = false)
908
1316
  res = @scanner.scan(rx)
909
1317
  if res
910
- @line += res.count(NEWLINE)
1318
+ # This fixes https://github.com/nex3/sass/issues/104, which affects
1319
+ # Ruby 1.8.7 and REE. This fix is to replace the ?= zero-width
1320
+ # positive lookahead operator in the Regexp (which matches without
1321
+ # consuming the matched group), with a match that does consume the
1322
+ # group, but then rewinds the scanner and removes the group from the
1323
+ # end of the matched string. This fix makes the assumption that the
1324
+ # matched group will always occur at the end of the match.
1325
+ if last_group_lookahead && @scanner[-1]
1326
+ @scanner.pos -= @scanner[-1].length
1327
+ res.slice!(-@scanner[-1].length..-1)
1328
+ end
1329
+
1330
+ newline_count = res.count(NEWLINE)
1331
+ if newline_count > 0
1332
+ @line += newline_count
1333
+ @offset = res[res.rindex(NEWLINE)..-1].size
1334
+ else
1335
+ @offset += res.size
1336
+ end
1337
+
911
1338
  @expected = nil
912
1339
  if !@strs.empty? && rx != COMMENT && rx != SINGLE_LINE_COMMENT
913
1340
  @strs.each {|s| s << res}