sass 3.2.0.alpha.57 → 3.2.0.alpha.59

Sign up to get free protection for your applications and to get access to all the features.
data/REVISION CHANGED
@@ -1 +1 @@
1
- a9d57b52dc7bfa8a777cf22d6ca36645855757ce
1
+ 77d6cfe978e5d2d28ee6126e207e19b86b661c27
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.2.0.alpha.57
1
+ 3.2.0.alpha.59
@@ -39,6 +39,7 @@ require 'sass/scss'
39
39
  require 'sass/error'
40
40
  require 'sass/importers'
41
41
  require 'sass/shared'
42
+ require 'sass/media'
42
43
 
43
44
  module Sass
44
45
 
@@ -223,7 +224,7 @@ module Sass
223
224
  # If you're compiling a single Sass file from the filesystem,
224
225
  # use \{Sass::Engine.for\_file}.
225
226
  # If you're compiling multiple files from the filesystem,
226
- # use {Sass::Plugin.
227
+ # use {Sass::Plugin}.
227
228
  #
228
229
  # @param template [String] The Sass template.
229
230
  # This template can be encoded using any encoding
@@ -639,7 +640,7 @@ WARNING
639
640
  # If value begins with url( or ",
640
641
  # it's a CSS @import rule and we don't want to touch it.
641
642
  if directive == "import"
642
- parse_import(line, value)
643
+ parse_import(line, value, offset)
643
644
  elsif directive == "mixin"
644
645
  parse_mixin_definition(line)
645
646
  elsif directive == "content"
@@ -691,9 +692,11 @@ WARNING
691
692
  :line => @line + 1) unless line.children.empty?
692
693
  Tree::CharsetNode.new(name)
693
694
  elsif directive == "media"
694
- Tree::MediaNode.new(value.split(',').map {|s| s.strip})
695
+ parser = Sass::SCSS::SassParser.new(value, @options[:filename], @line)
696
+ Tree::MediaNode.new(parser.parse_media_query_list)
695
697
  else
696
- Tree::DirectiveNode.new(line.text)
698
+ Tree::DirectiveNode.new(
699
+ value.nil? ? ["@#{directive}"] : ["@#{directive} "] + parse_interp(value, offset))
697
700
  end
698
701
  end
699
702
 
@@ -753,7 +756,7 @@ WARNING
753
756
  nil
754
757
  end
755
758
 
756
- def parse_import(line, value)
759
+ def parse_import(line, value, offset)
757
760
  raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath import directives.",
758
761
  :line => @line + 1) unless line.children.empty?
759
762
 
@@ -761,7 +764,7 @@ WARNING
761
764
  values = []
762
765
 
763
766
  loop do
764
- unless node = parse_import_arg(scanner)
767
+ unless node = parse_import_arg(scanner, offset + scanner.pos)
765
768
  raise SyntaxError.new("Invalid @import: expected file to import, was #{scanner.rest.inspect}",
766
769
  :line => @line)
767
770
  end
@@ -777,21 +780,27 @@ WARNING
777
780
  return values
778
781
  end
779
782
 
780
- def parse_import_arg(scanner)
783
+ def parse_import_arg(scanner, offset)
781
784
  return if scanner.eos?
782
- unless (str = scanner.scan(Sass::SCSS::RX::STRING)) ||
783
- (uri = scanner.scan(Sass::SCSS::RX::URI))
785
+
786
+ if scanner.match?(/url\(/i)
787
+ parser = Sass::Script::Parser.new(scanner, @line, offset, @options)
788
+ str = parser.parse_string
789
+ media = scanner.scan(/\s*[^,;].*/)
790
+ media &&= " #{media}"
791
+ return Tree::DirectiveNode.new(["@import ", str, media || ''])
792
+ end
793
+
794
+ unless str = scanner.scan(Sass::SCSS::RX::STRING)
784
795
  return Tree::ImportNode.new(scanner.scan(/[^,;]+/))
785
796
  end
786
797
 
787
798
  val = scanner[1] || scanner[2]
788
799
  scanner.scan(/\s*/)
789
800
  if media = scanner.scan(/[^,;].*/)
790
- Tree::DirectiveNode.new("@import #{str || uri} #{media}")
791
- elsif uri
792
- Tree::DirectiveNode.new("@import #{uri}")
801
+ Tree::DirectiveNode.new(["@import #{str || uri} #{media}"])
793
802
  elsif val =~ /^http:\/\//
794
- Tree::DirectiveNode.new("@import url(#{val})")
803
+ Tree::DirectiveNode.new(["@import url(#{val})"])
795
804
  else
796
805
  Tree::ImportNode.new(val)
797
806
  end
@@ -0,0 +1,300 @@
1
+ # A namespace for the `@media` query parse tree.
2
+ module Sass::Media
3
+ # A comma-separated list of queries.
4
+ #
5
+ # media_query [ ',' S* media_query ]*
6
+ class QueryList
7
+ # The queries contained in this list.
8
+ #
9
+ # @return [Array<Query>]
10
+ attr_accessor :queries
11
+
12
+ # @param queries [Array<Query>] See \{#queries}
13
+ def initialize(queries)
14
+ @queries = queries
15
+ end
16
+
17
+ # Runs the SassScript in the media query list.
18
+ #
19
+ # @yield [interp] A block that should perform interpolation.
20
+ # @yieldparam interp [Array<String, Sass::Script::Node>]
21
+ # An interpolation array to perform.
22
+ # @yieldreturn [String] The interpolated value.
23
+ def perform(&run_interp)
24
+ @queries.each {|q| q.perform(&run_interp)}
25
+ end
26
+
27
+ # Merges this query list with another. The returned query list
28
+ # queries for the intersection between the two inputs.
29
+ #
30
+ # Both query lists should be resolved.
31
+ #
32
+ # @param other [QueryList]
33
+ # @return [QueryList?] The merged list, or nil if there is no intersection.
34
+ def merge(other)
35
+ new_queries = queries.map {|q1| other.queries.map {|q2| q1.merge(q2)}}.flatten.compact
36
+ return if new_queries.empty?
37
+ QueryList.new(new_queries)
38
+ end
39
+
40
+ # Returns the CSS for the media query list.
41
+ #
42
+ # @return [String]
43
+ def to_css
44
+ queries.map {|q| q.to_css}.join(', ')
45
+ end
46
+
47
+ # Returns the Sass/SCSS code for the media query list.
48
+ #
49
+ # @param options [{Symbol => Object}] An options hash (see {Sass::CSS#initialize}).
50
+ # @return [String]
51
+ def to_src(options)
52
+ queries.map {|q| q.to_src(options)}.join(', ')
53
+ end
54
+
55
+ # Returns a deep copy of this query list and all its children.
56
+ #
57
+ # @return [QueryList]
58
+ def deep_copy
59
+ QueryList.new(queries.map {|q| q.deep_copy})
60
+ end
61
+
62
+ # Sets the options hash for the script nodes in the media query.
63
+ #
64
+ # @param options [{Symbol => Object}] The options has to set.
65
+ def options=(options)
66
+ queries.each {|q| q.options = options}
67
+ end
68
+ end
69
+
70
+ # A single media query.
71
+ #
72
+ # [ [ONLY | NOT]? S* media_type S* | expression ] [ AND S* expression ]*
73
+ class Query
74
+ # The modifier for the query.
75
+ #
76
+ # @return [Array<String, Sass::Script::Node>]
77
+ attr_accessor :modifier
78
+
79
+ # The modifier for the query after any SassScript has been resolved.
80
+ # Only set once \{Tree::Visitors::Perform} has been run.
81
+ #
82
+ # @return [String]
83
+ attr_accessor :resolved_modifier
84
+
85
+ # The type of the query (e.g. `"screen"` or `"print"`).
86
+ #
87
+ # @return [Array<String, Sass::Script::Node>]
88
+ attr_accessor :type
89
+
90
+ # The type of the query after any SassScript has been resolved.
91
+ # Only set once \{Tree::Visitors::Perform} has been run.
92
+ #
93
+ # @return [String]
94
+ attr_accessor :resolved_type
95
+
96
+ # The trailing expressions in the query.
97
+ #
98
+ # @return [Array<Expression>]
99
+ attr_accessor :expressions
100
+
101
+ # @param modifier [Array<String, Sass::Script::Node>] See \{#modifier}
102
+ # @param type [Array<String, Sass::Script::Node>] See \{#type}
103
+ # @param expressions [Array<Expression>] See \{#expressions}
104
+ def initialize(modifier, type, expressions)
105
+ @modifier = modifier
106
+ @type = type
107
+ @expressions = expressions
108
+ end
109
+
110
+ # Runs the SassScript in the media query.
111
+ #
112
+ # @yield [interp] A block that should perform interpolation.
113
+ # @yieldparam interp [Array<String, Sass::Script::Node>]
114
+ # An interpolation array to perform.
115
+ # @yieldreturn [String] The interpolated value.
116
+ def perform(&run_interp)
117
+ @resolved_modifier = yield modifier
118
+ @resolved_type = yield type
119
+ expressions.each {|e| e.perform(&run_interp)}
120
+ end
121
+
122
+ # Merges this query with another. The returned query queries for
123
+ # the intersection between the two inputs.
124
+ #
125
+ # Both queries should be resolved.
126
+ #
127
+ # @param other [Query]
128
+ # @return [Query?] The merged query, or nil if there is no intersection.
129
+ def merge(other)
130
+ m1, t1 = resolved_modifier.downcase, resolved_type.downcase
131
+ m2, t2 = other.resolved_modifier.downcase, other.resolved_type.downcase
132
+ t1 = t2 if t1.empty?
133
+ t2 = t1 if t2.empty?
134
+ if ((m1 == 'not') ^ (m2 == 'not'))
135
+ return if t1 == t2
136
+ type = m1 == 'not' ? t2 : t1
137
+ mod = m1 == 'not' ? m2 : m1
138
+ elsif m1 == 'not' && m2 == 'not'
139
+ # CSS has no way of representing "neither screen nor print"
140
+ return unless t1 == t2
141
+ type = t1
142
+ mod = 'not'
143
+ elsif t1 != t2
144
+ return
145
+ else # t1 == t2, neither m1 nor m2 are "not"
146
+ type = t1
147
+ mod = m1.empty? ? m2 : m1
148
+ end
149
+ q = Query.new([], [], other.expressions + expressions)
150
+ q.resolved_type = type
151
+ q.resolved_modifier = mod
152
+ return q
153
+ end
154
+
155
+ # Returns the CSS for the media query.
156
+ #
157
+ # @return [String]
158
+ def to_css
159
+ css = ''
160
+ css << resolved_modifier
161
+ css << ' ' unless resolved_modifier.empty?
162
+ css << resolved_type
163
+ css << ' and ' unless resolved_type.empty? || expressions.empty?
164
+ css << expressions.map {|e| e.to_css}.join(' and ')
165
+ css
166
+ end
167
+
168
+ # Returns the Sass/SCSS code for the media query.
169
+ #
170
+ # @param options [{Symbol => Object}] An options hash (see {Sass::CSS#initialize}).
171
+ # @return [String]
172
+ def to_src(options)
173
+ src = ''
174
+ src << Sass::Media._interp_or_var_to_src(modifier, options)
175
+ src << ' ' unless modifier.empty?
176
+ src << Sass::Media._interp_or_var_to_src(type, options)
177
+ src << ' and ' unless type.empty? || expressions.empty?
178
+ src << expressions.map {|e| e.to_src(options)}.join(' and ')
179
+ src
180
+ end
181
+
182
+ # Returns a deep copy of this query and all its children.
183
+ #
184
+ # @return [Query]
185
+ def deep_copy
186
+ Query.new(
187
+ modifier.map {|c| c.is_a?(Sass::Script::Node) ? c.deep_copy : c},
188
+ type.map {|c| c.is_a?(Sass::Script::Node) ? c.deep_copy : c},
189
+ expressions.map {|q| q.deep_copy})
190
+ end
191
+
192
+ # Sets the options hash for the script nodes in the media query.
193
+ #
194
+ # @param options [{Symbol => Object}] The options has to set.
195
+ def options=(options)
196
+ modifier.each {|m| m.options = options if m.is_a?(Sass::Script::Node)}
197
+ type.each {|t| t.options = options if t.is_a?(Sass::Script::Node)}
198
+ expressions.each {|e| e.options = options}
199
+ end
200
+ end
201
+
202
+ # A media query expression.
203
+ #
204
+ # '(' S* media_feature S* [ ':' S* expr ]? ')'
205
+ class Expression
206
+ # The name of the feature being queried for.
207
+ #
208
+ # @return [Array<String, Sass::Script::Node>]
209
+ attr_accessor :name
210
+
211
+ # The name of the feature after any SassScript has been resolved.
212
+ # Only set once \{Tree::Visitors::Perform} has been run.
213
+ #
214
+ # @return [String]
215
+ attr_accessor :resolved_name
216
+
217
+ # The value of the feature.
218
+ #
219
+ # @return [Array<String, Sass::Script::Node>]
220
+ attr_accessor :value
221
+
222
+ # The value of the feature after any SassScript has been resolved.
223
+ # Only set once \{Tree::Visitors::Perform} has been run.
224
+ #
225
+ # @return [String]
226
+ attr_accessor :resolved_value
227
+
228
+ # @param name [Array<String, Sass::Script::Node>] See \{#name}
229
+ # @param value [Array<String, Sass::Script::Node>] See \{#value}
230
+ def initialize(name, value)
231
+ @name = name
232
+ @value = value
233
+ end
234
+
235
+ # Runs the SassScript in the expression.
236
+ #
237
+ # @yield [interp] A block that should perform interpolation.
238
+ # @yieldparam interp [Array<String, Sass::Script::Node>]
239
+ # An interpolation array to perform.
240
+ # @yieldreturn [String] The interpolated value.
241
+ def perform
242
+ @resolved_name = yield name
243
+ @resolved_value = yield value
244
+ end
245
+
246
+ # Returns the CSS for the expression.
247
+ #
248
+ # @return [String]
249
+ def to_css
250
+ css = '('
251
+ css << resolved_name
252
+ css << ': ' << resolved_value unless resolved_value.empty?
253
+ css << ')'
254
+ css
255
+ end
256
+
257
+ # Returns the Sass/SCSS code for the expression.
258
+ #
259
+ # @param options [{Symbol => Object}] An options hash (see {Sass::CSS#initialize}).
260
+ # @return [String]
261
+ def to_src(options)
262
+ src = '('
263
+ src << Sass::Media._interp_or_var_to_src(name, options)
264
+ src << ': ' << Sass::Media._interp_or_var_to_src(value, options) unless value.empty?
265
+ src << ')'
266
+ src
267
+ end
268
+
269
+ # Returns a deep copy of this expression.
270
+ #
271
+ # @return [Expression]
272
+ def deep_copy
273
+ Expression.new(
274
+ name.map {|c| c.is_a?(Sass::Script::Node) ? c.deep_copy : c},
275
+ value.map {|c| c.is_a?(Sass::Script::Node) ? c.deep_copy : c})
276
+ end
277
+
278
+ # Sets the options hash for the script nodes in the expression.
279
+ #
280
+ # @param options [{Symbol => Object}] The options has to set.
281
+ def options=(options)
282
+ name.each {|n| n.options = options if n.is_a?(Sass::Script::Node)}
283
+ value.each {|v| v.options = options if v.is_a?(Sass::Script::Node)}
284
+ end
285
+ end
286
+
287
+ # Converts an interpolation array that may represent a single variable to source.
288
+ #
289
+ # @param [Array<String, Sass::Script::Node>] The interpolation array to convert.
290
+ # @param options [{Symbol => Object}] An options hash (see {Sass::CSS#initialize}).
291
+ # @return [String]
292
+ def self._interp_or_var_to_src(interp, options)
293
+ interp = interp.reject {|v| v.is_a?(String) && v.empty?}
294
+ return interp[0].to_sass(options) if interp.length == 1 && interp[0].is_a?(Sass::Script::Variable)
295
+ interp.map do |r|
296
+ next r if r.is_a?(String)
297
+ "\#{#{r.to_sass(options)}}"
298
+ end.join
299
+ end
300
+ end
@@ -36,7 +36,7 @@ module Sass
36
36
  # @return [String] A string representation of the function call
37
37
  def inspect
38
38
  args = @args.map {|a| a.inspect}.join(', ')
39
- keywords = @keywords.sort_by {|k, v| k}.
39
+ keywords = Sass::Util.hash_to_a(@keywords).
40
40
  map {|k, v| "$#{k}: #{v.inspect}"}.join(', ')
41
41
  "#{name}(#{args}#{', ' unless args.empty? || keywords.empty?}#{keywords})"
42
42
  end
@@ -44,7 +44,7 @@ module Sass
44
44
  # @see Node#to_sass
45
45
  def to_sass(opts = {})
46
46
  args = @args.map {|a| a.to_sass(opts)}.join(', ')
47
- keywords = @keywords.sort_by {|k, v| k}.
47
+ keywords = Sass::Util.hash_to_a(@keywords).
48
48
  map {|k, v| "$#{dasherize(k, opts)}: #{v.to_sass(opts)}"}.join(', ')
49
49
  "#{dasherize(name, opts)}(#{args}#{', ' unless args.empty? || keywords.empty?}#{keywords})"
50
50
  end
@@ -55,26 +55,6 @@ The #options attribute is not set on this #{self.class}.
55
55
  MSG
56
56
  end
57
57
 
58
- # The SassScript `and` operation.
59
- #
60
- # @param other [Literal] The right-hand side of the operator
61
- # @return [Literal] The result of a logical and:
62
- # `other` if this literal isn't a false {Bool},
63
- # and this literal otherwise
64
- def and(other)
65
- to_bool ? other : self
66
- end
67
-
68
- # The SassScript `or` operation.
69
- #
70
- # @param other [Literal] The right-hand side of the operator
71
- # @return [Literal] The result of the logical or:
72
- # this literal if it isn't a false {Bool},
73
- # and `other` otherwise
74
- def or(other)
75
- to_bool ? self : other
76
- end
77
-
78
58
  # The SassScript `==` operation.
79
59
  # **Note that this returns a {Sass::Script::Bool} object,
80
60
  # not a Ruby boolean**.
@@ -72,6 +72,14 @@ module Sass::Script
72
72
  # @raise [Sass::SyntaxError] if the operation is undefined for the operands
73
73
  def _perform(environment)
74
74
  literal1 = @operand1.perform(environment)
75
+
76
+ # Special-case :and and :or to support short-circuiting.
77
+ if @operator == :and
78
+ return literal1.to_bool ? @operand2.perform(environment) : literal1
79
+ elsif @operator == :or
80
+ return literal1.to_bool ? literal1 : @operand2.perform(environment)
81
+ end
82
+
75
83
  literal2 = @operand2.perform(environment)
76
84
 
77
85
  begin