sass 3.3.14 → 3.4.0.rc.1

Sign up to get free protection for your applications and to get access to all the features.
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