sass4 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.yardopts +13 -0
- data/AGENTS.md +534 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/CONTRIBUTING.md +148 -0
- data/MIT-LICENSE +20 -0
- data/README.md +242 -0
- data/VERSION +1 -0
- data/VERSION_NAME +1 -0
- data/bin/sass +13 -0
- data/bin/sass-convert +12 -0
- data/bin/scss +13 -0
- data/extra/sass-spec-ref.sh +40 -0
- data/extra/update_watch.rb +13 -0
- data/init.rb +18 -0
- data/lib/sass/cache_stores/base.rb +88 -0
- data/lib/sass/cache_stores/chain.rb +34 -0
- data/lib/sass/cache_stores/filesystem.rb +60 -0
- data/lib/sass/cache_stores/memory.rb +46 -0
- data/lib/sass/cache_stores/null.rb +25 -0
- data/lib/sass/cache_stores.rb +15 -0
- data/lib/sass/callbacks.rb +67 -0
- data/lib/sass/css.rb +407 -0
- data/lib/sass/deprecation.rb +55 -0
- data/lib/sass/engine.rb +1236 -0
- data/lib/sass/environment.rb +236 -0
- data/lib/sass/error.rb +198 -0
- data/lib/sass/exec/base.rb +188 -0
- data/lib/sass/exec/sass_convert.rb +283 -0
- data/lib/sass/exec/sass_scss.rb +436 -0
- data/lib/sass/exec.rb +9 -0
- data/lib/sass/features.rb +48 -0
- data/lib/sass/importers/base.rb +182 -0
- data/lib/sass/importers/deprecated_path.rb +51 -0
- data/lib/sass/importers/filesystem.rb +221 -0
- data/lib/sass/importers.rb +23 -0
- data/lib/sass/logger/base.rb +47 -0
- data/lib/sass/logger/delayed.rb +50 -0
- data/lib/sass/logger/log_level.rb +45 -0
- data/lib/sass/logger.rb +17 -0
- data/lib/sass/media.rb +210 -0
- data/lib/sass/plugin/compiler.rb +552 -0
- data/lib/sass/plugin/configuration.rb +134 -0
- data/lib/sass/plugin/generic.rb +15 -0
- data/lib/sass/plugin/merb.rb +48 -0
- data/lib/sass/plugin/rack.rb +60 -0
- data/lib/sass/plugin/rails.rb +47 -0
- data/lib/sass/plugin/staleness_checker.rb +199 -0
- data/lib/sass/plugin.rb +134 -0
- data/lib/sass/railtie.rb +10 -0
- data/lib/sass/repl.rb +57 -0
- data/lib/sass/root.rb +7 -0
- data/lib/sass/script/css_lexer.rb +33 -0
- data/lib/sass/script/css_parser.rb +36 -0
- data/lib/sass/script/functions.rb +3103 -0
- data/lib/sass/script/lexer.rb +518 -0
- data/lib/sass/script/parser.rb +1164 -0
- data/lib/sass/script/tree/funcall.rb +314 -0
- data/lib/sass/script/tree/interpolation.rb +220 -0
- data/lib/sass/script/tree/list_literal.rb +119 -0
- data/lib/sass/script/tree/literal.rb +49 -0
- data/lib/sass/script/tree/map_literal.rb +64 -0
- data/lib/sass/script/tree/node.rb +119 -0
- data/lib/sass/script/tree/operation.rb +149 -0
- data/lib/sass/script/tree/selector.rb +26 -0
- data/lib/sass/script/tree/string_interpolation.rb +125 -0
- data/lib/sass/script/tree/unary_operation.rb +69 -0
- data/lib/sass/script/tree/variable.rb +57 -0
- data/lib/sass/script/tree.rb +16 -0
- data/lib/sass/script/value/arg_list.rb +36 -0
- data/lib/sass/script/value/base.rb +258 -0
- data/lib/sass/script/value/bool.rb +35 -0
- data/lib/sass/script/value/callable.rb +25 -0
- data/lib/sass/script/value/color.rb +704 -0
- data/lib/sass/script/value/function.rb +19 -0
- data/lib/sass/script/value/helpers.rb +298 -0
- data/lib/sass/script/value/list.rb +135 -0
- data/lib/sass/script/value/map.rb +70 -0
- data/lib/sass/script/value/null.rb +44 -0
- data/lib/sass/script/value/number.rb +564 -0
- data/lib/sass/script/value/string.rb +138 -0
- data/lib/sass/script/value.rb +13 -0
- data/lib/sass/script.rb +66 -0
- data/lib/sass/scss/css_parser.rb +61 -0
- data/lib/sass/scss/parser.rb +1343 -0
- data/lib/sass/scss/rx.rb +134 -0
- data/lib/sass/scss/static_parser.rb +351 -0
- data/lib/sass/scss.rb +14 -0
- data/lib/sass/selector/abstract_sequence.rb +112 -0
- data/lib/sass/selector/comma_sequence.rb +195 -0
- data/lib/sass/selector/pseudo.rb +291 -0
- data/lib/sass/selector/sequence.rb +661 -0
- data/lib/sass/selector/simple.rb +124 -0
- data/lib/sass/selector/simple_sequence.rb +348 -0
- data/lib/sass/selector.rb +327 -0
- data/lib/sass/shared.rb +76 -0
- data/lib/sass/source/map.rb +209 -0
- data/lib/sass/source/position.rb +39 -0
- data/lib/sass/source/range.rb +41 -0
- data/lib/sass/stack.rb +140 -0
- data/lib/sass/supports.rb +225 -0
- data/lib/sass/tree/at_root_node.rb +83 -0
- data/lib/sass/tree/charset_node.rb +22 -0
- data/lib/sass/tree/comment_node.rb +82 -0
- data/lib/sass/tree/content_node.rb +9 -0
- data/lib/sass/tree/css_import_node.rb +68 -0
- data/lib/sass/tree/debug_node.rb +18 -0
- data/lib/sass/tree/directive_node.rb +59 -0
- data/lib/sass/tree/each_node.rb +24 -0
- data/lib/sass/tree/error_node.rb +18 -0
- data/lib/sass/tree/extend_node.rb +43 -0
- data/lib/sass/tree/for_node.rb +36 -0
- data/lib/sass/tree/function_node.rb +44 -0
- data/lib/sass/tree/if_node.rb +52 -0
- data/lib/sass/tree/import_node.rb +75 -0
- data/lib/sass/tree/keyframe_rule_node.rb +15 -0
- data/lib/sass/tree/media_node.rb +48 -0
- data/lib/sass/tree/mixin_def_node.rb +38 -0
- data/lib/sass/tree/mixin_node.rb +52 -0
- data/lib/sass/tree/node.rb +240 -0
- data/lib/sass/tree/prop_node.rb +162 -0
- data/lib/sass/tree/return_node.rb +19 -0
- data/lib/sass/tree/root_node.rb +44 -0
- data/lib/sass/tree/rule_node.rb +153 -0
- data/lib/sass/tree/supports_node.rb +38 -0
- data/lib/sass/tree/trace_node.rb +33 -0
- data/lib/sass/tree/variable_node.rb +36 -0
- data/lib/sass/tree/visitors/base.rb +72 -0
- data/lib/sass/tree/visitors/check_nesting.rb +173 -0
- data/lib/sass/tree/visitors/convert.rb +350 -0
- data/lib/sass/tree/visitors/cssize.rb +362 -0
- data/lib/sass/tree/visitors/deep_copy.rb +107 -0
- data/lib/sass/tree/visitors/extend.rb +64 -0
- data/lib/sass/tree/visitors/perform.rb +572 -0
- data/lib/sass/tree/visitors/set_options.rb +139 -0
- data/lib/sass/tree/visitors/to_css.rb +440 -0
- data/lib/sass/tree/warn_node.rb +18 -0
- data/lib/sass/tree/while_node.rb +18 -0
- data/lib/sass/util/multibyte_string_scanner.rb +151 -0
- data/lib/sass/util/normalized_map.rb +122 -0
- data/lib/sass/util/subset_map.rb +109 -0
- data/lib/sass/util/test.rb +9 -0
- data/lib/sass/util.rb +1137 -0
- data/lib/sass/version.rb +120 -0
- data/lib/sass.rb +102 -0
- data/rails/init.rb +1 -0
- metadata +283 -0
@@ -0,0 +1,1164 @@
|
|
1
|
+
require 'sass/script/lexer'
|
2
|
+
|
3
|
+
module Sass
|
4
|
+
module Script
|
5
|
+
# The parser for SassScript.
|
6
|
+
# It parses a string of code into a tree of {Script::Tree::Node}s.
|
7
|
+
class Parser
|
8
|
+
# The line number of the parser's current position.
|
9
|
+
#
|
10
|
+
# @return [Integer]
|
11
|
+
def line
|
12
|
+
@lexer.line
|
13
|
+
end
|
14
|
+
|
15
|
+
# The column number of the parser's current position.
|
16
|
+
#
|
17
|
+
# @return [Integer]
|
18
|
+
def offset
|
19
|
+
@lexer.offset
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param str [String, StringScanner] The source text to parse
|
23
|
+
# @param line [Integer] The line on which the SassScript appears.
|
24
|
+
# Used for error reporting and sourcemap building
|
25
|
+
# @param offset [Integer] The character (not byte) offset where the script starts in the line.
|
26
|
+
# Used for error reporting and sourcemap building
|
27
|
+
# @param options [{Symbol => Object}] An options hash; see
|
28
|
+
# {file:SASS_REFERENCE.md#Options the Sass options documentation}.
|
29
|
+
# This supports an additional `:allow_extra_text` option that controls
|
30
|
+
# whether the parser throws an error when extra text is encountered
|
31
|
+
# after the parsed construct.
|
32
|
+
def initialize(str, line, offset, options = {})
|
33
|
+
@options = options
|
34
|
+
@allow_extra_text = options.delete(:allow_extra_text)
|
35
|
+
@lexer = lexer_class.new(str, line, offset, options)
|
36
|
+
@stop_at = nil
|
37
|
+
end
|
38
|
+
|
39
|
+
# Parses a SassScript expression within an interpolated segment (`#{}`).
|
40
|
+
# This means that it stops when it comes across an unmatched `}`,
|
41
|
+
# which signals the end of an interpolated segment,
|
42
|
+
# it returns rather than throwing an error.
|
43
|
+
#
|
44
|
+
# @param warn_for_color [Boolean] Whether raw color values passed to
|
45
|
+
# interoplation should cause a warning.
|
46
|
+
# @return [Script::Tree::Node] The root node of the parse tree
|
47
|
+
# @raise [Sass::SyntaxError] if the expression isn't valid SassScript
|
48
|
+
def parse_interpolated(warn_for_color = false)
|
49
|
+
# Start two characters back to compensate for #{
|
50
|
+
start_pos = Sass::Source::Position.new(line, offset - 2)
|
51
|
+
expr = assert_expr :expr
|
52
|
+
assert_tok :end_interpolation
|
53
|
+
expr = Sass::Script::Tree::Interpolation.new(
|
54
|
+
nil, expr, nil, false, false, :warn_for_color => warn_for_color)
|
55
|
+
check_for_interpolation expr
|
56
|
+
expr.options = @options
|
57
|
+
node(expr, start_pos)
|
58
|
+
rescue Sass::SyntaxError => e
|
59
|
+
e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
|
60
|
+
raise e
|
61
|
+
end
|
62
|
+
|
63
|
+
# Parses a SassScript expression.
|
64
|
+
#
|
65
|
+
# @return [Script::Tree::Node] The root node of the parse tree
|
66
|
+
# @raise [Sass::SyntaxError] if the expression isn't valid SassScript
|
67
|
+
def parse
|
68
|
+
expr = assert_expr :expr
|
69
|
+
assert_done
|
70
|
+
expr.options = @options
|
71
|
+
check_for_interpolation expr
|
72
|
+
expr
|
73
|
+
rescue Sass::SyntaxError => e
|
74
|
+
e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
|
75
|
+
raise e
|
76
|
+
end
|
77
|
+
|
78
|
+
# Parses a SassScript expression,
|
79
|
+
# ending it when it encounters one of the given identifier tokens.
|
80
|
+
#
|
81
|
+
# @param tokens [#include?(String | Symbol)] A set of strings or symbols that delimit the expression.
|
82
|
+
# @return [Script::Tree::Node] The root node of the parse tree
|
83
|
+
# @raise [Sass::SyntaxError] if the expression isn't valid SassScript
|
84
|
+
def parse_until(tokens)
|
85
|
+
@stop_at = tokens
|
86
|
+
expr = assert_expr :expr
|
87
|
+
assert_done
|
88
|
+
expr.options = @options
|
89
|
+
check_for_interpolation expr
|
90
|
+
expr
|
91
|
+
rescue Sass::SyntaxError => e
|
92
|
+
e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
|
93
|
+
raise e
|
94
|
+
end
|
95
|
+
|
96
|
+
# Parses the argument list for a mixin include.
|
97
|
+
#
|
98
|
+
# @return [(Array<Script::Tree::Node>,
|
99
|
+
# {String => Script::Tree::Node},
|
100
|
+
# Script::Tree::Node,
|
101
|
+
# Script::Tree::Node)]
|
102
|
+
# The root nodes of the positional arguments, keyword arguments, and
|
103
|
+
# splat argument(s). Keyword arguments are in a hash from names to values.
|
104
|
+
# @raise [Sass::SyntaxError] if the argument list isn't valid SassScript
|
105
|
+
def parse_mixin_include_arglist
|
106
|
+
args, keywords = [], {}
|
107
|
+
if try_tok(:lparen)
|
108
|
+
args, keywords, splat, kwarg_splat = mixin_arglist
|
109
|
+
assert_tok(:rparen)
|
110
|
+
end
|
111
|
+
assert_done
|
112
|
+
|
113
|
+
args.each do |a|
|
114
|
+
check_for_interpolation a
|
115
|
+
a.options = @options
|
116
|
+
end
|
117
|
+
|
118
|
+
keywords.each do |_, v|
|
119
|
+
check_for_interpolation v
|
120
|
+
v.options = @options
|
121
|
+
end
|
122
|
+
|
123
|
+
if splat
|
124
|
+
check_for_interpolation splat
|
125
|
+
splat.options = @options
|
126
|
+
end
|
127
|
+
|
128
|
+
if kwarg_splat
|
129
|
+
check_for_interpolation kwarg_splat
|
130
|
+
kwarg_splat.options = @options
|
131
|
+
end
|
132
|
+
|
133
|
+
return args, keywords, splat, kwarg_splat
|
134
|
+
rescue Sass::SyntaxError => e
|
135
|
+
e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
|
136
|
+
raise e
|
137
|
+
end
|
138
|
+
|
139
|
+
# Parses the argument list for a mixin definition.
|
140
|
+
#
|
141
|
+
# @return [(Array<Script::Tree::Node>, Script::Tree::Node)]
|
142
|
+
# The root nodes of the arguments, and the splat argument.
|
143
|
+
# @raise [Sass::SyntaxError] if the argument list isn't valid SassScript
|
144
|
+
def parse_mixin_definition_arglist
|
145
|
+
args, splat = defn_arglist!(false)
|
146
|
+
assert_done
|
147
|
+
|
148
|
+
args.each do |k, v|
|
149
|
+
check_for_interpolation k
|
150
|
+
k.options = @options
|
151
|
+
|
152
|
+
if v
|
153
|
+
check_for_interpolation v
|
154
|
+
v.options = @options
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
if splat
|
159
|
+
check_for_interpolation splat
|
160
|
+
splat.options = @options
|
161
|
+
end
|
162
|
+
|
163
|
+
return args, splat
|
164
|
+
rescue Sass::SyntaxError => e
|
165
|
+
e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
|
166
|
+
raise e
|
167
|
+
end
|
168
|
+
|
169
|
+
# Parses the argument list for a function definition.
|
170
|
+
#
|
171
|
+
# @return [(Array<Script::Tree::Node>, Script::Tree::Node)]
|
172
|
+
# The root nodes of the arguments, and the splat argument.
|
173
|
+
# @raise [Sass::SyntaxError] if the argument list isn't valid SassScript
|
174
|
+
def parse_function_definition_arglist
|
175
|
+
args, splat = defn_arglist!(true)
|
176
|
+
assert_done
|
177
|
+
|
178
|
+
args.each do |k, v|
|
179
|
+
check_for_interpolation k
|
180
|
+
k.options = @options
|
181
|
+
|
182
|
+
if v
|
183
|
+
check_for_interpolation v
|
184
|
+
v.options = @options
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
if splat
|
189
|
+
check_for_interpolation splat
|
190
|
+
splat.options = @options
|
191
|
+
end
|
192
|
+
|
193
|
+
return args, splat
|
194
|
+
rescue Sass::SyntaxError => e
|
195
|
+
e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
|
196
|
+
raise e
|
197
|
+
end
|
198
|
+
|
199
|
+
# Parse a single string value, possibly containing interpolation.
|
200
|
+
# Doesn't assert that the scanner is finished after parsing.
|
201
|
+
#
|
202
|
+
# @return [Script::Tree::Node] The root node of the parse tree.
|
203
|
+
# @raise [Sass::SyntaxError] if the string isn't valid SassScript
|
204
|
+
def parse_string
|
205
|
+
unless (peek = @lexer.peek) &&
|
206
|
+
(peek.type == :string ||
|
207
|
+
(peek.type == :funcall && peek.value.downcase == 'url'))
|
208
|
+
lexer.expected!("string")
|
209
|
+
end
|
210
|
+
|
211
|
+
expr = assert_expr :funcall
|
212
|
+
check_for_interpolation expr
|
213
|
+
expr.options = @options
|
214
|
+
@lexer.unpeek!
|
215
|
+
expr
|
216
|
+
rescue Sass::SyntaxError => e
|
217
|
+
e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
|
218
|
+
raise e
|
219
|
+
end
|
220
|
+
|
221
|
+
# Parses a SassScript expression.
|
222
|
+
#
|
223
|
+
# @overload parse(str, line, offset, filename = nil)
|
224
|
+
# @return [Script::Tree::Node] The root node of the parse tree
|
225
|
+
# @see Parser#initialize
|
226
|
+
# @see Parser#parse
|
227
|
+
def self.parse(*args)
|
228
|
+
new(*args).parse
|
229
|
+
end
|
230
|
+
|
231
|
+
PRECEDENCE = [
|
232
|
+
:comma, :single_eq, :space, :slash, :or, :and,
|
233
|
+
[:eq, :neq],
|
234
|
+
[:gt, :gte, :lt, :lte],
|
235
|
+
[:plus, :minus],
|
236
|
+
[:times, :div, :mod],
|
237
|
+
]
|
238
|
+
|
239
|
+
ASSOCIATIVE = [:plus, :times]
|
240
|
+
|
241
|
+
class << self
|
242
|
+
# Returns an integer representing the precedence
|
243
|
+
# of the given operator.
|
244
|
+
# A lower integer indicates a looser binding.
|
245
|
+
#
|
246
|
+
# @private
|
247
|
+
def precedence_of(op)
|
248
|
+
PRECEDENCE.each_with_index do |e, i|
|
249
|
+
return i if Array(e).include?(op)
|
250
|
+
end
|
251
|
+
raise "[BUG] Unknown operator #{op.inspect}"
|
252
|
+
end
|
253
|
+
|
254
|
+
# Returns whether or not the given operation is associative.
|
255
|
+
#
|
256
|
+
# @private
|
257
|
+
def associative?(op)
|
258
|
+
ASSOCIATIVE.include?(op)
|
259
|
+
end
|
260
|
+
|
261
|
+
private
|
262
|
+
|
263
|
+
# Defines a simple left-associative production.
|
264
|
+
# name is the name of the production,
|
265
|
+
# sub is the name of the production beneath it,
|
266
|
+
# and ops is a list of operators for this precedence level
|
267
|
+
def production(name, sub, *ops)
|
268
|
+
class_eval <<RUBY, __FILE__, __LINE__ + 1
|
269
|
+
def #{name}
|
270
|
+
interp = try_ops_after_interp(#{ops.inspect}, #{name.inspect})
|
271
|
+
return interp if interp
|
272
|
+
return unless e = #{sub}
|
273
|
+
|
274
|
+
while tok = peek_toks(#{ops.map {|o| o.inspect}.join(', ')})
|
275
|
+
return e if @stop_at && @stop_at.include?(tok.type)
|
276
|
+
@lexer.next
|
277
|
+
|
278
|
+
if interp = try_op_before_interp(tok, e)
|
279
|
+
other_interp = try_ops_after_interp(#{ops.inspect}, #{name.inspect}, interp)
|
280
|
+
return interp unless other_interp
|
281
|
+
return other_interp
|
282
|
+
end
|
283
|
+
|
284
|
+
e = node(Tree::Operation.new(e, assert_expr(#{sub.inspect}), tok.type),
|
285
|
+
e.source_range.start_pos)
|
286
|
+
end
|
287
|
+
e
|
288
|
+
end
|
289
|
+
RUBY
|
290
|
+
end
|
291
|
+
|
292
|
+
def unary(op, sub)
|
293
|
+
class_eval <<RUBY, __FILE__, __LINE__ + 1
|
294
|
+
def unary_#{op}
|
295
|
+
return #{sub} unless tok = try_tok(:#{op})
|
296
|
+
interp = try_op_before_interp(tok)
|
297
|
+
return interp if interp
|
298
|
+
start_pos = source_position
|
299
|
+
node(Tree::UnaryOperation.new(assert_expr(:unary_#{op}), :#{op}), start_pos)
|
300
|
+
end
|
301
|
+
RUBY
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
private
|
306
|
+
|
307
|
+
def source_position
|
308
|
+
Sass::Source::Position.new(line, offset)
|
309
|
+
end
|
310
|
+
|
311
|
+
def range(start_pos, end_pos = source_position)
|
312
|
+
Sass::Source::Range.new(start_pos, end_pos, @options[:filename], @options[:importer])
|
313
|
+
end
|
314
|
+
|
315
|
+
# @private
|
316
|
+
def lexer_class; Lexer; end
|
317
|
+
|
318
|
+
def map
|
319
|
+
start_pos = source_position
|
320
|
+
e = interpolation
|
321
|
+
return unless e
|
322
|
+
return list e, start_pos unless @lexer.peek && @lexer.peek.type == :colon
|
323
|
+
|
324
|
+
pair = map_pair(e)
|
325
|
+
map = node(Sass::Script::Tree::MapLiteral.new([pair]), start_pos)
|
326
|
+
while try_tok(:comma)
|
327
|
+
pair = map_pair
|
328
|
+
return map unless pair
|
329
|
+
map.pairs << pair
|
330
|
+
map.source_range.end_pos = map.pairs.last.last.source_range.end_pos
|
331
|
+
end
|
332
|
+
map
|
333
|
+
end
|
334
|
+
|
335
|
+
def map_pair(key = nil)
|
336
|
+
return unless key ||= interpolation
|
337
|
+
assert_tok :colon
|
338
|
+
return key, assert_expr(:interpolation)
|
339
|
+
end
|
340
|
+
|
341
|
+
def expr
|
342
|
+
start_pos = source_position
|
343
|
+
e = interpolation
|
344
|
+
return unless e
|
345
|
+
list e, start_pos
|
346
|
+
end
|
347
|
+
|
348
|
+
def list(first, start_pos)
|
349
|
+
return first unless @lexer.peek && @lexer.peek.type == :comma
|
350
|
+
|
351
|
+
list = node(Sass::Script::Tree::ListLiteral.new([first], separator: :comma), start_pos)
|
352
|
+
while (tok = try_tok(:comma))
|
353
|
+
element_before_interp = list.elements.length == 1 ? list.elements.first : list
|
354
|
+
if (interp = try_op_before_interp(tok, element_before_interp))
|
355
|
+
other_interp = try_ops_after_interp([:comma], :expr, interp)
|
356
|
+
return interp unless other_interp
|
357
|
+
return other_interp
|
358
|
+
end
|
359
|
+
return list unless (e = interpolation)
|
360
|
+
list.elements << e
|
361
|
+
list.source_range.end_pos = list.elements.last.source_range.end_pos
|
362
|
+
end
|
363
|
+
list
|
364
|
+
end
|
365
|
+
|
366
|
+
production :equals, :interpolation, :single_eq
|
367
|
+
|
368
|
+
def try_op_before_interp(op, prev = nil, after_interp = false)
|
369
|
+
return unless @lexer.peek && @lexer.peek.type == :begin_interpolation
|
370
|
+
unary = !prev && !after_interp
|
371
|
+
wb = @lexer.whitespace?(op)
|
372
|
+
str = literal_node(Script::Value::String.new(Lexer::OPERATORS_REVERSE[op.type]),
|
373
|
+
op.source_range)
|
374
|
+
|
375
|
+
deprecation =
|
376
|
+
case op.type
|
377
|
+
when :comma; :potential
|
378
|
+
when :div, :single_eq; :none
|
379
|
+
when :plus; unary ? :none : :immediate
|
380
|
+
when :minus; @lexer.whitespace?(@lexer.peek) ? :immediate : :none
|
381
|
+
else; :immediate
|
382
|
+
end
|
383
|
+
|
384
|
+
interp = node(
|
385
|
+
Script::Tree::Interpolation.new(
|
386
|
+
prev, str, nil, wb, false, :originally_text => true, :deprecation => deprecation),
|
387
|
+
(prev || str).source_range.start_pos)
|
388
|
+
interpolation(first: interp)
|
389
|
+
end
|
390
|
+
|
391
|
+
def try_ops_after_interp(ops, name, prev = nil)
|
392
|
+
return unless @lexer.after_interpolation?
|
393
|
+
op = peek_toks(*ops)
|
394
|
+
return unless op
|
395
|
+
return if @stop_at && @stop_at.include?(op.type)
|
396
|
+
@lexer.next
|
397
|
+
|
398
|
+
interp = try_op_before_interp(op, prev, :after_interp)
|
399
|
+
return interp if interp
|
400
|
+
|
401
|
+
wa = @lexer.whitespace?
|
402
|
+
str = literal_node(Script::Value::String.new(Lexer::OPERATORS_REVERSE[op.type]),
|
403
|
+
op.source_range)
|
404
|
+
str.line = @lexer.line
|
405
|
+
|
406
|
+
deprecation =
|
407
|
+
case op.type
|
408
|
+
when :comma; :potential
|
409
|
+
when :div, :single_eq; :none
|
410
|
+
when :minus; @lexer.whitespace?(op) ? :immediate : :none
|
411
|
+
else; :immediate
|
412
|
+
end
|
413
|
+
interp = node(
|
414
|
+
Script::Tree::Interpolation.new(
|
415
|
+
prev, str, assert_expr(name), false, wa,
|
416
|
+
:originally_text => true, :deprecation => deprecation),
|
417
|
+
(prev || str).source_range.start_pos)
|
418
|
+
interp
|
419
|
+
end
|
420
|
+
|
421
|
+
def interpolation(first: nil, inner: :space)
|
422
|
+
e = first || send(inner)
|
423
|
+
while (interp = try_tok(:begin_interpolation))
|
424
|
+
wb = @lexer.whitespace?(interp)
|
425
|
+
char_before = @lexer.char(interp.pos - 1)
|
426
|
+
mid = without_stop_at {assert_expr :expr}
|
427
|
+
assert_tok :end_interpolation
|
428
|
+
wa = @lexer.whitespace?
|
429
|
+
char_after = @lexer.char
|
430
|
+
|
431
|
+
after = send(inner)
|
432
|
+
before_deprecation = e.is_a?(Script::Tree::Interpolation) ? e.deprecation : :none
|
433
|
+
after_deprecation = after.is_a?(Script::Tree::Interpolation) ? after.deprecation : :none
|
434
|
+
|
435
|
+
deprecation =
|
436
|
+
if before_deprecation == :immediate || after_deprecation == :immediate ||
|
437
|
+
# Warn for #{foo}$var and #{foo}(1) but not #{$foo}1.
|
438
|
+
(after && !wa && char_after =~ /[$(]/) ||
|
439
|
+
# Warn for $var#{foo} and (a)#{foo} but not a#{foo}.
|
440
|
+
(e && !wb && is_unsafe_before?(e, char_before))
|
441
|
+
:immediate
|
442
|
+
else
|
443
|
+
:potential
|
444
|
+
end
|
445
|
+
|
446
|
+
e = node(
|
447
|
+
Script::Tree::Interpolation.new(e, mid, after, wb, wa, :deprecation => deprecation),
|
448
|
+
(e || interp).source_range.start_pos)
|
449
|
+
end
|
450
|
+
e
|
451
|
+
end
|
452
|
+
|
453
|
+
# Returns whether `expr` is unsafe to include before an interpolation.
|
454
|
+
#
|
455
|
+
# @param expr [Node] The expression to check.
|
456
|
+
# @param char_before [String] The character immediately before the
|
457
|
+
# interpolation being checked (and presumably the last character of
|
458
|
+
# `expr`).
|
459
|
+
# @return [Boolean]
|
460
|
+
def is_unsafe_before?(expr, char_before)
|
461
|
+
return char_before == ')' if is_safe_value?(expr)
|
462
|
+
|
463
|
+
# Otherwise, it's only safe if it was another interpolation.
|
464
|
+
!expr.is_a?(Script::Tree::Interpolation)
|
465
|
+
end
|
466
|
+
|
467
|
+
# Returns whether `expr` is safe as the value immediately before an
|
468
|
+
# interpolation.
|
469
|
+
#
|
470
|
+
# It's safe as long as the previous expression is an identifier or number,
|
471
|
+
# or a list whose last element is also safe.
|
472
|
+
def is_safe_value?(expr)
|
473
|
+
return is_safe_value?(expr.elements.last) if expr.is_a?(Script::Tree::ListLiteral)
|
474
|
+
return false unless expr.is_a?(Script::Tree::Literal)
|
475
|
+
expr.value.is_a?(Script::Value::Number) ||
|
476
|
+
(expr.value.is_a?(Script::Value::String) && expr.value.type == :identifier)
|
477
|
+
end
|
478
|
+
|
479
|
+
def space
|
480
|
+
start_pos = source_position
|
481
|
+
e = or_expr
|
482
|
+
return unless e
|
483
|
+
arr = [e]
|
484
|
+
while (e = or_expr)
|
485
|
+
arr << e
|
486
|
+
end
|
487
|
+
if arr.size == 1
|
488
|
+
arr.first
|
489
|
+
else
|
490
|
+
node(Sass::Script::Tree::ListLiteral.new(arr, separator: :space), start_pos)
|
491
|
+
end
|
492
|
+
end
|
493
|
+
|
494
|
+
production :or_expr, :and_expr, :or
|
495
|
+
production :and_expr, :eq_or_neq, :and
|
496
|
+
production :eq_or_neq, :relational, :eq, :neq
|
497
|
+
production :relational, :plus_or_minus, :gt, :gte, :lt, :lte
|
498
|
+
production :plus_or_minus, :times_div_or_mod, :plus, :minus
|
499
|
+
production :times_div_or_mod, :unary_plus, :times, :div, :mod
|
500
|
+
|
501
|
+
unary :plus, :unary_minus
|
502
|
+
unary :minus, :unary_div
|
503
|
+
unary :div, :unary_not # For strings, so /foo/bar works
|
504
|
+
unary :not, :ident
|
505
|
+
|
506
|
+
def ident
|
507
|
+
return css_min_max unless @lexer.peek && @lexer.peek.type == :ident
|
508
|
+
return if @stop_at && @stop_at.include?(@lexer.peek.value)
|
509
|
+
|
510
|
+
name = @lexer.next
|
511
|
+
if (color = Sass::Script::Value::Color::COLOR_NAMES[name.value.downcase])
|
512
|
+
literal_node(Sass::Script::Value::Color.new(color, name.value), name.source_range)
|
513
|
+
elsif name.value == "true"
|
514
|
+
literal_node(Sass::Script::Value::Bool.new(true), name.source_range)
|
515
|
+
elsif name.value == "false"
|
516
|
+
literal_node(Sass::Script::Value::Bool.new(false), name.source_range)
|
517
|
+
elsif name.value == "null"
|
518
|
+
literal_node(Sass::Script::Value::Null.new, name.source_range)
|
519
|
+
else
|
520
|
+
literal_node(Sass::Script::Value::String.new(name.value, :identifier), name.source_range)
|
521
|
+
end
|
522
|
+
end
|
523
|
+
|
524
|
+
def css_min_max
|
525
|
+
@lexer.try do
|
526
|
+
next unless tok = try_tok(:funcall)
|
527
|
+
next unless %w[min max].include?(tok.value.downcase)
|
528
|
+
next unless contents = min_max_contents
|
529
|
+
node(array_to_interpolation(["#{tok.value}(", *contents]),
|
530
|
+
tok.source_range.start_pos, source_position)
|
531
|
+
end || funcall
|
532
|
+
end
|
533
|
+
|
534
|
+
def min_max_contents(allow_comma: true)
|
535
|
+
result = []
|
536
|
+
loop do
|
537
|
+
if tok = try_tok(:number)
|
538
|
+
result << tok.value.to_s
|
539
|
+
elsif value = min_max_interpolation
|
540
|
+
result << value
|
541
|
+
elsif value = min_max_calc
|
542
|
+
result << value.value
|
543
|
+
elsif value = min_max_function ||
|
544
|
+
min_max_parens ||
|
545
|
+
nested_min_max
|
546
|
+
result.concat value
|
547
|
+
else
|
548
|
+
return
|
549
|
+
end
|
550
|
+
|
551
|
+
if try_tok(:rparen)
|
552
|
+
result << ")"
|
553
|
+
return result
|
554
|
+
elsif tok = try_tok(:plus) || try_tok(:minus) || try_tok(:times) || try_tok(:div)
|
555
|
+
result << " #{Lexer::OPERATORS_REVERSE[tok.type]} "
|
556
|
+
elsif allow_comma && try_tok(:comma)
|
557
|
+
result << ", "
|
558
|
+
else
|
559
|
+
return
|
560
|
+
end
|
561
|
+
end
|
562
|
+
end
|
563
|
+
|
564
|
+
def min_max_interpolation
|
565
|
+
without_stop_at do
|
566
|
+
tok = try_tok(:begin_interpolation)
|
567
|
+
return unless tok
|
568
|
+
expr = without_stop_at {assert_expr :expr}
|
569
|
+
assert_tok :end_interpolation
|
570
|
+
expr
|
571
|
+
end
|
572
|
+
end
|
573
|
+
|
574
|
+
def min_max_function
|
575
|
+
return unless tok = peek_tok(:funcall)
|
576
|
+
return unless %w[calc env var].include?(tok.value.downcase)
|
577
|
+
@lexer.next
|
578
|
+
result = [tok.value, '(', *declaration_value, ')']
|
579
|
+
assert_tok :rparen
|
580
|
+
result
|
581
|
+
end
|
582
|
+
|
583
|
+
def min_max_calc
|
584
|
+
return unless tok = peek_tok(:special_fun)
|
585
|
+
return unless tok.value.value.downcase.start_with?("calc(")
|
586
|
+
@lexer.next.value
|
587
|
+
end
|
588
|
+
|
589
|
+
def min_max_parens
|
590
|
+
return unless try_tok :lparen
|
591
|
+
return unless contents = min_max_contents(allow_comma: false)
|
592
|
+
['(', *contents]
|
593
|
+
end
|
594
|
+
|
595
|
+
def nested_min_max
|
596
|
+
return unless tok = peek_tok(:funcall)
|
597
|
+
return unless %w[min max].include?(tok.value.downcase)
|
598
|
+
@lexer.next
|
599
|
+
return unless contents = min_max_contents
|
600
|
+
[tok.value, '(', *contents]
|
601
|
+
end
|
602
|
+
|
603
|
+
def declaration_value
|
604
|
+
result = []
|
605
|
+
brackets = []
|
606
|
+
loop do
|
607
|
+
result << @lexer.str do
|
608
|
+
until @lexer.done? ||
|
609
|
+
peek_toks(:begin_interpolation,
|
610
|
+
:end_interpolation,
|
611
|
+
:lcurly,
|
612
|
+
:lparen,
|
613
|
+
:lsquare,
|
614
|
+
:rparen,
|
615
|
+
:rsquare)
|
616
|
+
@lexer.next || @lexer.next_char
|
617
|
+
end
|
618
|
+
end
|
619
|
+
|
620
|
+
if try_tok(:begin_interpolation)
|
621
|
+
result << assert_expr(:expr)
|
622
|
+
assert_tok :end_interpolation
|
623
|
+
elsif tok = try_toks(:lcurly, :lparen, :lsquare)
|
624
|
+
brackets << case tok.type
|
625
|
+
when :lcurly; :end_interpolation
|
626
|
+
when :lparen; :rparen
|
627
|
+
when :lsquare; :rsquare
|
628
|
+
end
|
629
|
+
result << Lexer::OPERATORS_REVERSE[tok.type]
|
630
|
+
elsif brackets.empty?
|
631
|
+
return result
|
632
|
+
else
|
633
|
+
bracket = brackets.pop
|
634
|
+
assert_tok bracket
|
635
|
+
result << Lexer::OPERATORS_REVERSE[bracket]
|
636
|
+
end
|
637
|
+
end
|
638
|
+
end
|
639
|
+
|
640
|
+
def funcall
|
641
|
+
tok = try_tok(:funcall)
|
642
|
+
return raw unless tok
|
643
|
+
|
644
|
+
# Special handling for CSS Color Level 4 functions
|
645
|
+
if %w[rgb rgba hsl hsla].include?(tok.value.downcase)
|
646
|
+
args, keywords, splat, kwarg_splat = color_fn_arglist
|
647
|
+
else
|
648
|
+
args, keywords, splat, kwarg_splat = fn_arglist
|
649
|
+
end
|
650
|
+
|
651
|
+
assert_tok(:rparen)
|
652
|
+
node(Script::Tree::Funcall.new(tok.value, args, keywords, splat, kwarg_splat),
|
653
|
+
tok.source_range.start_pos, source_position)
|
654
|
+
end
|
655
|
+
|
656
|
+
def defn_arglist!(must_have_parens)
|
657
|
+
if must_have_parens
|
658
|
+
assert_tok(:lparen)
|
659
|
+
else
|
660
|
+
return [], nil unless try_tok(:lparen)
|
661
|
+
end
|
662
|
+
|
663
|
+
without_stop_at do
|
664
|
+
res = []
|
665
|
+
splat = nil
|
666
|
+
must_have_default = false
|
667
|
+
loop do
|
668
|
+
break if peek_tok(:rparen)
|
669
|
+
c = assert_tok(:const)
|
670
|
+
var = node(Script::Tree::Variable.new(c.value), c.source_range)
|
671
|
+
if try_tok(:colon)
|
672
|
+
val = assert_expr(:space)
|
673
|
+
must_have_default = true
|
674
|
+
elsif try_tok(:splat)
|
675
|
+
splat = var
|
676
|
+
break
|
677
|
+
elsif must_have_default
|
678
|
+
raise SyntaxError.new(
|
679
|
+
"Required argument #{var.inspect} must come before any optional arguments.")
|
680
|
+
end
|
681
|
+
res << [var, val]
|
682
|
+
break unless try_tok(:comma)
|
683
|
+
end
|
684
|
+
assert_tok(:rparen)
|
685
|
+
return res, splat
|
686
|
+
end
|
687
|
+
end
|
688
|
+
|
689
|
+
def fn_arglist
|
690
|
+
arglist(:equals, "function argument")
|
691
|
+
end
|
692
|
+
|
693
|
+
# Special arglist parser for CSS Color Level 4 syntax
|
694
|
+
# Handles: rgb(255 0 0), rgb(255 0 0 / 0.5), rgb(255, 0, 0), rgb($r: 255, $g: 0, $b: 0)
|
695
|
+
def color_fn_arglist
|
696
|
+
without_stop_at do
|
697
|
+
args = []
|
698
|
+
keywords = Sass::Util::NormalizedMap.new
|
699
|
+
splat = nil
|
700
|
+
|
701
|
+
# Parse first argument using unary_plus to avoid division interpretation
|
702
|
+
e = unary_plus
|
703
|
+
return [args, keywords, splat] unless e
|
704
|
+
|
705
|
+
# Check what follows
|
706
|
+
peek = @lexer.peek
|
707
|
+
|
708
|
+
# Keyword argument: $red: 255
|
709
|
+
if peek && peek.type == :colon
|
710
|
+
name = e
|
711
|
+
@lexer.expected!("comma") unless name.is_a?(Tree::Variable)
|
712
|
+
assert_tok(:colon)
|
713
|
+
value = assert_expr(:equals, "function argument")
|
714
|
+
keywords[name.name] = value
|
715
|
+
|
716
|
+
# Continue parsing keyword arguments
|
717
|
+
while try_tok(:comma)
|
718
|
+
e = send(:equals)
|
719
|
+
break unless e
|
720
|
+
|
721
|
+
if @lexer.peek && @lexer.peek.type == :colon
|
722
|
+
name = e
|
723
|
+
@lexer.expected!("comma") unless name.is_a?(Tree::Variable)
|
724
|
+
assert_tok(:colon)
|
725
|
+
value = assert_expr(:equals, "function argument")
|
726
|
+
|
727
|
+
if keywords[name.name]
|
728
|
+
raise SyntaxError.new("Keyword argument \"#{name.to_sass}\" passed more than once")
|
729
|
+
end
|
730
|
+
|
731
|
+
keywords[name.name] = value
|
732
|
+
else
|
733
|
+
# Mixed positional and keyword - error will be raised by function
|
734
|
+
raise SyntaxError.new("Positional arguments must come before keyword arguments.")
|
735
|
+
end
|
736
|
+
end
|
737
|
+
|
738
|
+
return [args, keywords, splat]
|
739
|
+
end
|
740
|
+
|
741
|
+
# Comma-separated (legacy syntax): 255, 0, 0
|
742
|
+
if peek && peek.type == :comma
|
743
|
+
args << e
|
744
|
+
|
745
|
+
while try_tok(:comma)
|
746
|
+
e = send(:equals)
|
747
|
+
break unless e
|
748
|
+
|
749
|
+
# Check for keyword argument after positional
|
750
|
+
if @lexer.peek && @lexer.peek.type == :colon
|
751
|
+
name = e
|
752
|
+
@lexer.expected!("comma") unless name.is_a?(Tree::Variable)
|
753
|
+
assert_tok(:colon)
|
754
|
+
value = assert_expr(:equals, "function argument")
|
755
|
+
keywords[name.name] = value
|
756
|
+
|
757
|
+
# Continue parsing remaining keyword arguments
|
758
|
+
while try_tok(:comma)
|
759
|
+
e = send(:equals)
|
760
|
+
break unless e
|
761
|
+
|
762
|
+
if @lexer.peek && @lexer.peek.type == :colon
|
763
|
+
name = e
|
764
|
+
@lexer.expected!("comma") unless name.is_a?(Tree::Variable)
|
765
|
+
assert_tok(:colon)
|
766
|
+
value = assert_expr(:equals, "function argument")
|
767
|
+
|
768
|
+
if keywords[name.name]
|
769
|
+
raise SyntaxError.new("Keyword argument \"#{name.to_sass}\" passed more than once")
|
770
|
+
end
|
771
|
+
|
772
|
+
keywords[name.name] = value
|
773
|
+
else
|
774
|
+
raise SyntaxError.new("Positional arguments must come before keyword arguments.")
|
775
|
+
end
|
776
|
+
end
|
777
|
+
break
|
778
|
+
end
|
779
|
+
|
780
|
+
args << e
|
781
|
+
end
|
782
|
+
|
783
|
+
return [args, keywords, splat]
|
784
|
+
end
|
785
|
+
|
786
|
+
# Space-separated syntax (CSS Color Level 4): 255 0 0 / 0.5
|
787
|
+
start_pos = (e.source_range rescue nil)&.start_pos || source_position
|
788
|
+
values = [e]
|
789
|
+
|
790
|
+
# Collect space-separated values
|
791
|
+
loop do
|
792
|
+
peek = @lexer.peek
|
793
|
+
break if !peek || peek.type == :rparen
|
794
|
+
|
795
|
+
# Handle slash separator
|
796
|
+
if peek.type == :div
|
797
|
+
@lexer.next # consume /
|
798
|
+
|
799
|
+
# Parse alpha value
|
800
|
+
alpha = unary_plus
|
801
|
+
if alpha
|
802
|
+
# Build slash-separated list for alpha
|
803
|
+
alpha_list = node(
|
804
|
+
Sass::Script::Tree::ListLiteral.new([alpha], separator: :slash),
|
805
|
+
alpha.source_range.start_pos
|
806
|
+
)
|
807
|
+
|
808
|
+
# Build space-separated list for RGB/HSL values
|
809
|
+
rgb_list = if values.length == 1
|
810
|
+
values.first
|
811
|
+
else
|
812
|
+
node(
|
813
|
+
Sass::Script::Tree::ListLiteral.new(values, separator: :space),
|
814
|
+
start_pos
|
815
|
+
)
|
816
|
+
end
|
817
|
+
|
818
|
+
# Build outer space-separated list: (rgb alpha)
|
819
|
+
result = node(
|
820
|
+
Sass::Script::Tree::ListLiteral.new([rgb_list, alpha_list], separator: :space),
|
821
|
+
start_pos
|
822
|
+
)
|
823
|
+
|
824
|
+
return [[result], keywords, splat]
|
825
|
+
end
|
826
|
+
break
|
827
|
+
end
|
828
|
+
|
829
|
+
# Parse next value
|
830
|
+
val = unary_plus
|
831
|
+
break unless val
|
832
|
+
values << val
|
833
|
+
end
|
834
|
+
|
835
|
+
# Return space-separated values
|
836
|
+
result = if values.length == 1
|
837
|
+
values.first
|
838
|
+
else
|
839
|
+
node(
|
840
|
+
Sass::Script::Tree::ListLiteral.new(values, separator: :space),
|
841
|
+
start_pos
|
842
|
+
)
|
843
|
+
end
|
844
|
+
|
845
|
+
[[result], keywords, splat]
|
846
|
+
end
|
847
|
+
end
|
848
|
+
|
849
|
+
def mixin_arglist
|
850
|
+
arglist(:interpolation, "mixin argument")
|
851
|
+
end
|
852
|
+
|
853
|
+
def arglist(subexpr, description)
|
854
|
+
without_stop_at do
|
855
|
+
args = []
|
856
|
+
keywords = Sass::Util::NormalizedMap.new
|
857
|
+
splat = nil
|
858
|
+
while (e = send(subexpr))
|
859
|
+
if @lexer.peek && @lexer.peek.type == :colon
|
860
|
+
name = e
|
861
|
+
@lexer.expected!("comma") unless name.is_a?(Tree::Variable)
|
862
|
+
assert_tok(:colon)
|
863
|
+
value = assert_expr(subexpr, description)
|
864
|
+
|
865
|
+
if keywords[name.name]
|
866
|
+
raise SyntaxError.new("Keyword argument \"#{name.to_sass}\" passed more than once")
|
867
|
+
end
|
868
|
+
|
869
|
+
keywords[name.name] = value
|
870
|
+
else
|
871
|
+
if try_tok(:splat)
|
872
|
+
return args, keywords, splat, e if splat
|
873
|
+
splat, e = e, nil
|
874
|
+
elsif splat
|
875
|
+
raise SyntaxError.new("Only keyword arguments may follow variable arguments (...).")
|
876
|
+
elsif !keywords.empty?
|
877
|
+
raise SyntaxError.new("Positional arguments must come before keyword arguments.")
|
878
|
+
end
|
879
|
+
args << e if e
|
880
|
+
end
|
881
|
+
|
882
|
+
return args, keywords, splat unless try_tok(:comma)
|
883
|
+
end
|
884
|
+
return args, keywords
|
885
|
+
end
|
886
|
+
end
|
887
|
+
|
888
|
+
def raw
|
889
|
+
tok = try_tok(:raw)
|
890
|
+
return special_fun unless tok
|
891
|
+
literal_node(Script::Value::String.new(tok.value), tok.source_range)
|
892
|
+
end
|
893
|
+
|
894
|
+
def special_fun
|
895
|
+
first = try_tok(:special_fun)
|
896
|
+
return square_list unless first
|
897
|
+
str = literal_node(first.value, first.source_range)
|
898
|
+
return str unless try_tok(:string_interpolation)
|
899
|
+
mid = without_stop_at {assert_expr :expr}
|
900
|
+
assert_tok :end_interpolation
|
901
|
+
last = assert_expr(:special_fun)
|
902
|
+
node(
|
903
|
+
Tree::Interpolation.new(str, mid, last, false, false),
|
904
|
+
first.source_range.start_pos)
|
905
|
+
end
|
906
|
+
|
907
|
+
def square_list
|
908
|
+
start_pos = source_position
|
909
|
+
return paren unless try_tok(:lsquare)
|
910
|
+
|
911
|
+
without_stop_at do
|
912
|
+
space_start_pos = source_position
|
913
|
+
e = interpolation(inner: :or_expr)
|
914
|
+
separator = nil
|
915
|
+
if e
|
916
|
+
elements = [e]
|
917
|
+
while (e = interpolation(inner: :or_expr))
|
918
|
+
elements << e
|
919
|
+
end
|
920
|
+
|
921
|
+
# If there's a comma after a space-separated list, it's actually a
|
922
|
+
# space-separated list nested in a comma-separated list.
|
923
|
+
if try_tok(:comma)
|
924
|
+
e = if elements.length == 1
|
925
|
+
elements.first
|
926
|
+
else
|
927
|
+
node(
|
928
|
+
Sass::Script::Tree::ListLiteral.new(elements, separator: :space),
|
929
|
+
space_start_pos)
|
930
|
+
end
|
931
|
+
elements = [e]
|
932
|
+
|
933
|
+
while (e = space)
|
934
|
+
elements << e
|
935
|
+
break unless try_tok(:comma)
|
936
|
+
end
|
937
|
+
separator = :comma
|
938
|
+
else
|
939
|
+
separator = :space if elements.length > 1
|
940
|
+
end
|
941
|
+
else
|
942
|
+
elements = []
|
943
|
+
end
|
944
|
+
|
945
|
+
assert_tok(:rsquare)
|
946
|
+
end_pos = source_position
|
947
|
+
|
948
|
+
node(Sass::Script::Tree::ListLiteral.new(elements, separator: separator, bracketed: true),
|
949
|
+
start_pos, end_pos)
|
950
|
+
end
|
951
|
+
end
|
952
|
+
|
953
|
+
def paren
|
954
|
+
return variable unless try_tok(:lparen)
|
955
|
+
without_stop_at do
|
956
|
+
start_pos = source_position
|
957
|
+
e = map
|
958
|
+
e.force_division! if e
|
959
|
+
end_pos = source_position
|
960
|
+
assert_tok(:rparen)
|
961
|
+
e || node(Sass::Script::Tree::ListLiteral.new([]), start_pos, end_pos)
|
962
|
+
end
|
963
|
+
end
|
964
|
+
|
965
|
+
def variable
|
966
|
+
start_pos = source_position
|
967
|
+
c = try_tok(:const)
|
968
|
+
return string unless c
|
969
|
+
node(Tree::Variable.new(*c.value), start_pos)
|
970
|
+
end
|
971
|
+
|
972
|
+
def string
|
973
|
+
first = try_tok(:string)
|
974
|
+
return number unless first
|
975
|
+
str = literal_node(first.value, first.source_range)
|
976
|
+
return str unless try_tok(:string_interpolation)
|
977
|
+
mid = assert_expr :expr
|
978
|
+
assert_tok :end_interpolation
|
979
|
+
last = without_stop_at {assert_expr(:string)}
|
980
|
+
node(Tree::StringInterpolation.new(str, mid, last), first.source_range.start_pos)
|
981
|
+
end
|
982
|
+
|
983
|
+
def number
|
984
|
+
tok = try_tok(:number)
|
985
|
+
return selector unless tok
|
986
|
+
num = tok.value
|
987
|
+
num.options = @options
|
988
|
+
num.original = num.to_s
|
989
|
+
literal_node(num, tok.source_range.start_pos)
|
990
|
+
end
|
991
|
+
|
992
|
+
def selector
|
993
|
+
tok = try_tok(:selector)
|
994
|
+
return literal unless tok
|
995
|
+
node(tok.value, tok.source_range.start_pos)
|
996
|
+
end
|
997
|
+
|
998
|
+
def literal
|
999
|
+
t = try_tok(:color)
|
1000
|
+
return literal_node(t.value, t.source_range) if t
|
1001
|
+
end
|
1002
|
+
|
1003
|
+
# It would be possible to have unified #assert and #try methods,
|
1004
|
+
# but detecting the method/token difference turns out to be quite expensive.
|
1005
|
+
|
1006
|
+
EXPR_NAMES = {
|
1007
|
+
:string => "string",
|
1008
|
+
:default => "expression (e.g. 1px, bold)",
|
1009
|
+
:mixin_arglist => "mixin argument",
|
1010
|
+
:fn_arglist => "function argument",
|
1011
|
+
:splat => "...",
|
1012
|
+
:special_fun => '")"',
|
1013
|
+
}
|
1014
|
+
|
1015
|
+
def assert_expr(name, expected = nil)
|
1016
|
+
e = send(name)
|
1017
|
+
return e if e
|
1018
|
+
@lexer.expected!(expected || EXPR_NAMES[name] || EXPR_NAMES[:default])
|
1019
|
+
end
|
1020
|
+
|
1021
|
+
def assert_tok(name)
|
1022
|
+
# Avoids an array allocation caused by argument globbing in assert_toks.
|
1023
|
+
t = try_tok(name)
|
1024
|
+
return t if t
|
1025
|
+
@lexer.expected!(Lexer::TOKEN_NAMES[name] || name.to_s)
|
1026
|
+
end
|
1027
|
+
|
1028
|
+
def assert_toks(*names)
|
1029
|
+
t = try_toks(*names)
|
1030
|
+
return t if t
|
1031
|
+
@lexer.expected!(names.map {|tok| Lexer::TOKEN_NAMES[tok] || tok}.join(" or "))
|
1032
|
+
end
|
1033
|
+
|
1034
|
+
def peek_tok(name)
|
1035
|
+
# Avoids an array allocation caused by argument globbing in the try_toks method.
|
1036
|
+
peeked = @lexer.peek
|
1037
|
+
peeked && name == peeked.type && peeked
|
1038
|
+
end
|
1039
|
+
|
1040
|
+
def peek_toks(*names)
|
1041
|
+
peeked = @lexer.peek
|
1042
|
+
peeked && names.include?(peeked.type) && peeked
|
1043
|
+
end
|
1044
|
+
|
1045
|
+
def try_tok(name)
|
1046
|
+
peek_tok(name) && @lexer.next
|
1047
|
+
end
|
1048
|
+
|
1049
|
+
def try_toks(*names)
|
1050
|
+
peek_toks(*names) && @lexer.next
|
1051
|
+
end
|
1052
|
+
|
1053
|
+
def assert_done
|
1054
|
+
if @allow_extra_text
|
1055
|
+
# If extra text is allowed, just rewind the lexer so that the
|
1056
|
+
# StringScanner is pointing to the end of the parsed text.
|
1057
|
+
@lexer.unpeek!
|
1058
|
+
else
|
1059
|
+
return if @lexer.done?
|
1060
|
+
@lexer.expected!(EXPR_NAMES[:default])
|
1061
|
+
end
|
1062
|
+
end
|
1063
|
+
|
1064
|
+
def without_stop_at
|
1065
|
+
old_stop_at = @stop_at
|
1066
|
+
@stop_at = nil
|
1067
|
+
yield
|
1068
|
+
ensure
|
1069
|
+
@stop_at = old_stop_at
|
1070
|
+
end
|
1071
|
+
|
1072
|
+
# @overload node(value, source_range)
|
1073
|
+
# @param value [Sass::Script::Value::Base]
|
1074
|
+
# @param source_range [Sass::Source::Range]
|
1075
|
+
# @overload node(value, start_pos, end_pos = source_position)
|
1076
|
+
# @param value [Sass::Script::Value::Base]
|
1077
|
+
# @param start_pos [Sass::Source::Position]
|
1078
|
+
# @param end_pos [Sass::Source::Position]
|
1079
|
+
def literal_node(value, source_range_or_start_pos, end_pos = source_position)
|
1080
|
+
node(Sass::Script::Tree::Literal.new(value), source_range_or_start_pos, end_pos)
|
1081
|
+
end
|
1082
|
+
|
1083
|
+
# @overload node(node, source_range)
|
1084
|
+
# @param node [Sass::Script::Tree::Node]
|
1085
|
+
# @param source_range [Sass::Source::Range]
|
1086
|
+
# @overload node(node, start_pos, end_pos = source_position)
|
1087
|
+
# @param node [Sass::Script::Tree::Node]
|
1088
|
+
# @param start_pos [Sass::Source::Position]
|
1089
|
+
# @param end_pos [Sass::Source::Position]
|
1090
|
+
def node(node, source_range_or_start_pos, end_pos = source_position)
|
1091
|
+
source_range =
|
1092
|
+
if source_range_or_start_pos.is_a?(Sass::Source::Range)
|
1093
|
+
source_range_or_start_pos
|
1094
|
+
else
|
1095
|
+
range(source_range_or_start_pos, end_pos)
|
1096
|
+
end
|
1097
|
+
|
1098
|
+
node.line = source_range.start_pos.line
|
1099
|
+
node.source_range = source_range
|
1100
|
+
node.filename = @options[:filename]
|
1101
|
+
node
|
1102
|
+
end
|
1103
|
+
|
1104
|
+
# Converts an array of strings and expressions to a string interoplation
|
1105
|
+
# object.
|
1106
|
+
#
|
1107
|
+
# @param array [Array<Script::Tree:Node | String>]
|
1108
|
+
# @return [Script::Tree::StringInterpolation]
|
1109
|
+
def array_to_interpolation(array)
|
1110
|
+
Sass::Util.merge_adjacent_strings(array).reverse.inject(nil) do |after, value|
|
1111
|
+
if value.is_a?(::String)
|
1112
|
+
literal = Sass::Script::Tree::Literal.new(
|
1113
|
+
Sass::Script::Value::String.new(value))
|
1114
|
+
next literal unless after
|
1115
|
+
Sass::Script::Tree::StringInterpolation.new(literal, after.mid, after.after)
|
1116
|
+
else
|
1117
|
+
Sass::Script::Tree::StringInterpolation.new(
|
1118
|
+
Sass::Script::Tree::Literal.new(
|
1119
|
+
Sass::Script::Value::String.new('')),
|
1120
|
+
value,
|
1121
|
+
after || Sass::Script::Tree::Literal.new(
|
1122
|
+
Sass::Script::Value::String.new('')))
|
1123
|
+
end
|
1124
|
+
end
|
1125
|
+
end
|
1126
|
+
|
1127
|
+
# Checks a script node for any immediately-deprecated interpolations, and
|
1128
|
+
# emits warnings for them.
|
1129
|
+
#
|
1130
|
+
# @param node [Sass::Script::Tree::Node]
|
1131
|
+
def check_for_interpolation(node)
|
1132
|
+
nodes = [node]
|
1133
|
+
until nodes.empty?
|
1134
|
+
node = nodes.pop
|
1135
|
+
unless node.is_a?(Sass::Script::Tree::Interpolation) &&
|
1136
|
+
node.deprecation == :immediate
|
1137
|
+
nodes.concat node.children
|
1138
|
+
next
|
1139
|
+
end
|
1140
|
+
|
1141
|
+
interpolation_deprecation(node)
|
1142
|
+
end
|
1143
|
+
end
|
1144
|
+
|
1145
|
+
# Emits a deprecation warning for an interpolation node.
|
1146
|
+
#
|
1147
|
+
# @param node [Sass::Script::Tree::Node]
|
1148
|
+
def interpolation_deprecation(interpolation)
|
1149
|
+
return if @options[:_convert]
|
1150
|
+
location = "on line #{interpolation.line}"
|
1151
|
+
location << " of #{interpolation.filename}" if interpolation.filename
|
1152
|
+
Sass::Util.sass_warn <<WARNING
|
1153
|
+
DEPRECATION WARNING #{location}:
|
1154
|
+
\#{} interpolation near operators will be simplified in a future version of Sass.
|
1155
|
+
To preserve the current behavior, use quotes:
|
1156
|
+
|
1157
|
+
#{interpolation.to_quoted_equivalent.to_sass}
|
1158
|
+
|
1159
|
+
You can use the sass-convert command to automatically fix most cases.
|
1160
|
+
WARNING
|
1161
|
+
end
|
1162
|
+
end
|
1163
|
+
end
|
1164
|
+
end
|