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/lib/sass/script/parser.rb
CHANGED
@@ -130,6 +130,27 @@ module Sass
|
|
130
130
|
raise e
|
131
131
|
end
|
132
132
|
|
133
|
+
# Parse a single string value, possibly containing interpolation.
|
134
|
+
# Doesn't assert that the scanner is finished after parsing.
|
135
|
+
#
|
136
|
+
# @return [Script::Node] The root node of the parse tree.
|
137
|
+
# @raise [Sass::SyntaxError] if the string isn't valid SassScript
|
138
|
+
def parse_string
|
139
|
+
unless (peek = @lexer.peek) &&
|
140
|
+
(peek.type == :string ||
|
141
|
+
(peek.type == :funcall && peek.value.downcase == 'url'))
|
142
|
+
lexer.expected!("string")
|
143
|
+
end
|
144
|
+
|
145
|
+
expr = assert_expr :funcall
|
146
|
+
expr.options = @options
|
147
|
+
@lexer.unpeek!
|
148
|
+
expr
|
149
|
+
rescue Sass::SyntaxError => e
|
150
|
+
e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
|
151
|
+
raise e
|
152
|
+
end
|
153
|
+
|
133
154
|
# Parses a SassScript expression.
|
134
155
|
#
|
135
156
|
# @overload parse(str, line, offset, filename = nil)
|
data/lib/sass/scss/css_parser.rb
CHANGED
@@ -7,22 +7,11 @@ module Sass
|
|
7
7
|
# parent references, nested selectors, and so forth.
|
8
8
|
# It does support all the same CSS hacks as the SCSS parser, though.
|
9
9
|
class CssParser < StaticParser
|
10
|
-
# Parse a selector, and return its value as a string.
|
11
|
-
#
|
12
|
-
# @return [String, nil] The parsed selector, or nil if no selector was parsed
|
13
|
-
# @raise [Sass::SyntaxError] if there's a syntax error in the selector
|
14
|
-
def parse_selector_string
|
15
|
-
init_scanner!
|
16
|
-
str {return unless selector}
|
17
|
-
end
|
18
|
-
|
19
10
|
private
|
20
11
|
|
21
12
|
def placeholder_selector; nil; end
|
22
13
|
def parent_selector; nil; end
|
23
14
|
def interpolation; nil; end
|
24
|
-
def interp_string; tok(STRING); end
|
25
|
-
def interp_ident(ident = IDENT); tok(ident); end
|
26
15
|
def use_css_import?; true; end
|
27
16
|
|
28
17
|
def block_child(context)
|
data/lib/sass/scss/parser.rb
CHANGED
@@ -40,6 +40,18 @@ module Sass
|
|
40
40
|
interp_ident
|
41
41
|
end
|
42
42
|
|
43
|
+
# Parses a media query list.
|
44
|
+
#
|
45
|
+
# @return [Sass::Media::QueryList] The parsed query list
|
46
|
+
# @raise [Sass::SyntaxError] if there's a syntax error in the query list,
|
47
|
+
# or if it doesn't take up the entire input string.
|
48
|
+
def parse_media_query_list
|
49
|
+
init_scanner!
|
50
|
+
ql = media_query_list
|
51
|
+
expected("media query list") unless @scanner.eos?
|
52
|
+
ql
|
53
|
+
end
|
54
|
+
|
43
55
|
private
|
44
56
|
|
45
57
|
include Sass::SCSS::RX
|
@@ -122,10 +134,11 @@ module Sass
|
|
122
134
|
end
|
123
135
|
|
124
136
|
# Most at-rules take expressions (e.g. @import),
|
125
|
-
# but some (e.g. @page) take selector-like arguments
|
126
|
-
|
127
|
-
val
|
128
|
-
|
137
|
+
# but some (e.g. @page) take selector-like arguments.
|
138
|
+
# Some take no arguments at all.
|
139
|
+
val = expr || selector
|
140
|
+
val = val ? ["@#{name} "] + Sass::Util.strip_string_array(val) : ["@#{name}"]
|
141
|
+
node = node(Sass::Tree::DirectiveNode.new(val))
|
129
142
|
|
130
143
|
if tok(/\{/)
|
131
144
|
node.has_children = true
|
@@ -277,14 +290,21 @@ module Sass
|
|
277
290
|
end
|
278
291
|
|
279
292
|
def import_arg
|
280
|
-
return unless
|
281
|
-
|
293
|
+
return unless (str = tok(STRING)) || (uri = tok?(/url\(/i))
|
294
|
+
if uri
|
295
|
+
str = sass_script(:parse_string)
|
296
|
+
media = str {media_query_list}.strip
|
297
|
+
media = " #{media}" unless media.empty?
|
298
|
+
ss
|
299
|
+
return node(Tree::DirectiveNode.new(["@import ", str, media]))
|
300
|
+
end
|
301
|
+
|
302
|
+
path = @scanner[1] || @scanner[2]
|
282
303
|
ss
|
283
304
|
|
284
305
|
media = str {media_query_list}.strip
|
285
|
-
|
286
|
-
|
287
|
-
return node(Sass::Tree::DirectiveNode.new("@import #{arg} #{media}".strip))
|
306
|
+
if path =~ /^http:\/\// || !media.empty? || use_css_import?
|
307
|
+
return node(Sass::Tree::DirectiveNode.new(["@import #{str} #{media}"]))
|
288
308
|
end
|
289
309
|
|
290
310
|
node(Sass::Tree::ImportNode.new(path.strip))
|
@@ -298,52 +318,66 @@ module Sass
|
|
298
318
|
|
299
319
|
# http://www.w3.org/TR/css3-mediaqueries/#syntax
|
300
320
|
def media_query_list
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
return unless has_q
|
305
|
-
queries = [q.strip]
|
321
|
+
return unless query = media_query
|
322
|
+
queries = [query]
|
306
323
|
|
307
324
|
ss
|
308
325
|
while tok(/,/)
|
309
|
-
ss; queries <<
|
326
|
+
ss; queries << expr!(:media_query)
|
310
327
|
end
|
328
|
+
ss
|
311
329
|
|
312
|
-
queries
|
330
|
+
Sass::Media::QueryList.new(queries)
|
313
331
|
end
|
314
332
|
|
315
333
|
def media_query
|
316
|
-
if
|
334
|
+
if ident1 = interp_ident_or_var
|
317
335
|
ss
|
318
|
-
|
319
|
-
tok!(IDENT)
|
336
|
+
ident2 = interp_ident_or_var
|
320
337
|
ss
|
321
|
-
|
322
|
-
|
338
|
+
if ident2 && ident2.length == 1 && ident2[0].is_a?(String) && ident2[0].downcase == 'and'
|
339
|
+
query = Sass::Media::Query.new([], ident1, [])
|
340
|
+
else
|
341
|
+
if ident2
|
342
|
+
query = Sass::Media::Query.new(ident1, ident2, [])
|
343
|
+
else
|
344
|
+
query = Sass::Media::Query.new([], ident1, [])
|
345
|
+
end
|
346
|
+
return query unless tok(/and/i)
|
347
|
+
ss
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
if query
|
352
|
+
expr = expr!(:media_expr)
|
353
|
+
else
|
354
|
+
return unless expr = media_expr
|
323
355
|
end
|
356
|
+
query ||= Sass::Media::Query.new([], [], [])
|
357
|
+
query.expressions << expr
|
324
358
|
|
325
359
|
ss
|
326
360
|
while tok(/and/i)
|
327
|
-
ss; expr!(:media_expr)
|
361
|
+
ss; query.expressions << expr!(:media_expr)
|
328
362
|
end
|
329
363
|
|
330
|
-
|
364
|
+
query
|
331
365
|
end
|
332
366
|
|
333
367
|
def media_expr
|
334
368
|
return unless tok(/\(/)
|
335
369
|
ss
|
336
370
|
@expected = "media feature (e.g. min-device-width, color)"
|
337
|
-
|
371
|
+
name = expr!(:interp_ident_or_var)
|
338
372
|
ss
|
339
373
|
|
340
374
|
if tok(/:/)
|
341
|
-
ss; expr!(:expr)
|
375
|
+
ss; value = expr!(:expr)
|
342
376
|
end
|
343
377
|
tok!(/\)/)
|
344
378
|
ss
|
345
379
|
|
346
|
-
|
380
|
+
Sass::Media::Expression.new(name, value || [])
|
347
381
|
end
|
348
382
|
|
349
383
|
def charset_directive
|
@@ -371,10 +405,6 @@ module Sass
|
|
371
405
|
str {ss if tok(/[\/,:.=]/)}
|
372
406
|
end
|
373
407
|
|
374
|
-
def unary_operator
|
375
|
-
tok(/[+-]/)
|
376
|
-
end
|
377
|
-
|
378
408
|
def ruleset
|
379
409
|
return unless rules = selector_sequence
|
380
410
|
block(node(Sass::Tree::RuleNode.new(rules.flatten.compact)), :ruleset)
|
@@ -507,9 +537,9 @@ module Sass
|
|
507
537
|
|
508
538
|
def simple_selector_sequence
|
509
539
|
# This allows for stuff like http://www.w3.org/TR/css3-animations/#keyframes-
|
510
|
-
return expr unless e = element_name || id_selector ||
|
511
|
-
placeholder_selector || attrib || negation ||
|
512
|
-
interpolation_selector
|
540
|
+
return expr(!:allow_var) unless e = element_name || id_selector ||
|
541
|
+
class_selector || placeholder_selector || attrib || negation ||
|
542
|
+
pseudo || parent_selector || interpolation_selector
|
513
543
|
res = [e]
|
514
544
|
|
515
545
|
# The tok(/\*/) allows the "E*" hack
|
@@ -698,17 +728,6 @@ module Sass
|
|
698
728
|
return space, sass_script(:parse)
|
699
729
|
end
|
700
730
|
|
701
|
-
def plain_value
|
702
|
-
return unless tok(/:/)
|
703
|
-
space = !str {ss}.empty?
|
704
|
-
@use_property_exception ||= space || !tok?(IDENT)
|
705
|
-
|
706
|
-
expression = expr
|
707
|
-
expression << tok(IMPORTANT) if expression
|
708
|
-
# expression, space, value
|
709
|
-
return expression, space, expression || [""]
|
710
|
-
end
|
711
|
-
|
712
731
|
def nested_properties!(node, space)
|
713
732
|
err(<<MESSAGE) unless space
|
714
733
|
Invalid CSS: a space is required between a property and its definition
|
@@ -720,43 +739,53 @@ MESSAGE
|
|
720
739
|
block(node, :property)
|
721
740
|
end
|
722
741
|
|
723
|
-
def expr
|
724
|
-
return unless t = term
|
742
|
+
def expr(allow_var = true)
|
743
|
+
return unless t = term(allow_var)
|
725
744
|
res = [t, str{ss}]
|
726
745
|
|
727
|
-
while (o = operator) && (t = term)
|
746
|
+
while (o = operator) && (t = term(allow_var))
|
728
747
|
res << o << t << str{ss}
|
729
748
|
end
|
730
749
|
|
731
|
-
res
|
750
|
+
res.flatten
|
732
751
|
end
|
733
752
|
|
734
|
-
def term
|
735
|
-
|
753
|
+
def term(allow_var)
|
754
|
+
if e = tok(NUMBER) ||
|
736
755
|
tok(URI) ||
|
737
|
-
function ||
|
756
|
+
function(allow_var) ||
|
738
757
|
tok(STRING) ||
|
739
758
|
tok(UNICODERANGE) ||
|
740
|
-
|
741
|
-
tok(HEXCOLOR)
|
742
|
-
|
743
|
-
return
|
744
|
-
@expected = "number or function"
|
745
|
-
return [op, tok(NUMBER) || expr!(:function)]
|
759
|
+
interp_ident ||
|
760
|
+
tok(HEXCOLOR) ||
|
761
|
+
(allow_var && var_expr)
|
762
|
+
return e
|
746
763
|
end
|
747
|
-
|
764
|
+
|
765
|
+
return unless op = tok(/[+-]/)
|
766
|
+
@expected = "number or function"
|
767
|
+
return [op, tok(NUMBER) || function(allow_var) ||
|
768
|
+
(allow_var && var_expr) || expr!(:interpolation)]
|
748
769
|
end
|
749
770
|
|
750
|
-
def function
|
771
|
+
def function(allow_var)
|
751
772
|
return unless name = tok(FUNCTION)
|
752
773
|
if name == "expression(" || name == "calc("
|
753
774
|
str, _ = Sass::Shared.balance(@scanner, ?(, ?), 1)
|
754
775
|
[name, str]
|
755
776
|
else
|
756
|
-
[name, str{ss}, expr, tok!(/\)/)]
|
777
|
+
[name, str{ss}, expr(allow_var), tok!(/\)/)]
|
757
778
|
end
|
758
779
|
end
|
759
780
|
|
781
|
+
def var_expr
|
782
|
+
return unless tok(/\$/)
|
783
|
+
line = @line
|
784
|
+
var = Sass::Script::Variable.new(tok!(IDENT))
|
785
|
+
var.line = line
|
786
|
+
var
|
787
|
+
end
|
788
|
+
|
760
789
|
def interpolation
|
761
790
|
return unless tok(INTERP_START)
|
762
791
|
sass_script(:parse_interpolated)
|
@@ -789,6 +818,11 @@ MESSAGE
|
|
789
818
|
res
|
790
819
|
end
|
791
820
|
|
821
|
+
def interp_ident_or_var
|
822
|
+
(id = interp_ident) and return id
|
823
|
+
(var = var_expr) and return [var]
|
824
|
+
end
|
825
|
+
|
792
826
|
def interp_name
|
793
827
|
interp_ident NAME
|
794
828
|
end
|
@@ -841,7 +875,7 @@ MESSAGE
|
|
841
875
|
|
842
876
|
EXPR_NAMES = {
|
843
877
|
:media_query => "media query (e.g. print, screen, print and screen)",
|
844
|
-
:media_expr => "media expression (e.g. (min-device-width: 800px))
|
878
|
+
:media_expr => "media expression (e.g. (min-device-width: 800px))",
|
845
879
|
:pseudo_expr => "expression (e.g. fr, 2n+1)",
|
846
880
|
:interp_ident => "identifier",
|
847
881
|
:interp_name => "identifier",
|
@@ -27,6 +27,7 @@ module Sass
|
|
27
27
|
def variable; nil; end
|
28
28
|
def script_value; nil; end
|
29
29
|
def interpolation; nil; end
|
30
|
+
def var_expr; nil; end
|
30
31
|
def interp_string; s = tok(STRING) and [s]; end
|
31
32
|
def interp_ident(ident = IDENT); s = tok(ident) and [s]; end
|
32
33
|
def use_css_import?; true; end
|
data/lib/sass/selector.rb
CHANGED
@@ -379,7 +379,7 @@ module Sass
|
|
379
379
|
attr_reader :selector
|
380
380
|
|
381
381
|
# @param [String] The name of the pseudoclass
|
382
|
-
# @param [Selector::
|
382
|
+
# @param [Selector::CommaSequence] The selector argument
|
383
383
|
def initialize(name, selector)
|
384
384
|
@name = name
|
385
385
|
@selector = selector
|
@@ -9,15 +9,29 @@ module Sass::Tree
|
|
9
9
|
#
|
10
10
|
# @see Sass::Tree
|
11
11
|
class DirectiveNode < Node
|
12
|
-
# The text of the directive, `@` and all.
|
12
|
+
# The text of the directive, `@` and all, with interpolation included.
|
13
13
|
#
|
14
|
-
# @return [String]
|
14
|
+
# @return [Array<String, Sass::Script::Node>]
|
15
15
|
attr_accessor :value
|
16
16
|
|
17
|
-
#
|
17
|
+
# The text of the directive after any interpolated SassScript has been resolved.
|
18
|
+
# Only set once \{Tree::Visitors::Perform} has been run.
|
19
|
+
#
|
20
|
+
# @return [String]
|
21
|
+
attr_accessor :resolved_value
|
22
|
+
|
23
|
+
# @param value [Array<String, Sass::Script::Node>] See \{#value}
|
18
24
|
def initialize(value)
|
19
25
|
@value = value
|
20
26
|
super()
|
21
27
|
end
|
28
|
+
|
29
|
+
# @param value [String] See \{#resolved_value}
|
30
|
+
# @return [DirectiveNode]
|
31
|
+
def self.resolved(value)
|
32
|
+
node = new([value])
|
33
|
+
node.resolved_value = value
|
34
|
+
node
|
35
|
+
end
|
22
36
|
end
|
23
37
|
end
|
data/lib/sass/tree/media_node.rb
CHANGED
@@ -6,9 +6,9 @@ module Sass::Tree
|
|
6
6
|
#
|
7
7
|
# @see Sass::Tree
|
8
8
|
class MediaNode < DirectiveNode
|
9
|
-
# The media query.
|
9
|
+
# The media query.
|
10
10
|
#
|
11
|
-
# @return [
|
11
|
+
# @return [Sass::Media::Query]
|
12
12
|
attr_accessor :query
|
13
13
|
|
14
14
|
# @see RuleNode#tabs
|
@@ -17,7 +17,7 @@ module Sass::Tree
|
|
17
17
|
# @see RuleNode#group_end
|
18
18
|
attr_accessor :group_end
|
19
19
|
|
20
|
-
# @param query [
|
20
|
+
# @param query [Sass::Media::Query] See \{#query}
|
21
21
|
def initialize(query)
|
22
22
|
@query = query
|
23
23
|
@tabs = 0
|
@@ -25,8 +25,11 @@ module Sass::Tree
|
|
25
25
|
end
|
26
26
|
|
27
27
|
# @see DirectiveNode#value
|
28
|
-
def value
|
29
|
-
|
28
|
+
def value; raise NotImplementedError; end
|
29
|
+
|
30
|
+
# @see DirectiveNode#resolved_value
|
31
|
+
def resolved_value
|
32
|
+
@resolved_value ||= "@media #{query.to_css}"
|
30
33
|
end
|
31
34
|
end
|
32
35
|
end
|
@@ -49,11 +49,7 @@ class Sass::Tree::Visitors::Convert < Sass::Tree::Visitors::Base
|
|
49
49
|
end
|
50
50
|
|
51
51
|
def visit_comment(node)
|
52
|
-
value = node.value
|
53
|
-
next r if r.is_a?(String)
|
54
|
-
"\#{#{r.to_sass(@options)}}"
|
55
|
-
end.join
|
56
|
-
|
52
|
+
value = interp_to_src(node.value)
|
57
53
|
content = if @format == :sass
|
58
54
|
content = value.gsub(/\*\/$/, '').rstrip
|
59
55
|
if content =~ /\A[ \t]/
|
@@ -91,7 +87,7 @@ class Sass::Tree::Visitors::Convert < Sass::Tree::Visitors::Base
|
|
91
87
|
end.gsub(/^/, spaces) + "\n"
|
92
88
|
content
|
93
89
|
end
|
94
|
-
content.sub!(%r{^\s*(/\*)}, '/*!') if node.type == :loud
|
90
|
+
content.sub!(%r{^\s*(/\*)}, '/*!') if node.type == :loud #'
|
95
91
|
content
|
96
92
|
end
|
97
93
|
|
@@ -100,7 +96,8 @@ class Sass::Tree::Visitors::Convert < Sass::Tree::Visitors::Base
|
|
100
96
|
end
|
101
97
|
|
102
98
|
def visit_directive(node)
|
103
|
-
res = "#{tab_str}#{node.value}"
|
99
|
+
res = "#{tab_str}#{interp_to_src(node.value)}"
|
100
|
+
res.gsub!(/^@import \#\{(.*)\}([^}]*)$/, '@import \1\2');
|
104
101
|
return res + "#{semi}\n" unless node.has_children
|
105
102
|
res + yield + "\n"
|
106
103
|
end
|
@@ -148,7 +145,7 @@ class Sass::Tree::Visitors::Convert < Sass::Tree::Visitors::Base
|
|
148
145
|
end
|
149
146
|
|
150
147
|
def visit_media(node)
|
151
|
-
"#{tab_str}@media #{node.query.
|
148
|
+
"#{tab_str}@media #{node.query.to_src(@options)}#{yield}"
|
152
149
|
end
|
153
150
|
|
154
151
|
def visit_mixindef(node)
|
@@ -171,7 +168,8 @@ class Sass::Tree::Visitors::Convert < Sass::Tree::Visitors::Base
|
|
171
168
|
def visit_mixin(node)
|
172
169
|
unless node.args.empty? && node.keywords.empty?
|
173
170
|
args = node.args.map {|a| a.to_sass(@options)}.join(", ")
|
174
|
-
keywords = node.keywords
|
171
|
+
keywords = Sass::Util.hash_to_a(node.keywords).
|
172
|
+
map {|k, v| "$#{dasherize(k)}: #{v.to_sass(@options)}"}.join(', ')
|
175
173
|
arglist = "(#{args}#{', ' unless args.empty? || keywords.empty?}#{keywords})"
|
176
174
|
end
|
177
175
|
"#{tab_str}#{@format == :sass ? '+' : '@include '}#{dasherize(node.name)}#{arglist}#{node.has_children ? yield : semi}\n"
|
@@ -221,6 +219,13 @@ class Sass::Tree::Visitors::Convert < Sass::Tree::Visitors::Base
|
|
221
219
|
|
222
220
|
private
|
223
221
|
|
222
|
+
def interp_to_src(interp)
|
223
|
+
interp.map do |r|
|
224
|
+
next r if r.is_a?(String)
|
225
|
+
"\#{#{r.to_sass(@options)}}"
|
226
|
+
end.join
|
227
|
+
end
|
228
|
+
|
224
229
|
def selector_to_src(sel)
|
225
230
|
@format == :sass ? selector_to_sass(sel) : selector_to_scss(sel)
|
226
231
|
end
|
@@ -236,8 +241,7 @@ class Sass::Tree::Visitors::Convert < Sass::Tree::Visitors::Base
|
|
236
241
|
end
|
237
242
|
|
238
243
|
def selector_to_scss(sel)
|
239
|
-
sel.
|
240
|
-
join.gsub(/^[ \t]*/, tab_str).gsub(/[ \t]*$/, '')
|
244
|
+
interp_to_src(sel).gsub(/^[ \t]*/, tab_str).gsub(/[ \t]*$/, '')
|
241
245
|
end
|
242
246
|
|
243
247
|
def semi
|