sass 3.3.14 → 3.4.0.rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +1 -1
  3. data/README.md +5 -5
  4. data/VERSION +1 -1
  5. data/VERSION_DATE +1 -1
  6. data/VERSION_NAME +1 -1
  7. data/bin/sass +1 -1
  8. data/bin/scss +1 -1
  9. data/lib/sass.rb +0 -5
  10. data/lib/sass/css.rb +1 -3
  11. data/lib/sass/engine.rb +28 -39
  12. data/lib/sass/environment.rb +13 -17
  13. data/lib/sass/error.rb +6 -9
  14. data/lib/sass/exec.rb +5 -771
  15. data/lib/sass/exec/base.rb +187 -0
  16. data/lib/sass/exec/sass_convert.rb +264 -0
  17. data/lib/sass/exec/sass_scss.rb +419 -0
  18. data/lib/sass/features.rb +6 -0
  19. data/lib/sass/importers.rb +0 -1
  20. data/lib/sass/importers/base.rb +5 -1
  21. data/lib/sass/importers/filesystem.rb +4 -21
  22. data/lib/sass/media.rb +1 -4
  23. data/lib/sass/plugin/compiler.rb +32 -136
  24. data/lib/sass/script/css_lexer.rb +1 -1
  25. data/lib/sass/script/functions.rb +363 -39
  26. data/lib/sass/script/lexer.rb +68 -50
  27. data/lib/sass/script/parser.rb +29 -14
  28. data/lib/sass/script/tree.rb +1 -0
  29. data/lib/sass/script/tree/funcall.rb +1 -1
  30. data/lib/sass/script/tree/interpolation.rb +19 -1
  31. data/lib/sass/script/tree/selector.rb +26 -0
  32. data/lib/sass/script/value.rb +0 -1
  33. data/lib/sass/script/value/bool.rb +0 -5
  34. data/lib/sass/script/value/color.rb +32 -12
  35. data/lib/sass/script/value/helpers.rb +107 -0
  36. data/lib/sass/script/value/list.rb +0 -15
  37. data/lib/sass/script/value/null.rb +0 -5
  38. data/lib/sass/script/value/number.rb +60 -14
  39. data/lib/sass/script/value/string.rb +53 -9
  40. data/lib/sass/scss/css_parser.rb +8 -2
  41. data/lib/sass/scss/parser.rb +175 -319
  42. data/lib/sass/scss/rx.rb +14 -5
  43. data/lib/sass/scss/static_parser.rb +298 -1
  44. data/lib/sass/selector.rb +56 -193
  45. data/lib/sass/selector/abstract_sequence.rb +28 -13
  46. data/lib/sass/selector/comma_sequence.rb +91 -12
  47. data/lib/sass/selector/pseudo.rb +256 -0
  48. data/lib/sass/selector/sequence.rb +99 -31
  49. data/lib/sass/selector/simple.rb +14 -25
  50. data/lib/sass/selector/simple_sequence.rb +101 -37
  51. data/lib/sass/shared.rb +1 -1
  52. data/lib/sass/source/map.rb +23 -9
  53. data/lib/sass/stack.rb +0 -6
  54. data/lib/sass/supports.rb +1 -1
  55. data/lib/sass/tree/at_root_node.rb +1 -0
  56. data/lib/sass/tree/directive_node.rb +7 -1
  57. data/lib/sass/tree/error_node.rb +18 -0
  58. data/lib/sass/tree/keyframe_rule_node.rb +15 -0
  59. data/lib/sass/tree/prop_node.rb +1 -1
  60. data/lib/sass/tree/rule_node.rb +11 -6
  61. data/lib/sass/tree/visitors/check_nesting.rb +3 -4
  62. data/lib/sass/tree/visitors/convert.rb +8 -17
  63. data/lib/sass/tree/visitors/cssize.rb +12 -24
  64. data/lib/sass/tree/visitors/deep_copy.rb +5 -0
  65. data/lib/sass/tree/visitors/perform.rb +43 -28
  66. data/lib/sass/tree/visitors/set_options.rb +5 -0
  67. data/lib/sass/tree/visitors/to_css.rb +14 -13
  68. data/lib/sass/util.rb +94 -90
  69. data/test/sass/cache_test.rb +1 -1
  70. data/test/sass/callbacks_test.rb +1 -1
  71. data/test/sass/compiler_test.rb +5 -14
  72. data/test/sass/conversion_test.rb +47 -1
  73. data/test/sass/css2sass_test.rb +3 -3
  74. data/test/sass/encoding_test.rb +219 -0
  75. data/test/sass/engine_test.rb +128 -191
  76. data/test/sass/exec_test.rb +2 -2
  77. data/test/sass/extend_test.rb +234 -17
  78. data/test/sass/functions_test.rb +268 -213
  79. data/test/sass/importer_test.rb +31 -21
  80. data/test/sass/logger_test.rb +1 -1
  81. data/test/sass/more_results/more_import.css +1 -1
  82. data/test/sass/plugin_test.rb +12 -11
  83. data/test/sass/results/compact.css +1 -1
  84. data/test/sass/results/complex.css +4 -4
  85. data/test/sass/results/expanded.css +1 -1
  86. data/test/sass/results/import.css +1 -1
  87. data/test/sass/results/import_charset_ibm866.css +2 -2
  88. data/test/sass/results/mixins.css +17 -17
  89. data/test/sass/results/nested.css +1 -1
  90. data/test/sass/results/parent_ref.css +2 -2
  91. data/test/sass/results/script.css +3 -3
  92. data/test/sass/results/scss_import.css +1 -1
  93. data/test/sass/script_conversion_test.rb +7 -4
  94. data/test/sass/script_test.rb +202 -79
  95. data/test/sass/scss/css_test.rb +95 -25
  96. data/test/sass/scss/rx_test.rb +4 -4
  97. data/test/sass/scss/scss_test.rb +363 -19
  98. data/test/sass/source_map_test.rb +48 -41
  99. data/test/sass/superselector_test.rb +191 -0
  100. data/test/sass/templates/scss_import.scss +2 -1
  101. data/test/sass/test_helper.rb +1 -1
  102. data/test/sass/util/multibyte_string_scanner_test.rb +1 -1
  103. data/test/sass/util/normalized_map_test.rb +1 -1
  104. data/test/sass/util/subset_map_test.rb +2 -2
  105. data/test/sass/util_test.rb +1 -1
  106. data/test/sass/value_helpers_test.rb +3 -3
  107. data/test/test_helper.rb +2 -2
  108. metadata +30 -7
  109. data/lib/sass/importers/deprecated_path.rb +0 -51
  110. data/lib/sass/script/value/deprecated_false.rb +0 -55
@@ -121,8 +121,115 @@ module Sass::Script::Value
121
121
  end
122
122
  alias_method :identifier, :unquoted_string
123
123
 
124
+ # Parses a user-provided selector.
125
+ #
126
+ # @param value [Sass::Script::Value::String, Sass::Script::Value::List]
127
+ # The selector to parse. This can be either a string, a list of
128
+ # strings, or a list of lists of strings as returned by `&`.
129
+ # @param name [Symbol, nil]
130
+ # If provided, the name of the selector argument. This is used
131
+ # for error reporting.
132
+ # @param allow_parent_ref [Boolean]
133
+ # Whether the parsed selector should allow parent references.
134
+ # @return [Sass::Selector::CommaSequence] The parsed selector.
135
+ # @throw [ArgumentError] if the parse failed for any reason.
136
+ def parse_selector(value, name = nil, allow_parent_ref = false)
137
+ str = normalize_selector(value, name)
138
+ begin
139
+ Sass::SCSS::StaticParser.new(str, nil, nil, 1, 1, allow_parent_ref).parse_selector
140
+ rescue Sass::SyntaxError => e
141
+ err = "#{value.inspect} is not a valid selector: #{e}"
142
+ err = "$#{name.to_s.gsub('_', '-')}: #{err}" if name
143
+ raise ArgumentError.new(err)
144
+ end
145
+ end
146
+
147
+ # Parses a user-provided complex selector.
148
+ #
149
+ # A complex selector can contain combinators but cannot contain commas.
150
+ #
151
+ # @param value [Sass::Script::Value::String, Sass::Script::Value::List]
152
+ # The selector to parse. This can be either a string or a list of
153
+ # strings.
154
+ # @param name [Symbol, nil]
155
+ # If provided, the name of the selector argument. This is used
156
+ # for error reporting.
157
+ # @param allow_parent_ref [Boolean]
158
+ # Whether the parsed selector should allow parent references.
159
+ # @return [Sass::Selector::Sequence] The parsed selector.
160
+ # @throw [ArgumentError] if the parse failed for any reason.
161
+ def parse_complex_selector(value, name = nil, allow_parent_ref = false)
162
+ selector = parse_selector(value, name, allow_parent_ref)
163
+ return seq if selector.members.length == 1
164
+
165
+ err = "#{value.inspect} is not a complex selector"
166
+ err = "$#{name.to_s.gsub('_', '-')}: #{err}" if name
167
+ raise ArgumentError.new(err)
168
+ end
169
+
170
+ # Parses a user-provided compound selector.
171
+ #
172
+ # A compound selector cannot contain combinators or commas.
173
+ #
174
+ # @param value [Sass::Script::Value::String] The selector to parse.
175
+ # @param name [Symbol, nil]
176
+ # If provided, the name of the selector argument. This is used
177
+ # for error reporting.
178
+ # @param allow_parent_ref [Boolean]
179
+ # Whether the parsed selector should allow parent references.
180
+ # @return [Sass::Selector::SimpleSequence] The parsed selector.
181
+ # @throw [ArgumentError] if the parse failed for any reason.
182
+ def parse_compound_selector(value, name = nil, allow_parent_ref = false)
183
+ assert_type value, :String, name
184
+ selector = parse_selector(value, name, allow_parent_ref)
185
+ seq = selector.members.first
186
+ sseq = seq.members.first
187
+ if selector.members.length == 1 && seq.members.length == 1 &&
188
+ sseq.is_a?(Sass::Selector::SimpleSequence)
189
+ return sseq
190
+ end
191
+
192
+ err = "#{value.inspect} is not a compound selector"
193
+ err = "$#{name.to_s.gsub('_', '-')}: #{err}" if name
194
+ raise ArgumentError.new(err)
195
+ end
196
+
124
197
  private
125
198
 
199
+ # Converts a user-provided selector into string form or throws an
200
+ # ArgumentError if it's in an invalid format.
201
+ def normalize_selector(value, name)
202
+ if (str = selector_to_str(value))
203
+ return str
204
+ end
205
+
206
+ err = "#{value.inspect} is not a valid selector: it must be a string,\n" +
207
+ "a list of strings, or a list of lists of strings"
208
+ err = "$#{name.to_s.gsub('_', '-')}: #{err}" if name
209
+ raise ArgumentError.new(err)
210
+ end
211
+
212
+ # Converts a user-provided selector into string form or returns
213
+ # `nil` if it's in an invalid format.
214
+ def selector_to_str(value)
215
+ return value.value if value.is_a?(Sass::Script::String)
216
+ return unless value.is_a?(Sass::Script::List)
217
+
218
+ if value.separator == :comma
219
+ return value.to_a.map do |complex|
220
+ next complex.value if complex.is_a?(Sass::Script::String)
221
+ return unless complex.is_a?(Sass::Script::List) && complex.separator == :space
222
+ return unless (str = selector_to_str(complex))
223
+ str
224
+ end.join(', ')
225
+ end
226
+
227
+ value.to_a.map do |compound|
228
+ return unless compound.is_a?(Sass::Script::String)
229
+ compound.value
230
+ end.join(' ')
231
+ end
232
+
126
233
  # @private
127
234
  VALID_UNIT = /#{Sass::SCSS::RX::NMSTART}#{Sass::SCSS::RX::NMCHAR}|%*/
128
235
 
@@ -65,24 +65,9 @@ module Sass::Script::Value
65
65
  # @see Value#to_h
66
66
  def to_h
67
67
  return Sass::Util.ordered_hash if value.empty?
68
- return @map ||= Sass::Util.to_hash(value.map {|e| e.to_a}) if is_pseudo_map?
69
68
  super
70
69
  end
71
70
 
72
- # Returns whether a warning still needs to be printed for this list being used as a map.
73
- #
74
- # @return [Boolean]
75
- def needs_map_warning?
76
- !@value.empty? && !@map
77
- end
78
-
79
- # Returns whether this is a list of pairs that can be used as a map.
80
- #
81
- # @return [Boolean]
82
- def is_pseudo_map?
83
- @is_pseudo_map ||= value.all? {|e| e.is_a?(Sass::Script::Value::List) && e.to_a.length == 2}
84
- end
85
-
86
71
  # @see Value#inspect
87
72
  def inspect
88
73
  "(#{value.map {|e| e.inspect}.join(sep_str(nil))})"
@@ -25,11 +25,6 @@ module Sass::Script::Value
25
25
  true
26
26
  end
27
27
 
28
- def neq(other)
29
- return other.neq(self) if other.is_a?(DeprecatedFalse)
30
- super
31
- end
32
-
33
28
  # @return [String] '' (An empty string)
34
29
  def to_s(opts = {})
35
30
  ''
@@ -447,25 +447,71 @@ module Sass::Script::Value
447
447
  end
448
448
  end
449
449
 
450
- # A hash of unit names to their index in the conversion table
451
- CONVERTABLE_UNITS = %w(in cm pc mm pt px).inject({}) {|m, v| m[v] = m.size; m}
452
-
453
- # in cm pc mm pt px
454
- CONVERSION_TABLE = [[1, 2.54, 6, 25.4, 72 , 96], # in
455
- [nil, 1, 2.36220473, 10, 28.3464567, 37.795275591], # cm
456
- [nil, nil, 1, 4.23333333, 12 , 16], # pc
457
- [nil, nil, nil, 1, 2.83464567, 3.7795275591], # mm
458
- [nil, nil, nil, nil, 1 , 1.3333333333], # pt
459
- [nil, nil, nil, nil, nil , 1]] # px
450
+ # This is the source data for all the unit logic. It's pre-processed to make
451
+ # it efficient to figure out whether a set of units is mutually compatible
452
+ # and what the conversion ratio is between two units.
453
+ #
454
+ # These come from http://www.w3.org/TR/2012/WD-css3-values-20120308/.
455
+ relative_sizes = [
456
+ {
457
+ 'in' => Rational(1),
458
+ 'cm' => Rational(1, 2.54),
459
+ 'pc' => Rational(1, 6),
460
+ 'mm' => Rational(1, 25.4),
461
+ 'pt' => Rational(1, 72),
462
+ 'px' => Rational(1, 96)
463
+ },
464
+ {
465
+ 'deg' => Rational(1, 360),
466
+ 'grad' => Rational(1, 400),
467
+ 'rad' => Rational(1, 2 * Math::PI),
468
+ 'turn' => Rational(1)
469
+ },
470
+ {
471
+ 's' => Rational(1),
472
+ 'ms' => Rational(1, 1000)
473
+ },
474
+ {
475
+ 'Hz' => Rational(1),
476
+ 'kHz' => Rational(1000)
477
+ },
478
+ {
479
+ 'dpi' => Rational(1),
480
+ 'dpcm' => Rational(1, 2.54),
481
+ 'dppx' => Rational(1, 96)
482
+ }
483
+ ]
484
+
485
+ # A hash from each known unit to the set of units that it's mutually
486
+ # convertible with.
487
+ MUTUALLY_CONVERTIBLE = {}
488
+ relative_sizes.map do |values|
489
+ set = values.keys.to_set
490
+ values.keys.each {|name| MUTUALLY_CONVERTIBLE[name] = set}
491
+ end
492
+
493
+ # A two-dimensional hash from two units to the conversion ratio between
494
+ # them. Multiply `X` by `CONVERSION_TABLE[X][Y]` to convert it to `Y`.
495
+ CONVERSION_TABLE = {}
496
+ relative_sizes.each do |values|
497
+ values.each do |(name1, value1)|
498
+ CONVERSION_TABLE[name1] ||= {}
499
+ values.each do |(name2, value2)|
500
+ value = value1 / value2
501
+ CONVERSION_TABLE[name1][name2] = value.denominator == 1 ? value.to_i : value.to_f
502
+ end
503
+ end
504
+ end
460
505
 
461
506
  def conversion_factor(from_unit, to_unit)
462
- res = CONVERSION_TABLE[CONVERTABLE_UNITS[from_unit]][CONVERTABLE_UNITS[to_unit]]
463
- return 1.0 / conversion_factor(to_unit, from_unit) if res.nil?
464
- res
507
+ CONVERSION_TABLE[from_unit][to_unit]
465
508
  end
466
509
 
467
510
  def convertable?(units)
468
- Array(units).all? {|u| CONVERTABLE_UNITS.include?(u)}
511
+ units = Array(units).to_set
512
+ return true if units.empty?
513
+ return false unless (mutually_convertible = MUTUALLY_CONVERTIBLE[units.first])
514
+ units.subset?(mutually_convertible)
469
515
  end
470
516
 
471
517
  def sans_common_units(units1, units2)
@@ -1,3 +1,4 @@
1
+ # -*- coding: utf-8 -*-
1
2
  module Sass::Script::Value
2
3
  # A SassScript object representing a CSS string *or* a CSS identifier.
3
4
  class String < Base
@@ -13,6 +14,52 @@ module Sass::Script::Value
13
14
  # @return [Symbol] `:string` or `:identifier`
14
15
  attr_reader :type
15
16
 
17
+ def self.value(contents)
18
+ contents.gsub("\\\n", "").gsub(/\\(?:([0-9a-fA-F]{1,6})\s?|(.))/) do
19
+ next $2 if $2
20
+ # Handle unicode escapes as per CSS Syntax Level 3 section 4.3.8.
21
+ code_point = $1.to_i(16)
22
+ if code_point == 0 || code_point > 0x10FFFF ||
23
+ (code_point >= 0xD800 && code_point <= 0xDFFF)
24
+ '�'
25
+ else
26
+ [code_point].pack("U")
27
+ end
28
+ end
29
+ end
30
+
31
+ def self.quote(contents, quote = nil)
32
+ # Short-circuit if there are no characters that need quoting.
33
+ unless contents =~ /[\n\\"']/
34
+ quote ||= '"'
35
+ return "#{quote}#{contents}#{quote}"
36
+ end
37
+
38
+ if quote.nil?
39
+ if contents.include?('"')
40
+ if contents.include?("'")
41
+ quote = '"'
42
+ else
43
+ quote = "'"
44
+ end
45
+ else
46
+ quote = '"'
47
+ end
48
+ end
49
+
50
+ # Replace single backslashes with multiples.
51
+ contents = contents.gsub("\\", "\\\\\\\\")
52
+
53
+ if quote == '"'
54
+ contents = contents.gsub('"', "\\\"")
55
+ else
56
+ contents = contents.gsub("'", "\\'")
57
+ end
58
+
59
+ contents = contents.gsub(/\n(?![a-fA-F0-9\s])/, "\\a").gsub("\n", "\\a ")
60
+ "#{quote}#{contents}#{quote}"
61
+ end
62
+
16
63
  # Creates a new string.
17
64
  #
18
65
  # @param value [String] See \{#value}
@@ -30,20 +77,17 @@ module Sass::Script::Value
30
77
 
31
78
  # @see Value#to_s
32
79
  def to_s(opts = {})
33
- if @type == :identifier
34
- return @value.gsub(/\n\s*/, " ")
35
- end
36
-
37
- return "\"#{value.gsub('"', "\\\"")}\"" if opts[:quote] == %q{"}
38
- return "'#{value.gsub("'", "\\'")}'" if opts[:quote] == %q{'}
39
- return "\"#{value}\"" unless value.include?('"')
40
- return "'#{value}'" unless value.include?("'")
41
- "\"#{value.gsub('"', "\\\"")}\"" # '
80
+ return @value.gsub(/\n\s*/, ' ') if @type == :identifier
81
+ Sass::Script::Value::String.quote(value, opts[:quote])
42
82
  end
43
83
 
44
84
  # @see Value#to_sass
45
85
  def to_sass(opts = {})
46
86
  to_s
47
87
  end
88
+
89
+ def inspect
90
+ String.quote(value)
91
+ end
48
92
  end
49
93
  end
@@ -11,7 +11,7 @@ module Sass
11
11
 
12
12
  def placeholder_selector; nil; end
13
13
  def parent_selector; nil; end
14
- def interpolation; nil; end
14
+ def interpolation(warn_for_color = false); nil; end
15
15
  def use_css_import?; true; end
16
16
 
17
17
  def block_child(context)
@@ -25,10 +25,16 @@ module Sass
25
25
  end
26
26
  end
27
27
 
28
- def nested_properties!(node, space)
28
+ def nested_properties!(node)
29
29
  expected('expression (e.g. 1px, bold)')
30
30
  end
31
31
 
32
+ def ruleset
33
+ start_pos = source_position
34
+ return unless (selector = selector_comma_sequence)
35
+ block(node(Sass::Tree::RuleNode.new(selector, range(start_pos)), start_pos), :ruleset)
36
+ end
37
+
32
38
  @sass_script_parser = Class.new(Sass::Script::CssParser)
33
39
  @sass_script_parser.send(:include, ScriptParser)
34
40
  end
@@ -1,3 +1,4 @@
1
+ # -*- coding: utf-8 -*-
1
2
  require 'set'
2
3
 
3
4
  module Sass
@@ -180,7 +181,7 @@ module Sass
180
181
 
181
182
  DIRECTIVES = Set[:mixin, :include, :function, :return, :debug, :warn, :for,
182
183
  :each, :while, :if, :else, :extend, :import, :media, :charset, :content,
183
- :_moz_document, :at_root]
184
+ :_moz_document, :at_root, :error]
184
185
 
185
186
  PREFIXED_DIRECTIVES = Set[:supports]
186
187
 
@@ -196,10 +197,7 @@ module Sass
196
197
  return dir
197
198
  end
198
199
 
199
- # Most at-rules take expressions (e.g. @import),
200
- # but some (e.g. @page) take selector-like arguments.
201
- # Some take no arguments at all.
202
- val = expr || selector
200
+ val = almost_any_value
203
201
  val = val ? ["@#{name} "] + Sass::Util.strip_string_array(val) : ["@#{name}"]
204
202
  directive_body(val, start_pos)
205
203
  end
@@ -222,7 +220,7 @@ module Sass
222
220
  end
223
221
 
224
222
  def prefixed_directive(name, start_pos)
225
- sym = name.gsub(/^-[a-z0-9]+-/i, '').gsub('-', '_').to_sym
223
+ sym = deprefix(name).gsub('-', '_').to_sym
226
224
  PREFIXED_DIRECTIVES.include?(sym) && send("#{sym}_directive", name, start_pos)
227
225
  end
228
226
 
@@ -355,10 +353,12 @@ module Sass
355
353
  end
356
354
 
357
355
  def extend_directive(start_pos)
358
- selector, selector_range = expr!(:selector_sequence)
356
+ selector_start_pos = source_position
357
+ @expected = "selector"
358
+ selector = Sass::Util.strip_string_array(expr!(:almost_any_value))
359
359
  optional = tok(OPTIONAL)
360
360
  ss
361
- node(Sass::Tree::ExtendNode.new(selector, !!optional, selector_range), start_pos)
361
+ node(Sass::Tree::ExtendNode.new(selector, !!optional, range(selector_start_pos)), start_pos)
362
362
  end
363
363
 
364
364
  def import_directive(start_pos)
@@ -376,7 +376,7 @@ module Sass
376
376
 
377
377
  def import_arg
378
378
  start_pos = source_position
379
- return unless (str = tok(STRING)) || (uri = tok?(/url\(/i))
379
+ return unless (str = string) || (uri = tok?(/url\(/i))
380
380
  if uri
381
381
  str = sass_script(:parse_string)
382
382
  ss
@@ -384,16 +384,15 @@ module Sass
384
384
  ss
385
385
  return node(Tree::CssImportNode.new(str, media.to_a), start_pos)
386
386
  end
387
-
388
- path = @scanner[1] || @scanner[2]
389
387
  ss
390
388
 
391
389
  media = media_query_list
392
- if path =~ %r{^(https?:)?//} || media || use_css_import?
393
- return node(Sass::Tree::CssImportNode.new(str, media.to_a), start_pos)
390
+ if str =~ %r{^(https?:)?//} || media || use_css_import?
391
+ return node(Sass::Tree::CssImportNode.new(
392
+ Sass::Script::Value::String.quote(str), media.to_a), start_pos)
394
393
  end
395
394
 
396
- node(Sass::Tree::ImportNode.new(path.strip), start_pos)
395
+ node(Sass::Tree::ImportNode.new(str.strip), start_pos)
397
396
  end
398
397
 
399
398
  def use_css_import?; false; end
@@ -476,8 +475,7 @@ module Sass
476
475
  alias_method :at_root_query, :query_expr
477
476
 
478
477
  def charset_directive(start_pos)
479
- tok! STRING
480
- name = @scanner[1] || @scanner[2]
478
+ name = expr!(:string)
481
479
  ss
482
480
  node(Sass::Tree::CharsetNode.new(name), start_pos)
483
481
  end
@@ -534,6 +532,10 @@ module Sass
534
532
  arr
535
533
  end
536
534
 
535
+ def error_directive(start_pos)
536
+ node(Sass::Tree::ErrorNode.new(sass_script(:parse)), start_pos)
537
+ end
538
+
537
539
  # http://www.w3.org/TR/css3-conditional/
538
540
  def supports_directive(name, start_pos)
539
541
  condition = expr!(:supports_condition)
@@ -629,10 +631,9 @@ module Sass
629
631
 
630
632
  def ruleset
631
633
  start_pos = source_position
632
- rules, source_range = selector_sequence
633
- return unless rules
634
+ return unless (rules = almost_any_value)
634
635
  block(node(
635
- Sass::Tree::RuleNode.new(rules.flatten.compact, source_range), start_pos), :ruleset)
636
+ Sass::Tree::RuleNode.new(rules, range(start_pos)), start_pos), :ruleset)
636
637
  end
637
638
 
638
639
  def block(node, context)
@@ -666,315 +667,171 @@ module Sass
666
667
  child_or_array.has_children
667
668
  end
668
669
 
669
- # This is a nasty hack, and the only place in the parser
670
- # that requires a large amount of backtracking.
671
- # The reason is that we can't figure out if certain strings
672
- # are declarations or rulesets with fixed finite lookahead.
673
- # For example, "foo:bar baz baz baz..." could be either a property
674
- # or a selector.
670
+ # When parsing the contents of a ruleset, it can be difficult to tell
671
+ # declarations apart from nested rulesets. Since we don't thoroughly parse
672
+ # selectors until after resolving interpolation, we can share a bunch of
673
+ # the parsing of the two, but we need to disambiguate them first. We use
674
+ # the following criteria:
675
+ #
676
+ # * If the entity doesn't start with an identifier followed by a colon,
677
+ # it's a selector. There are some additional mostly-unimportant cases
678
+ # here to support various declaration hacks.
675
679
  #
676
- # To handle this, we simply check if it works as a property
677
- # (which is the most common case)
678
- # and, if it doesn't, try it as a ruleset.
680
+ # * If the colon is followed by another colon, it's a selector.
679
681
  #
680
- # We could eke some more efficiency out of this
681
- # by handling some easy cases (first token isn't an identifier,
682
- # no colon after the identifier, whitespace after the colon),
683
- # but I'm not sure the gains would be worth the added complexity.
682
+ # * Otherwise, if the colon is followed by anything other than
683
+ # interpolation or a character that's valid as the beginning of an
684
+ # identifier, it's a declaration.
685
+ #
686
+ # * If the colon is followed by interpolation or a valid identifier, try
687
+ # parsing it as a declaration value. If this fails, backtrack and parse
688
+ # it as a selector.
689
+ #
690
+ # * If the declaration value value valid but is followed by "{", backtrack
691
+ # and parse it as a selector anyway. This ensures that ".foo:bar {" is
692
+ # always parsed as a selector and never as a property with nested
693
+ # properties beneath it.
684
694
  def declaration_or_ruleset
685
- old_use_property_exception, @use_property_exception =
686
- @use_property_exception, false
687
- decl_err = catch_error do
688
- decl = declaration
689
- unless decl && decl.has_children
690
- # We want an exception if it's not there,
691
- # but we don't want to consume if it is
692
- tok!(/[;}]/) unless tok?(/[;}]/)
693
- end
694
- return decl
695
- end
696
-
697
- ruleset_err = catch_error {return ruleset}
698
- rethrow(@use_property_exception ? decl_err : ruleset_err)
699
- ensure
700
- @use_property_exception = old_use_property_exception
701
- end
702
-
703
- def selector_sequence
704
695
  start_pos = source_position
705
- if (sel = tok(STATIC_SELECTOR, true))
706
- return [sel], range(start_pos)
707
- end
708
-
709
- rules = []
710
- v = selector
711
- return unless v
712
- rules.concat v
696
+ declaration = try_declaration
713
697
 
714
- ws = ''
715
- while tok(/,/)
716
- ws << str {ss}
717
- if (v = selector)
718
- rules << ',' << ws
719
- rules.concat v
720
- ws = ''
721
- end
698
+ if declaration.nil?
699
+ return unless (selector = almost_any_value)
700
+ elsif declaration.is_a?(Array)
701
+ selector = declaration
702
+ else
703
+ # Declaration should be a PropNode.
704
+ return declaration
722
705
  end
723
- return rules, range(start_pos)
724
- end
725
706
 
726
- def selector
727
- sel = _selector
728
- return unless sel
729
- sel.to_a
730
- end
731
-
732
- def selector_comma_sequence
733
- sel = _selector
734
- return unless sel
735
- selectors = [sel]
736
- ws = ''
737
- while tok(/,/)
738
- ws << str {ss}
739
- if (sel = _selector)
740
- selectors << sel
741
- if ws.include?("\n")
742
- selectors[-1] = Selector::Sequence.new(["\n"] + selectors.last.members)
743
- end
744
- ws = ''
745
- end
707
+ if (additional_selector = almost_any_value)
708
+ selector << additional_selector
746
709
  end
747
- Selector::CommaSequence.new(selectors)
748
- end
749
710
 
750
- def _selector
751
- # The combinator here allows the "> E" hack
752
- val = combinator || simple_selector_sequence
753
- return unless val
754
- nl = str {ss}.include?("\n")
755
- res = []
756
- res << val
757
- res << "\n" if nl
758
-
759
- while (val = combinator || simple_selector_sequence)
760
- res << val
761
- res << "\n" if str {ss}.include?("\n")
762
- end
763
- Selector::Sequence.new(res.compact)
764
- end
765
-
766
- def combinator
767
- tok(PLUS) || tok(GREATER) || tok(TILDE) || reference_combinator
768
- end
769
-
770
- def reference_combinator
771
- return unless tok(/\//)
772
- res = ['/']
773
- ns, name = expr!(:qualified_name)
774
- res << ns << '|' if ns
775
- res << name << tok!(/\//)
776
- res = res.flatten
777
- res = res.join '' if res.all? {|e| e.is_a?(String)}
778
- res
711
+ block(node(
712
+ Sass::Tree::RuleNode.new(merge(selector), range(start_pos)), start_pos), :ruleset)
779
713
  end
780
714
 
781
- def simple_selector_sequence
782
- # Returning expr by default allows for stuff like
783
- # http://www.w3.org/TR/css3-animations/#keyframes-
784
-
785
- start_pos = source_position
786
- e = element_name || id_selector ||
787
- class_selector || placeholder_selector || attrib || pseudo ||
788
- parent_selector || interpolation_selector
789
- return expr(!:allow_var) unless e
790
- res = [e]
791
-
792
- # The tok(/\*/) allows the "E*" hack
793
- while (v = id_selector || class_selector || placeholder_selector ||
794
- attrib || pseudo || interpolation_selector ||
795
- (tok(/\*/) && Selector::Universal.new(nil)))
796
- res << v
715
+ # Tries to parse a declaration, and returns the value parsed so far if it
716
+ # fails.
717
+ #
718
+ # This has three possible return types. It can return `nil`, indicating
719
+ # that parsing failed completely and the scanner hasn't moved forward at
720
+ # all. It can return an Array, indicating that parsing failed after
721
+ # consuming some text (possibly containing interpolation), which is
722
+ # returned. Or it can return a PropNode, indicating that parsing
723
+ # succeeded.
724
+ def try_declaration
725
+ # This allows the "*prop: val", ":prop: val", "#prop: val", and ".prop:
726
+ # val" hacks.
727
+ name_start_pos = source_position
728
+ if (s = tok(/[:\*\.]|\#(?!\{)/))
729
+ name = [s, str {ss}]
730
+ return name unless (ident = interp_ident)
731
+ name << ident
732
+ else
733
+ return unless (name = interp_ident)
734
+ name = Array(name)
797
735
  end
798
736
 
799
- pos = @scanner.pos
800
- line = @line
801
- if (sel = str? {simple_selector_sequence})
802
- @scanner.pos = pos
803
- @line = line
804
- begin
805
- # If we see "*E", don't force a throw because this could be the
806
- # "*prop: val" hack.
807
- expected('"{"') if res.length == 1 && res[0].is_a?(Selector::Universal)
808
- throw_error {expected('"{"')}
809
- rescue Sass::SyntaxError => e
810
- e.message << "\n\n\"#{sel}\" may only be used at the beginning of a compound selector."
811
- raise e
812
- end
737
+ if (comment = tok(COMMENT))
738
+ name << comment
813
739
  end
740
+ name_end_pos = source_position
814
741
 
815
- Selector::SimpleSequence.new(res, tok(/!/), range(start_pos))
816
- end
817
-
818
- def parent_selector
819
- return unless tok(/&/)
820
- Selector::Parent.new(interp_ident(NAME) || [])
821
- end
822
-
823
- def class_selector
824
- return unless tok(/\./)
825
- @expected = "class name"
826
- Selector::Class.new(merge(expr!(:interp_ident)))
827
- end
828
-
829
- def id_selector
830
- return unless tok(/#(?!\{)/)
831
- @expected = "id name"
832
- Selector::Id.new(merge(expr!(:interp_name)))
833
- end
834
-
835
- def placeholder_selector
836
- return unless tok(/%/)
837
- @expected = "placeholder name"
838
- Selector::Placeholder.new(merge(expr!(:interp_ident)))
839
- end
840
-
841
- def element_name
842
- ns, name = Sass::Util.destructure(qualified_name(:allow_star_name))
843
- return unless ns || name
742
+ mid = [str {ss}]
743
+ return name + mid unless tok(/:/)
744
+ mid << ':'
745
+ return name + mid + [':'] if tok(/:/)
746
+ mid << str {ss}
747
+ post_colon_whitespace = !mid.last.empty?
748
+ could_be_selector = !post_colon_whitespace && (tok?(IDENT_START) || tok?(INTERP_START))
844
749
 
845
- if name == '*'
846
- Selector::Universal.new(merge(ns))
847
- else
848
- Selector::Element.new(merge(name), merge(ns))
750
+ value_start_pos = source_position
751
+ value = nil
752
+ error = catch_error do
753
+ value = value!
754
+ if tok?(/\{/)
755
+ # Properties that are ambiguous with selectors can't have additional
756
+ # properties nested beneath them.
757
+ tok!(/;/) if could_be_selector
758
+ elsif !tok?(/[;{}]/)
759
+ # We want an exception if there's no valid end-of-property character
760
+ # exists, but we don't want to consume it if it does.
761
+ tok!(/[;{}]/)
762
+ end
849
763
  end
850
- end
851
764
 
852
- def qualified_name(allow_star_name = false)
853
- name = interp_ident || tok(/\*/) || (tok?(/\|/) && "")
854
- return unless name
855
- return nil, name unless tok(/\|/)
765
+ if error
766
+ rethrow error unless could_be_selector
856
767
 
857
- return name, expr!(:interp_ident) unless allow_star_name
858
- @expected = "identifier or *"
859
- return name, interp_ident || tok!(/\*/)
860
- end
768
+ # If the value would be followed by a semicolon, it's definitely
769
+ # supposed to be a property, not a selector.
770
+ additional_selector = almost_any_value
771
+ rethrow error if tok?(/;/)
861
772
 
862
- def interpolation_selector
863
- if (script = interpolation)
864
- Selector::Interpolation.new(script)
773
+ return name + mid + (additional_selector || [])
865
774
  end
866
- end
867
775
 
868
- def attrib
869
- return unless tok(/\[/)
870
- ss
871
- ns, name = attrib_name!
776
+ value_end_pos = source_position
872
777
  ss
778
+ require_block = tok?(/\{/)
873
779
 
874
- op = tok(/=/) ||
875
- tok(INCLUDES) ||
876
- tok(DASHMATCH) ||
877
- tok(PREFIXMATCH) ||
878
- tok(SUFFIXMATCH) ||
879
- tok(SUBSTRINGMATCH)
880
- if op
881
- @expected = "identifier or string"
882
- ss
883
- val = interp_ident || expr!(:interp_string)
884
- ss
885
- end
886
- flags = interp_ident || interp_string
887
- tok!(/\]/)
888
-
889
- Selector::Attribute.new(merge(name), merge(ns), op, merge(val), merge(flags))
890
- end
780
+ node = node(Sass::Tree::PropNode.new(name.flatten.compact, value, :new),
781
+ name_start_pos, value_end_pos)
782
+ node.name_source_range = range(name_start_pos, name_end_pos)
783
+ node.value_source_range = range(value_start_pos, value_end_pos)
891
784
 
892
- def attrib_name!
893
- if (name_or_ns = interp_ident)
894
- # E, E|E
895
- if tok(/\|(?!=)/)
896
- ns = name_or_ns
897
- name = interp_ident
898
- else
899
- name = name_or_ns
900
- end
901
- else
902
- # *|E or |E
903
- ns = [tok(/\*/) || ""]
904
- tok!(/\|/)
905
- name = expr!(:interp_ident)
906
- end
907
- return ns, name
785
+ return node unless require_block
786
+ nested_properties! node
908
787
  end
909
788
 
910
- def pseudo
911
- s = tok(/::?/)
912
- return unless s
913
- @expected = "pseudoclass or pseudoelement"
914
- name = expr!(:interp_ident)
915
- if tok(/\(/)
916
- ss
917
- arg = expr!(:pseudo_arg)
918
- while tok(/,/)
919
- arg << ',' << str {ss}
920
- arg.concat expr!(:pseudo_arg)
921
- end
922
- tok!(/\)/)
923
- end
924
- Selector::Pseudo.new(s == ':' ? :class : :element, merge(name), merge(arg))
925
- end
926
-
927
- def pseudo_arg
928
- # In the CSS spec, every pseudo-class/element either takes a pseudo
929
- # expression or a selector comma sequence as an argument. However, we
930
- # don't want to have to know which takes which, so we handle both at
931
- # once.
932
- #
933
- # However, there are some ambiguities between the two. For instance, "n"
934
- # could start a pseudo expression like "n+1", or it could start a
935
- # selector like "n|m". In order to handle this, we must regrettably
936
- # backtrack.
937
- expr, sel = nil, nil
938
- pseudo_err = catch_error do
939
- expr = pseudo_expr
940
- next if tok?(/[,)]/)
941
- expr = nil
942
- expected '")"'
789
+ # This production is similar to the CSS [`<any-value>`][any-value]
790
+ # production, but as the name implies, not quite the same. It's meant to
791
+ # consume values that could be a selector, an expression, or a combination
792
+ # of both. It respects strings and comments and supports interpolation. It
793
+ # will consume up to "{", "}", ";", or "!".
794
+ #
795
+ # [any-value]: http://dev.w3.org/csswg/css-variables/#typedef-any-value
796
+ #
797
+ # Values consumed by this production will usually be parsed more
798
+ # thoroughly once interpolation has been resolved.
799
+ def almost_any_value
800
+ return unless (tok = almost_any_value_token)
801
+ sel = [tok]
802
+ while (tok = almost_any_value_token)
803
+ sel << tok
943
804
  end
944
-
945
- return expr if expr
946
- sel_err = catch_error {sel = selector}
947
- return sel if sel
948
- rethrow pseudo_err if pseudo_err
949
- rethrow sel_err if sel_err
950
- nil
805
+ merge(sel)
951
806
  end
952
807
 
953
- def pseudo_expr_token
954
- tok(PLUS) || tok(/[-*]/) || tok(NUMBER) || interp_string || tok(IDENT) || interpolation
955
- end
956
-
957
- def pseudo_expr
958
- e = pseudo_expr_token
959
- return unless e
960
- res = [e, str {ss}]
961
- while (e = pseudo_expr_token)
962
- res << e << str {ss}
963
- end
964
- res
808
+ def almost_any_value_token
809
+ tok(%r{
810
+ (
811
+ (?!url\()
812
+ [^"/\#!;\{\}] # "
813
+ |
814
+ /(?![/*])
815
+ |
816
+ \#(?!\{)
817
+ |
818
+ !(?![a-z]) # TODO: never consume "!" when issue 1126 is fixed.
819
+ )+
820
+ }xi) || tok(COMMENT) || tok(SINGLE_LINE_COMMENT) || interp_string || interp_uri ||
821
+ interpolation(:warn_for_color)
965
822
  end
966
823
 
967
824
  def declaration
968
- # This allows the "*prop: val", ":prop: val", and ".prop: val" hacks
825
+ # This allows the "*prop: val", ":prop: val", "#prop: val", and ".prop:
826
+ # val" hacks.
969
827
  name_start_pos = source_position
970
828
  if (s = tok(/[:\*\.]|\#(?!\{)/))
971
- @use_property_exception = s !~ /[\.\#]/
972
829
  name = [s, str {ss}, *expr!(:interp_ident)]
973
830
  else
974
- name = interp_ident
975
- return unless name
976
- name = [name] if name.is_a?(String)
831
+ return unless (name = interp_ident)
832
+ name = Array(name)
977
833
  end
834
+
978
835
  if (comment = tok(COMMENT))
979
836
  name << comment
980
837
  end
@@ -982,7 +839,9 @@ module Sass
982
839
  ss
983
840
 
984
841
  tok!(/:/)
985
- value_start_pos, space, value = value!
842
+ ss
843
+ value_start_pos = source_position
844
+ value = value!
986
845
  value_end_pos = source_position
987
846
  ss
988
847
  require_block = tok?(/\{/)
@@ -993,19 +852,15 @@ module Sass
993
852
  node.value_source_range = range(value_start_pos, value_end_pos)
994
853
 
995
854
  return node unless require_block
996
- nested_properties! node, space
855
+ nested_properties! node
997
856
  end
998
857
 
999
858
  def value!
1000
- space = !str {ss}.empty?
1001
- value_start_pos = source_position
1002
- @use_property_exception ||= space || !tok?(IDENT)
1003
-
1004
859
  if tok?(/\{/)
1005
860
  str = Sass::Script::Tree::Literal.new(Sass::Script::Value::String.new(""))
1006
861
  str.line = source_position.line
1007
862
  str.source_range = range(source_position)
1008
- return value_start_pos, true, str
863
+ return str
1009
864
  end
1010
865
 
1011
866
  start_pos = source_position
@@ -1018,18 +873,12 @@ module Sass
1018
873
  str = Sass::Script::Tree::Literal.new(Sass::Script::Value::String.new(val.strip))
1019
874
  str.line = start_pos.line
1020
875
  str.source_range = range(start_pos)
1021
- return value_start_pos, space, str
876
+ return str
1022
877
  end
1023
- return value_start_pos, space, sass_script(:parse)
878
+ sass_script(:parse)
1024
879
  end
1025
880
 
1026
- def nested_properties!(node, space)
1027
- err(<<MESSAGE) unless space
1028
- Invalid CSS: a space is required between a property and its definition
1029
- when it has other properties nested beneath it.
1030
- MESSAGE
1031
-
1032
- @use_property_exception = true
881
+ def nested_properties!(node)
1033
882
  @expected = 'expression (e.g. 1px, bold) or "{"'
1034
883
  block(node, :property)
1035
884
  end
@@ -1083,9 +932,14 @@ MESSAGE
1083
932
  var
1084
933
  end
1085
934
 
1086
- def interpolation
935
+ def interpolation(warn_for_color = false)
1087
936
  return unless tok(INTERP_START)
1088
- sass_script(:parse_interpolated)
937
+ sass_script(:parse_interpolated, warn_for_color)
938
+ end
939
+
940
+ def string
941
+ return unless tok(STRING)
942
+ Sass::Script::Value::String.value(@scanner[1] || @scanner[2])
1089
943
  end
1090
944
 
1091
945
  def interp_string
@@ -1112,10 +966,10 @@ MESSAGE
1112
966
  end
1113
967
 
1114
968
  def interp_ident(start = IDENT)
1115
- val = tok(start) || interpolation || tok(IDENT_HYPHEN_INTERP, true)
969
+ val = tok(start) || interpolation(:warn_for_color) || tok(IDENT_HYPHEN_INTERP, true)
1116
970
  return unless val
1117
971
  res = [val]
1118
- while (val = tok(NAME) || interpolation)
972
+ while (val = tok(NAME) || interpolation(:warn_for_color))
1119
973
  res << val
1120
974
  end
1121
975
  res
@@ -1128,10 +982,6 @@ MESSAGE
1128
982
  return [var] if var
1129
983
  end
1130
984
 
1131
- def interp_name
1132
- interp_ident NAME
1133
- end
1134
-
1135
985
  def str
1136
986
  @strs.push ""
1137
987
  yield
@@ -1196,25 +1046,26 @@ MESSAGE
1196
1046
  :media_expr => "media expression (e.g. (min-device-width: 800px))",
1197
1047
  :at_root_query => "@at-root query (e.g. (without: media))",
1198
1048
  :at_root_directive_list => '* or identifier',
1199
- :pseudo_arg => "expression (e.g. fr, 2n+1)",
1049
+ :pseudo_args => "expression (e.g. fr, 2n+1)",
1200
1050
  :interp_ident => "identifier",
1201
- :interp_name => "identifier",
1202
1051
  :qualified_name => "identifier",
1203
1052
  :expr => "expression (e.g. 1px, bold)",
1204
- :_selector => "selector",
1205
1053
  :selector_comma_sequence => "selector",
1206
- :simple_selector_sequence => "selector",
1054
+ :string => "string",
1207
1055
  :import_arg => "file to import (string or url())",
1208
1056
  :moz_document_function => "matching function (e.g. url-prefix(), domain())",
1209
1057
  :supports_condition => "@supports condition (e.g. (display: flexbox))",
1210
1058
  :supports_condition_in_parens => "@supports condition (e.g. (display: flexbox))",
1059
+ :a_n_plus_b => "An+B expression",
1060
+ :keyframes_selector_component => "from, to, or a percentage",
1061
+ :keyframes_selector => "keyframes selector (e.g. 10%)"
1211
1062
  }
1212
1063
 
1213
1064
  TOK_NAMES = Sass::Util.to_hash(Sass::SCSS::RX.constants.map do |c|
1214
1065
  [Sass::SCSS::RX.const_get(c), c.downcase]
1215
1066
  end).merge(
1216
1067
  IDENT => "identifier",
1217
- /[;}]/ => '";"',
1068
+ /[;{}]/ => '";"',
1218
1069
  /\b(without|with)\b/ => '"with" or "without"'
1219
1070
  )
1220
1071
 
@@ -1348,6 +1199,11 @@ MESSAGE
1348
1199
  res
1349
1200
  end
1350
1201
  end
1202
+
1203
+ # Remove a vendor prefix from `str`.
1204
+ def deprefix(str)
1205
+ str.gsub(/^-[a-zA-Z0-9]+-/, '')
1206
+ end
1351
1207
  end
1352
1208
  end
1353
1209
  end