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 +1 -1
- data/VERSION +1 -1
- data/lib/sass/engine.rb +22 -13
- data/lib/sass/media.rb +300 -0
- data/lib/sass/script/funcall.rb +2 -2
- data/lib/sass/script/literal.rb +0 -20
- data/lib/sass/script/operation.rb +8 -0
- data/lib/sass/script/parser.rb +21 -0
- data/lib/sass/scss/css_parser.rb +0 -11
- data/lib/sass/scss/parser.rb +95 -61
- data/lib/sass/scss/static_parser.rb +1 -0
- data/lib/sass/selector.rb +1 -1
- data/lib/sass/tree/directive_node.rb +17 -3
- data/lib/sass/tree/media_node.rb +8 -5
- data/lib/sass/tree/visitors/convert.rb +15 -11
- data/lib/sass/tree/visitors/cssize.rb +1 -3
- data/lib/sass/tree/visitors/deep_copy.rb +10 -0
- data/lib/sass/tree/visitors/perform.rb +10 -5
- data/lib/sass/tree/visitors/set_options.rb +10 -0
- data/lib/sass/tree/visitors/to_css.rb +16 -8
- data/lib/sass/util.rb +14 -0
- data/test/sass/conversion_test.rb +60 -0
- data/test/sass/engine_test.rb +171 -16
- data/test/sass/script_test.rb +5 -0
- data/test/sass/scss/css_test.rb +1 -1
- data/test/sass/scss/scss_test.rb +46 -24
- metadata +9 -8
data/REVISION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
77d6cfe978e5d2d28ee6126e207e19b86b661c27
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
3.2.0.alpha.
|
1
|
+
3.2.0.alpha.59
|
data/lib/sass/engine.rb
CHANGED
@@ -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
|
-
|
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(
|
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
|
-
|
783
|
-
|
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
|
data/lib/sass/media.rb
ADDED
@@ -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
|
data/lib/sass/script/funcall.rb
CHANGED
@@ -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.
|
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.
|
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
|
data/lib/sass/script/literal.rb
CHANGED
@@ -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
|