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,1343 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
module Sass
|
5
|
+
module SCSS
|
6
|
+
# The parser for SCSS.
|
7
|
+
# It parses a string of code into a tree of {Sass::Tree::Node}s.
|
8
|
+
class Parser
|
9
|
+
# Expose for the SASS parser.
|
10
|
+
attr_accessor :offset
|
11
|
+
|
12
|
+
# @param str [String, StringScanner] The source document to parse.
|
13
|
+
# Note that `Parser` *won't* raise a nice error message if this isn't properly parsed;
|
14
|
+
# for that, you should use the higher-level {Sass::Engine} or {Sass::CSS}.
|
15
|
+
# @param filename [String] The name of the file being parsed. Used for
|
16
|
+
# warnings and source maps.
|
17
|
+
# @param importer [Sass::Importers::Base] The importer used to import the
|
18
|
+
# file being parsed. Used for source maps.
|
19
|
+
# @param line [Integer] The 1-based line on which the source string appeared,
|
20
|
+
# if it's part of another document.
|
21
|
+
# @param offset [Integer] The 1-based character (not byte) offset in the line on
|
22
|
+
# which the source string starts. Used for error reporting and sourcemap
|
23
|
+
# building.
|
24
|
+
def initialize(str, filename, importer, line = 1, offset = 1)
|
25
|
+
@template = str
|
26
|
+
@filename = filename
|
27
|
+
@importer = importer
|
28
|
+
@line = line
|
29
|
+
@offset = offset
|
30
|
+
@strs = []
|
31
|
+
@expected = nil
|
32
|
+
@throw_error = false
|
33
|
+
end
|
34
|
+
|
35
|
+
# Parses an SCSS document.
|
36
|
+
#
|
37
|
+
# @return [Sass::Tree::RootNode] The root node of the document tree
|
38
|
+
# @raise [Sass::SyntaxError] if there's a syntax error in the document
|
39
|
+
def parse
|
40
|
+
init_scanner!
|
41
|
+
root = stylesheet
|
42
|
+
expected("selector or at-rule") unless root && @scanner.eos?
|
43
|
+
root
|
44
|
+
end
|
45
|
+
|
46
|
+
# Parses an identifier with interpolation.
|
47
|
+
# Note that this won't assert that the identifier takes up the entire input string;
|
48
|
+
# it's meant to be used with `StringScanner`s as part of other parsers.
|
49
|
+
#
|
50
|
+
# @return [Array<String, Sass::Script::Tree::Node>, nil]
|
51
|
+
# The interpolated identifier, or nil if none could be parsed
|
52
|
+
def parse_interp_ident
|
53
|
+
init_scanner!
|
54
|
+
interp_ident
|
55
|
+
end
|
56
|
+
|
57
|
+
# Parses a supports clause for an @import directive
|
58
|
+
def parse_supports_clause
|
59
|
+
init_scanner!
|
60
|
+
ss
|
61
|
+
clause = supports_clause
|
62
|
+
ss
|
63
|
+
clause
|
64
|
+
end
|
65
|
+
|
66
|
+
# Parses a media query list.
|
67
|
+
#
|
68
|
+
# @return [Sass::Media::QueryList] The parsed query list
|
69
|
+
# @raise [Sass::SyntaxError] if there's a syntax error in the query list,
|
70
|
+
# or if it doesn't take up the entire input string.
|
71
|
+
def parse_media_query_list
|
72
|
+
init_scanner!
|
73
|
+
ql = media_query_list
|
74
|
+
expected("media query list") unless ql && @scanner.eos?
|
75
|
+
ql
|
76
|
+
end
|
77
|
+
|
78
|
+
# Parses an at-root query.
|
79
|
+
#
|
80
|
+
# @return [Array<String, Sass::Script;:Tree::Node>] The interpolated query.
|
81
|
+
# @raise [Sass::SyntaxError] if there's a syntax error in the query,
|
82
|
+
# or if it doesn't take up the entire input string.
|
83
|
+
def parse_at_root_query
|
84
|
+
init_scanner!
|
85
|
+
query = at_root_query
|
86
|
+
expected("@at-root query list") unless query && @scanner.eos?
|
87
|
+
query
|
88
|
+
end
|
89
|
+
|
90
|
+
# Parses a supports query condition.
|
91
|
+
#
|
92
|
+
# @return [Sass::Supports::Condition] The parsed condition
|
93
|
+
# @raise [Sass::SyntaxError] if there's a syntax error in the condition,
|
94
|
+
# or if it doesn't take up the entire input string.
|
95
|
+
def parse_supports_condition
|
96
|
+
init_scanner!
|
97
|
+
condition = supports_condition
|
98
|
+
expected("supports condition") unless condition && @scanner.eos?
|
99
|
+
condition
|
100
|
+
end
|
101
|
+
|
102
|
+
# Parses a custom property value.
|
103
|
+
#
|
104
|
+
# @return [Array<String, Sass::Script;:Tree::Node>] The interpolated value.
|
105
|
+
# @raise [Sass::SyntaxError] if there's a syntax error in the value,
|
106
|
+
# or if it doesn't take up the entire input string.
|
107
|
+
def parse_declaration_value
|
108
|
+
init_scanner!
|
109
|
+
value = declaration_value
|
110
|
+
expected('"}"') unless value && @scanner.eos?
|
111
|
+
value
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
include Sass::SCSS::RX
|
117
|
+
|
118
|
+
def source_position
|
119
|
+
Sass::Source::Position.new(@line, @offset)
|
120
|
+
end
|
121
|
+
|
122
|
+
def range(start_pos, end_pos = source_position)
|
123
|
+
Sass::Source::Range.new(start_pos, end_pos, @filename, @importer)
|
124
|
+
end
|
125
|
+
|
126
|
+
def init_scanner!
|
127
|
+
@scanner =
|
128
|
+
if @template.is_a?(StringScanner)
|
129
|
+
@template
|
130
|
+
else
|
131
|
+
Sass::Util::MultibyteStringScanner.new(@template.tr("\r", ""))
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def stylesheet
|
136
|
+
node = node(Sass::Tree::RootNode.new(@scanner.string), source_position)
|
137
|
+
block_contents(node, :stylesheet) {s(node)}
|
138
|
+
end
|
139
|
+
|
140
|
+
def s(node)
|
141
|
+
while tok(S) || tok(CDC) || tok(CDO) || (c = tok(SINGLE_LINE_COMMENT)) || (c = tok(COMMENT))
|
142
|
+
next unless c
|
143
|
+
process_comment c, node
|
144
|
+
c = nil
|
145
|
+
end
|
146
|
+
true
|
147
|
+
end
|
148
|
+
|
149
|
+
def ss
|
150
|
+
nil while tok(S) || tok(SINGLE_LINE_COMMENT) || tok(COMMENT)
|
151
|
+
true
|
152
|
+
end
|
153
|
+
|
154
|
+
def ss_comments(node)
|
155
|
+
while tok(S) || (c = tok(SINGLE_LINE_COMMENT)) || (c = tok(COMMENT))
|
156
|
+
next unless c
|
157
|
+
process_comment c, node
|
158
|
+
c = nil
|
159
|
+
end
|
160
|
+
|
161
|
+
true
|
162
|
+
end
|
163
|
+
|
164
|
+
def whitespace
|
165
|
+
return unless tok(S) || tok(SINGLE_LINE_COMMENT) || tok(COMMENT)
|
166
|
+
ss
|
167
|
+
end
|
168
|
+
|
169
|
+
def process_comment(text, node)
|
170
|
+
silent = text =~ %r{\A//}
|
171
|
+
loud = !silent && text =~ %r{\A/[/*]!}
|
172
|
+
line = @line - text.count("\n")
|
173
|
+
comment_start = @scanner.pos - text.length
|
174
|
+
index_before_line = @scanner.string.rindex("\n", comment_start) || -1
|
175
|
+
offset = comment_start - index_before_line
|
176
|
+
|
177
|
+
if silent
|
178
|
+
value = [text.sub(%r{\A\s*//}, '/*').gsub(%r{^\s*//}, ' *') + ' */']
|
179
|
+
else
|
180
|
+
value = Sass::Engine.parse_interp(text, line, offset, :filename => @filename)
|
181
|
+
line_before_comment = @scanner.string[index_before_line + 1...comment_start]
|
182
|
+
value.unshift(line_before_comment.gsub(/[^\s]/, ' '))
|
183
|
+
end
|
184
|
+
|
185
|
+
type = if silent
|
186
|
+
:silent
|
187
|
+
elsif loud
|
188
|
+
:loud
|
189
|
+
else
|
190
|
+
:normal
|
191
|
+
end
|
192
|
+
start_pos = Sass::Source::Position.new(line, offset)
|
193
|
+
comment = node(Sass::Tree::CommentNode.new(value, type), start_pos)
|
194
|
+
node << comment
|
195
|
+
end
|
196
|
+
|
197
|
+
DIRECTIVES = Set[:mixin, :include, :function, :return, :debug, :warn, :for,
|
198
|
+
:each, :while, :if, :else, :extend, :import, :media, :charset, :content,
|
199
|
+
:_moz_document, :at_root, :error]
|
200
|
+
|
201
|
+
PREFIXED_DIRECTIVES = Set[:supports]
|
202
|
+
|
203
|
+
def directive
|
204
|
+
start_pos = source_position
|
205
|
+
return unless tok(/@/)
|
206
|
+
name = ident!
|
207
|
+
ss
|
208
|
+
|
209
|
+
if (dir = special_directive(name, start_pos))
|
210
|
+
return dir
|
211
|
+
elsif (dir = prefixed_directive(name, start_pos))
|
212
|
+
return dir
|
213
|
+
end
|
214
|
+
|
215
|
+
val = almost_any_value
|
216
|
+
val = val ? ["@#{name} "] + Sass::Util.strip_string_array(val) : ["@#{name}"]
|
217
|
+
directive_body(val, start_pos)
|
218
|
+
end
|
219
|
+
|
220
|
+
def directive_body(value, start_pos)
|
221
|
+
node = Sass::Tree::DirectiveNode.new(value)
|
222
|
+
|
223
|
+
if tok(/\{/)
|
224
|
+
node.has_children = true
|
225
|
+
block_contents(node, :directive)
|
226
|
+
tok!(/\}/)
|
227
|
+
end
|
228
|
+
|
229
|
+
node(node, start_pos)
|
230
|
+
end
|
231
|
+
|
232
|
+
def special_directive(name, start_pos)
|
233
|
+
sym = name.tr('-', '_').to_sym
|
234
|
+
DIRECTIVES.include?(sym) && send("#{sym}_directive", start_pos)
|
235
|
+
end
|
236
|
+
|
237
|
+
def prefixed_directive(name, start_pos)
|
238
|
+
sym = deprefix(name).tr('-', '_').to_sym
|
239
|
+
PREFIXED_DIRECTIVES.include?(sym) && send("#{sym}_directive", name, start_pos)
|
240
|
+
end
|
241
|
+
|
242
|
+
def mixin_directive(start_pos)
|
243
|
+
name = ident!
|
244
|
+
args, splat = sass_script(:parse_mixin_definition_arglist)
|
245
|
+
ss
|
246
|
+
block(node(Sass::Tree::MixinDefNode.new(name, args, splat), start_pos), :directive)
|
247
|
+
end
|
248
|
+
|
249
|
+
def include_directive(start_pos)
|
250
|
+
name = ident!
|
251
|
+
args, keywords, splat, kwarg_splat = sass_script(:parse_mixin_include_arglist)
|
252
|
+
ss
|
253
|
+
include_node = node(
|
254
|
+
Sass::Tree::MixinNode.new(name, args, keywords, splat, kwarg_splat), start_pos)
|
255
|
+
if tok?(/\{/)
|
256
|
+
include_node.has_children = true
|
257
|
+
block(include_node, :directive)
|
258
|
+
else
|
259
|
+
include_node
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
def content_directive(start_pos)
|
264
|
+
ss
|
265
|
+
node(Sass::Tree::ContentNode.new, start_pos)
|
266
|
+
end
|
267
|
+
|
268
|
+
def function_directive(start_pos)
|
269
|
+
name = ident!
|
270
|
+
args, splat = sass_script(:parse_function_definition_arglist)
|
271
|
+
ss
|
272
|
+
block(node(Sass::Tree::FunctionNode.new(name, args, splat), start_pos), :function)
|
273
|
+
end
|
274
|
+
|
275
|
+
def return_directive(start_pos)
|
276
|
+
node(Sass::Tree::ReturnNode.new(sass_script(:parse)), start_pos)
|
277
|
+
end
|
278
|
+
|
279
|
+
def debug_directive(start_pos)
|
280
|
+
node(Sass::Tree::DebugNode.new(sass_script(:parse)), start_pos)
|
281
|
+
end
|
282
|
+
|
283
|
+
def warn_directive(start_pos)
|
284
|
+
node(Sass::Tree::WarnNode.new(sass_script(:parse)), start_pos)
|
285
|
+
end
|
286
|
+
|
287
|
+
def for_directive(start_pos)
|
288
|
+
tok!(/\$/)
|
289
|
+
var = ident!
|
290
|
+
ss
|
291
|
+
|
292
|
+
tok!(/from/)
|
293
|
+
from = sass_script(:parse_until, Set["to", "through"])
|
294
|
+
ss
|
295
|
+
|
296
|
+
@expected = '"to" or "through"'
|
297
|
+
exclusive = (tok(/to/) || tok!(/through/)) == 'to'
|
298
|
+
to = sass_script(:parse)
|
299
|
+
ss
|
300
|
+
|
301
|
+
block(node(Sass::Tree::ForNode.new(var, from, to, exclusive), start_pos), :directive)
|
302
|
+
end
|
303
|
+
|
304
|
+
def each_directive(start_pos)
|
305
|
+
tok!(/\$/)
|
306
|
+
vars = [ident!]
|
307
|
+
ss
|
308
|
+
while tok(/,/)
|
309
|
+
ss
|
310
|
+
tok!(/\$/)
|
311
|
+
vars << ident!
|
312
|
+
ss
|
313
|
+
end
|
314
|
+
|
315
|
+
tok!(/in/)
|
316
|
+
list = sass_script(:parse)
|
317
|
+
ss
|
318
|
+
|
319
|
+
block(node(Sass::Tree::EachNode.new(vars, list), start_pos), :directive)
|
320
|
+
end
|
321
|
+
|
322
|
+
def while_directive(start_pos)
|
323
|
+
expr = sass_script(:parse)
|
324
|
+
ss
|
325
|
+
block(node(Sass::Tree::WhileNode.new(expr), start_pos), :directive)
|
326
|
+
end
|
327
|
+
|
328
|
+
def if_directive(start_pos)
|
329
|
+
expr = sass_script(:parse)
|
330
|
+
ss
|
331
|
+
node = block(node(Sass::Tree::IfNode.new(expr), start_pos), :directive)
|
332
|
+
pos = @scanner.pos
|
333
|
+
line = @line
|
334
|
+
ss
|
335
|
+
|
336
|
+
else_block(node) ||
|
337
|
+
begin
|
338
|
+
# Backtrack in case there are any comments we want to parse
|
339
|
+
@scanner.pos = pos
|
340
|
+
@line = line
|
341
|
+
node
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
def else_block(node)
|
346
|
+
start_pos = source_position
|
347
|
+
return unless tok(/@else/)
|
348
|
+
ss
|
349
|
+
else_node = block(
|
350
|
+
node(Sass::Tree::IfNode.new((sass_script(:parse) if tok(/if/))), start_pos),
|
351
|
+
:directive)
|
352
|
+
node.add_else(else_node)
|
353
|
+
pos = @scanner.pos
|
354
|
+
line = @line
|
355
|
+
ss
|
356
|
+
|
357
|
+
else_block(node) ||
|
358
|
+
begin
|
359
|
+
# Backtrack in case there are any comments we want to parse
|
360
|
+
@scanner.pos = pos
|
361
|
+
@line = line
|
362
|
+
node
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
def else_directive(start_pos)
|
367
|
+
err("Invalid CSS: @else must come after @if")
|
368
|
+
end
|
369
|
+
|
370
|
+
def extend_directive(start_pos)
|
371
|
+
selector_start_pos = source_position
|
372
|
+
@expected = "selector"
|
373
|
+
selector = Sass::Util.strip_string_array(expr!(:almost_any_value))
|
374
|
+
optional = tok(OPTIONAL)
|
375
|
+
ss
|
376
|
+
node(Sass::Tree::ExtendNode.new(selector, !!optional, range(selector_start_pos)), start_pos)
|
377
|
+
end
|
378
|
+
|
379
|
+
def import_directive(start_pos)
|
380
|
+
values = []
|
381
|
+
|
382
|
+
loop do
|
383
|
+
values << expr!(:import_arg)
|
384
|
+
break if use_css_import?
|
385
|
+
break unless tok(/,/)
|
386
|
+
ss
|
387
|
+
end
|
388
|
+
|
389
|
+
values
|
390
|
+
end
|
391
|
+
|
392
|
+
def import_arg
|
393
|
+
start_pos = source_position
|
394
|
+
return unless (str = string) || (uri = tok?(/url\(/i))
|
395
|
+
if uri
|
396
|
+
str = sass_script(:parse_string)
|
397
|
+
ss
|
398
|
+
supports = supports_clause
|
399
|
+
ss
|
400
|
+
media = media_query_list
|
401
|
+
ss
|
402
|
+
return node(Tree::CssImportNode.new(str, media.to_a, supports), start_pos)
|
403
|
+
end
|
404
|
+
ss
|
405
|
+
|
406
|
+
supports = supports_clause
|
407
|
+
ss
|
408
|
+
media = media_query_list
|
409
|
+
if str =~ %r{^(https?:)?//} || media || supports || use_css_import?
|
410
|
+
return node(
|
411
|
+
Sass::Tree::CssImportNode.new(
|
412
|
+
Sass::Script::Value::String.quote(str), media.to_a, supports), start_pos)
|
413
|
+
end
|
414
|
+
|
415
|
+
node(Sass::Tree::ImportNode.new(str.strip), start_pos)
|
416
|
+
end
|
417
|
+
|
418
|
+
def use_css_import?; false; end
|
419
|
+
|
420
|
+
def media_directive(start_pos)
|
421
|
+
block(node(Sass::Tree::MediaNode.new(expr!(:media_query_list).to_a), start_pos), :directive)
|
422
|
+
end
|
423
|
+
|
424
|
+
# http://www.w3.org/TR/css3-mediaqueries/#syntax
|
425
|
+
def media_query_list
|
426
|
+
query = media_query
|
427
|
+
return unless query
|
428
|
+
queries = [query]
|
429
|
+
|
430
|
+
ss
|
431
|
+
while tok(/,/)
|
432
|
+
ss; queries << expr!(:media_query)
|
433
|
+
end
|
434
|
+
ss
|
435
|
+
|
436
|
+
Sass::Media::QueryList.new(queries)
|
437
|
+
end
|
438
|
+
|
439
|
+
def media_query
|
440
|
+
if (ident1 = interp_ident)
|
441
|
+
ss
|
442
|
+
ident2 = interp_ident
|
443
|
+
ss
|
444
|
+
if ident2 && ident2.length == 1 && ident2[0].is_a?(String) && ident2[0].downcase == 'and'
|
445
|
+
query = Sass::Media::Query.new([], ident1, [])
|
446
|
+
else
|
447
|
+
if ident2
|
448
|
+
query = Sass::Media::Query.new(ident1, ident2, [])
|
449
|
+
else
|
450
|
+
query = Sass::Media::Query.new([], ident1, [])
|
451
|
+
end
|
452
|
+
return query unless tok(/and/i)
|
453
|
+
ss
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
if query
|
458
|
+
expr = expr!(:media_expr)
|
459
|
+
else
|
460
|
+
expr = media_expr
|
461
|
+
return unless expr
|
462
|
+
end
|
463
|
+
query ||= Sass::Media::Query.new([], [], [])
|
464
|
+
query.expressions << expr
|
465
|
+
|
466
|
+
ss
|
467
|
+
while tok(/and/i)
|
468
|
+
ss; query.expressions << expr!(:media_expr)
|
469
|
+
end
|
470
|
+
|
471
|
+
query
|
472
|
+
end
|
473
|
+
|
474
|
+
def query_expr
|
475
|
+
interp = interpolation
|
476
|
+
return interp if interp
|
477
|
+
return unless tok(/\(/)
|
478
|
+
res = ['(']
|
479
|
+
ss
|
480
|
+
stop_at = Set[:single_eq, :lt, :lte, :gt, :gte]
|
481
|
+
res << sass_script(:parse_until, stop_at)
|
482
|
+
|
483
|
+
if tok(/:/)
|
484
|
+
res << ': '
|
485
|
+
ss
|
486
|
+
res << sass_script(:parse)
|
487
|
+
elsif comparison1 = tok(/=|[<>]=?/)
|
488
|
+
res << ' ' << comparison1 << ' '
|
489
|
+
ss
|
490
|
+
res << sass_script(:parse_until, stop_at)
|
491
|
+
if ((comparison1 == ">" || comparison1 == ">=") && comparison2 = tok(/>=?/)) ||
|
492
|
+
((comparison1 == "<" || comparison1 == "<=") && comparison2 = tok(/<=?/))
|
493
|
+
res << ' ' << comparison2 << ' '
|
494
|
+
ss
|
495
|
+
res << sass_script(:parse_until, stop_at)
|
496
|
+
end
|
497
|
+
end
|
498
|
+
res << tok!(/\)/)
|
499
|
+
ss
|
500
|
+
res
|
501
|
+
end
|
502
|
+
|
503
|
+
# Aliases allow us to use different descriptions if the same
|
504
|
+
# expression fails in different contexts.
|
505
|
+
alias_method :media_expr, :query_expr
|
506
|
+
alias_method :at_root_query, :query_expr
|
507
|
+
|
508
|
+
def charset_directive(start_pos)
|
509
|
+
name = expr!(:string)
|
510
|
+
ss
|
511
|
+
node(Sass::Tree::CharsetNode.new(name), start_pos)
|
512
|
+
end
|
513
|
+
|
514
|
+
# The document directive is specified in
|
515
|
+
# http://www.w3.org/TR/css3-conditional/, but Gecko allows the
|
516
|
+
# `url-prefix` and `domain` functions to omit quotation marks, contrary to
|
517
|
+
# the standard.
|
518
|
+
#
|
519
|
+
# We could parse all document directives according to Mozilla's syntax,
|
520
|
+
# but if someone's using e.g. @-webkit-document we don't want them to
|
521
|
+
# think WebKit works sans quotes.
|
522
|
+
def _moz_document_directive(start_pos)
|
523
|
+
res = ["@-moz-document "]
|
524
|
+
loop do
|
525
|
+
res << str {ss} << expr!(:moz_document_function)
|
526
|
+
if (c = tok(/,/))
|
527
|
+
res << c
|
528
|
+
else
|
529
|
+
break
|
530
|
+
end
|
531
|
+
end
|
532
|
+
directive_body(res.flatten, start_pos)
|
533
|
+
end
|
534
|
+
|
535
|
+
def moz_document_function
|
536
|
+
val = interp_uri || _interp_string(:url_prefix) ||
|
537
|
+
_interp_string(:domain) || function(false) || interpolation
|
538
|
+
return unless val
|
539
|
+
ss
|
540
|
+
val
|
541
|
+
end
|
542
|
+
|
543
|
+
def at_root_directive(start_pos)
|
544
|
+
if tok?(/\(/) && (expr = at_root_query)
|
545
|
+
return block(node(Sass::Tree::AtRootNode.new(expr), start_pos), :directive)
|
546
|
+
end
|
547
|
+
|
548
|
+
at_root_node = node(Sass::Tree::AtRootNode.new, start_pos)
|
549
|
+
rule_node = ruleset
|
550
|
+
return block(at_root_node, :stylesheet) unless rule_node
|
551
|
+
at_root_node << rule_node
|
552
|
+
at_root_node
|
553
|
+
end
|
554
|
+
|
555
|
+
def at_root_directive_list
|
556
|
+
return unless (first = ident)
|
557
|
+
arr = [first]
|
558
|
+
ss
|
559
|
+
while (e = ident)
|
560
|
+
arr << e
|
561
|
+
ss
|
562
|
+
end
|
563
|
+
arr
|
564
|
+
end
|
565
|
+
|
566
|
+
def error_directive(start_pos)
|
567
|
+
node(Sass::Tree::ErrorNode.new(sass_script(:parse)), start_pos)
|
568
|
+
end
|
569
|
+
|
570
|
+
# http://www.w3.org/TR/css3-conditional/
|
571
|
+
def supports_directive(name, start_pos)
|
572
|
+
condition = expr!(:supports_condition)
|
573
|
+
node = Sass::Tree::SupportsNode.new(name, condition)
|
574
|
+
|
575
|
+
tok!(/\{/)
|
576
|
+
node.has_children = true
|
577
|
+
block_contents(node, :directive)
|
578
|
+
tok!(/\}/)
|
579
|
+
|
580
|
+
node(node, start_pos)
|
581
|
+
end
|
582
|
+
|
583
|
+
def supports_clause
|
584
|
+
return unless tok(/supports\(/i)
|
585
|
+
ss
|
586
|
+
supports = import_supports_condition
|
587
|
+
ss
|
588
|
+
tok!(/\)/)
|
589
|
+
supports
|
590
|
+
end
|
591
|
+
|
592
|
+
def supports_condition
|
593
|
+
supports_negation || supports_operator || supports_interpolation
|
594
|
+
end
|
595
|
+
|
596
|
+
def import_supports_condition
|
597
|
+
supports_condition || supports_declaration
|
598
|
+
end
|
599
|
+
|
600
|
+
def supports_negation
|
601
|
+
return unless tok(/not/i)
|
602
|
+
ss
|
603
|
+
Sass::Supports::Negation.new(expr!(:supports_condition_in_parens))
|
604
|
+
end
|
605
|
+
|
606
|
+
def supports_operator
|
607
|
+
cond = supports_condition_in_parens
|
608
|
+
return unless cond
|
609
|
+
re = /and|or/i
|
610
|
+
while (op = tok(re))
|
611
|
+
re = /#{op}/i
|
612
|
+
ss
|
613
|
+
cond = Sass::Supports::Operator.new(
|
614
|
+
cond, expr!(:supports_condition_in_parens), op)
|
615
|
+
end
|
616
|
+
cond
|
617
|
+
end
|
618
|
+
|
619
|
+
def supports_declaration
|
620
|
+
name = sass_script(:parse)
|
621
|
+
tok!(/:/); ss
|
622
|
+
value = sass_script(:parse)
|
623
|
+
Sass::Supports::Declaration.new(name, value)
|
624
|
+
end
|
625
|
+
|
626
|
+
def supports_condition_in_parens
|
627
|
+
interp = supports_interpolation
|
628
|
+
return interp if interp
|
629
|
+
return unless tok(/\(/); ss
|
630
|
+
if (cond = supports_condition)
|
631
|
+
tok!(/\)/); ss
|
632
|
+
cond
|
633
|
+
else
|
634
|
+
decl = supports_declaration
|
635
|
+
tok!(/\)/); ss
|
636
|
+
decl
|
637
|
+
end
|
638
|
+
end
|
639
|
+
|
640
|
+
def supports_interpolation
|
641
|
+
interp = interpolation
|
642
|
+
return unless interp
|
643
|
+
ss
|
644
|
+
Sass::Supports::Interpolation.new(interp)
|
645
|
+
end
|
646
|
+
|
647
|
+
def variable
|
648
|
+
return unless tok(/\$/)
|
649
|
+
start_pos = source_position
|
650
|
+
name = ident!
|
651
|
+
ss; tok!(/:/); ss
|
652
|
+
|
653
|
+
expr = sass_script(:parse)
|
654
|
+
while tok(/!/)
|
655
|
+
flag_name = ident!
|
656
|
+
if flag_name == 'default'
|
657
|
+
guarded ||= true
|
658
|
+
elsif flag_name == 'global'
|
659
|
+
global ||= true
|
660
|
+
else
|
661
|
+
raise Sass::SyntaxError.new("Invalid flag \"!#{flag_name}\".", :line => @line)
|
662
|
+
end
|
663
|
+
ss
|
664
|
+
end
|
665
|
+
|
666
|
+
result = Sass::Tree::VariableNode.new(name, expr, guarded, global)
|
667
|
+
node(result, start_pos)
|
668
|
+
end
|
669
|
+
|
670
|
+
def operator
|
671
|
+
# Many of these operators (all except / and ,)
|
672
|
+
# are disallowed by the CSS spec,
|
673
|
+
# but they're included here for compatibility
|
674
|
+
# with some proprietary MS properties
|
675
|
+
str {ss if tok(%r{[/,:.=]})}
|
676
|
+
end
|
677
|
+
|
678
|
+
def ruleset
|
679
|
+
start_pos = source_position
|
680
|
+
return unless (rules = almost_any_value)
|
681
|
+
block(
|
682
|
+
node(
|
683
|
+
Sass::Tree::RuleNode.new(rules, range(start_pos)), start_pos), :ruleset)
|
684
|
+
end
|
685
|
+
|
686
|
+
def block(node, context)
|
687
|
+
node.has_children = true
|
688
|
+
tok!(/\{/)
|
689
|
+
block_contents(node, context)
|
690
|
+
tok!(/\}/)
|
691
|
+
node
|
692
|
+
end
|
693
|
+
|
694
|
+
# A block may contain declarations and/or rulesets
|
695
|
+
def block_contents(node, context)
|
696
|
+
block_given? ? yield : ss_comments(node)
|
697
|
+
node << (child = block_child(context))
|
698
|
+
while tok(/;/) || has_children?(child)
|
699
|
+
block_given? ? yield : ss_comments(node)
|
700
|
+
node << (child = block_child(context))
|
701
|
+
end
|
702
|
+
node
|
703
|
+
end
|
704
|
+
|
705
|
+
def block_child(context)
|
706
|
+
return variable || directive if context == :function
|
707
|
+
return variable || directive || ruleset if context == :stylesheet
|
708
|
+
variable || directive || declaration_or_ruleset
|
709
|
+
end
|
710
|
+
|
711
|
+
def has_children?(child_or_array)
|
712
|
+
return false unless child_or_array
|
713
|
+
return child_or_array.last.has_children if child_or_array.is_a?(Array)
|
714
|
+
child_or_array.has_children
|
715
|
+
end
|
716
|
+
|
717
|
+
# When parsing the contents of a ruleset, it can be difficult to tell
|
718
|
+
# declarations apart from nested rulesets. Since we don't thoroughly parse
|
719
|
+
# selectors until after resolving interpolation, we can share a bunch of
|
720
|
+
# the parsing of the two, but we need to disambiguate them first. We use
|
721
|
+
# the following criteria:
|
722
|
+
#
|
723
|
+
# * If the entity doesn't start with an identifier followed by a colon,
|
724
|
+
# it's a selector. There are some additional mostly-unimportant cases
|
725
|
+
# here to support various declaration hacks.
|
726
|
+
#
|
727
|
+
# * If the colon is followed by another colon, it's a selector.
|
728
|
+
#
|
729
|
+
# * Otherwise, if the colon is followed by anything other than
|
730
|
+
# interpolation or a character that's valid as the beginning of an
|
731
|
+
# identifier, it's a declaration.
|
732
|
+
#
|
733
|
+
# * If the colon is followed by interpolation or a valid identifier, try
|
734
|
+
# parsing it as a declaration value. If this fails, backtrack and parse
|
735
|
+
# it as a selector.
|
736
|
+
#
|
737
|
+
# * If the declaration value value valid but is followed by "{", backtrack
|
738
|
+
# and parse it as a selector anyway. This ensures that ".foo:bar {" is
|
739
|
+
# always parsed as a selector and never as a property with nested
|
740
|
+
# properties beneath it.
|
741
|
+
def declaration_or_ruleset
|
742
|
+
start_pos = source_position
|
743
|
+
declaration = try_declaration
|
744
|
+
|
745
|
+
if declaration.nil?
|
746
|
+
return unless (selector = almost_any_value)
|
747
|
+
elsif declaration.is_a?(Array)
|
748
|
+
selector = declaration
|
749
|
+
else
|
750
|
+
# Declaration should be a PropNode.
|
751
|
+
return declaration
|
752
|
+
end
|
753
|
+
|
754
|
+
if (additional_selector = almost_any_value)
|
755
|
+
selector << additional_selector
|
756
|
+
end
|
757
|
+
|
758
|
+
block(
|
759
|
+
node(
|
760
|
+
Sass::Tree::RuleNode.new(merge(selector), range(start_pos)), start_pos), :ruleset)
|
761
|
+
end
|
762
|
+
|
763
|
+
# Tries to parse a declaration, and returns the value parsed so far if it
|
764
|
+
# fails.
|
765
|
+
#
|
766
|
+
# This has three possible return types. It can return `nil`, indicating
|
767
|
+
# that parsing failed completely and the scanner hasn't moved forward at
|
768
|
+
# all. It can return an Array, indicating that parsing failed after
|
769
|
+
# consuming some text (possibly containing interpolation), which is
|
770
|
+
# returned. Or it can return a PropNode, indicating that parsing
|
771
|
+
# succeeded.
|
772
|
+
def try_declaration
|
773
|
+
# This allows the "*prop: val", ":prop: val", "#prop: val", and ".prop:
|
774
|
+
# val" hacks.
|
775
|
+
name_start_pos = source_position
|
776
|
+
if (s = tok(/[:\*\.]|\#(?!\{)/))
|
777
|
+
name = [s, str {ss}]
|
778
|
+
return name unless (ident = interp_ident)
|
779
|
+
name << ident
|
780
|
+
else
|
781
|
+
return unless (name = interp_ident)
|
782
|
+
name = Array(name)
|
783
|
+
end
|
784
|
+
|
785
|
+
if (comment = tok(COMMENT))
|
786
|
+
name << comment
|
787
|
+
end
|
788
|
+
name_end_pos = source_position
|
789
|
+
|
790
|
+
mid = [str {ss}]
|
791
|
+
return name + mid unless tok(/:/)
|
792
|
+
mid << ':'
|
793
|
+
|
794
|
+
# If this is a CSS variable, parse it as a property no matter what.
|
795
|
+
if name.first.is_a?(String) && name.first.start_with?("--")
|
796
|
+
return css_variable_declaration(name, name_start_pos, name_end_pos)
|
797
|
+
end
|
798
|
+
|
799
|
+
return name + mid + [':'] if tok(/:/)
|
800
|
+
mid << str {ss}
|
801
|
+
post_colon_whitespace = !mid.last.empty?
|
802
|
+
could_be_selector = !post_colon_whitespace && (tok?(IDENT_START) || tok?(INTERP_START))
|
803
|
+
|
804
|
+
value_start_pos = source_position
|
805
|
+
value = nil
|
806
|
+
error = catch_error do
|
807
|
+
value = value!
|
808
|
+
if tok?(/\{/)
|
809
|
+
# Properties that are ambiguous with selectors can't have additional
|
810
|
+
# properties nested beneath them.
|
811
|
+
tok!(/;/) if could_be_selector
|
812
|
+
elsif !tok?(/[;{}]/)
|
813
|
+
# We want an exception if there's no valid end-of-property character
|
814
|
+
# exists, but we don't want to consume it if it does.
|
815
|
+
tok!(/[;{}]/)
|
816
|
+
end
|
817
|
+
end
|
818
|
+
|
819
|
+
if error
|
820
|
+
rethrow error unless could_be_selector
|
821
|
+
|
822
|
+
# If the value would be followed by a semicolon, it's definitely
|
823
|
+
# supposed to be a property, not a selector.
|
824
|
+
additional_selector = almost_any_value
|
825
|
+
rethrow error if tok?(/;/)
|
826
|
+
|
827
|
+
return name + mid + (additional_selector || [])
|
828
|
+
end
|
829
|
+
|
830
|
+
value_end_pos = source_position
|
831
|
+
ss
|
832
|
+
require_block = tok?(/\{/)
|
833
|
+
|
834
|
+
node = node(Sass::Tree::PropNode.new(name.flatten.compact, [value], :new),
|
835
|
+
name_start_pos, value_end_pos)
|
836
|
+
node.name_source_range = range(name_start_pos, name_end_pos)
|
837
|
+
node.value_source_range = range(value_start_pos, value_end_pos)
|
838
|
+
|
839
|
+
return node unless require_block
|
840
|
+
nested_properties! node
|
841
|
+
end
|
842
|
+
|
843
|
+
def css_variable_declaration(name, name_start_pos, name_end_pos)
|
844
|
+
value_start_pos = source_position
|
845
|
+
value = declaration_value
|
846
|
+
value_end_pos = source_position
|
847
|
+
|
848
|
+
node = node(Sass::Tree::PropNode.new(name.flatten.compact, value, :new),
|
849
|
+
name_start_pos, value_end_pos)
|
850
|
+
node.name_source_range = range(name_start_pos, name_end_pos)
|
851
|
+
node.value_source_range = range(value_start_pos, value_end_pos)
|
852
|
+
node
|
853
|
+
end
|
854
|
+
|
855
|
+
# This production consumes values that could be a selector, an expression,
|
856
|
+
# or a combination of both. It respects strings and comments and supports
|
857
|
+
# interpolation. It will consume up to "{", "}", ";", or "!".
|
858
|
+
#
|
859
|
+
# Values consumed by this production will usually be parsed more
|
860
|
+
# thoroughly once interpolation has been resolved.
|
861
|
+
def almost_any_value
|
862
|
+
return unless (tok = almost_any_value_token)
|
863
|
+
sel = [tok]
|
864
|
+
while (tok = almost_any_value_token)
|
865
|
+
sel << tok
|
866
|
+
end
|
867
|
+
merge(sel)
|
868
|
+
end
|
869
|
+
|
870
|
+
def almost_any_value_token
|
871
|
+
tok(%r{
|
872
|
+
(
|
873
|
+
\\.
|
874
|
+
|
|
875
|
+
(?!url\()
|
876
|
+
[^"'/\#!;\{\}] # "
|
877
|
+
|
|
878
|
+
# interp_uri will handle most url() calls, but not ones that take strings
|
879
|
+
url\(#{W}(?=")
|
880
|
+
|
|
881
|
+
/(?![/*])
|
882
|
+
|
|
883
|
+
\#(?!\{)
|
884
|
+
|
|
885
|
+
!(?![a-z]) # TODO: never consume "!" when issue 1126 is fixed.
|
886
|
+
)+
|
887
|
+
}xi) || tok(COMMENT) || tok(SINGLE_LINE_COMMENT) || interp_string || interp_uri ||
|
888
|
+
interpolation(:warn_for_color)
|
889
|
+
end
|
890
|
+
|
891
|
+
def declaration_value(top_level: true)
|
892
|
+
return unless (tok = declaration_value_token(top_level))
|
893
|
+
value = [tok]
|
894
|
+
while (tok = declaration_value_token(top_level))
|
895
|
+
value << tok
|
896
|
+
end
|
897
|
+
merge(value)
|
898
|
+
end
|
899
|
+
|
900
|
+
def declaration_value_token(top_level)
|
901
|
+
# This comes, more or less, from the [token consumption algorithm][].
|
902
|
+
# However, since we don't have to worry about the token semantics, we
|
903
|
+
# just consume everything until we come across a token with special
|
904
|
+
# semantics.
|
905
|
+
#
|
906
|
+
# [token consumption algorithm]: https://drafts.csswg.org/css-syntax-3/#consume-token.
|
907
|
+
result = tok(%r{
|
908
|
+
(
|
909
|
+
(?!
|
910
|
+
url\(
|
911
|
+
)
|
912
|
+
[^()\[\]{}"'#/ \t\r\n\f#{top_level ? ";" : ""}]
|
913
|
+
|
|
914
|
+
\#(?!\{)
|
915
|
+
|
|
916
|
+
/(?!\*)
|
917
|
+
)+
|
918
|
+
}xi) || interp_string || interp_uri || interpolation || tok(COMMENT)
|
919
|
+
return result if result
|
920
|
+
|
921
|
+
# Fold together multiple characters of whitespace that don't include
|
922
|
+
# newlines. The value only cares about the tokenization, so this is safe
|
923
|
+
# as long as we don't delete whitespace entirely. It's important that we
|
924
|
+
# fold here rather than post-processing, since we aren't allowed to fold
|
925
|
+
# whitespace within strings and we lose that context later on.
|
926
|
+
if (ws = tok(S))
|
927
|
+
return ws.include?("\n") ? ws.gsub(/\A[^\n]*/, '') : ' '
|
928
|
+
end
|
929
|
+
|
930
|
+
if tok(/\(/)
|
931
|
+
value = declaration_value(top_level: false)
|
932
|
+
tok!(/\)/)
|
933
|
+
['(', *value, ')']
|
934
|
+
elsif tok(/\[/)
|
935
|
+
value = declaration_value(top_level: false)
|
936
|
+
tok!(/\]/)
|
937
|
+
['[', *value, ']']
|
938
|
+
elsif tok(/\{/)
|
939
|
+
value = declaration_value(top_level: false)
|
940
|
+
tok!(/\}/)
|
941
|
+
['{', *value, '}']
|
942
|
+
end
|
943
|
+
end
|
944
|
+
|
945
|
+
def declaration
|
946
|
+
# This allows the "*prop: val", ":prop: val", "#prop: val", and ".prop:
|
947
|
+
# val" hacks.
|
948
|
+
name_start_pos = source_position
|
949
|
+
if (s = tok(/[:\*\.]|\#(?!\{)/))
|
950
|
+
name = [s, str {ss}, *expr!(:interp_ident)]
|
951
|
+
else
|
952
|
+
return unless (name = interp_ident)
|
953
|
+
name = Array(name)
|
954
|
+
end
|
955
|
+
|
956
|
+
if (comment = tok(COMMENT))
|
957
|
+
name << comment
|
958
|
+
end
|
959
|
+
name_end_pos = source_position
|
960
|
+
ss
|
961
|
+
|
962
|
+
tok!(/:/)
|
963
|
+
ss
|
964
|
+
value_start_pos = source_position
|
965
|
+
value = value!
|
966
|
+
value_end_pos = source_position
|
967
|
+
ss
|
968
|
+
require_block = tok?(/\{/)
|
969
|
+
|
970
|
+
node = node(Sass::Tree::PropNode.new(name.flatten.compact, [value], :new),
|
971
|
+
name_start_pos, value_end_pos)
|
972
|
+
node.name_source_range = range(name_start_pos, name_end_pos)
|
973
|
+
node.value_source_range = range(value_start_pos, value_end_pos)
|
974
|
+
|
975
|
+
return node unless require_block
|
976
|
+
nested_properties! node
|
977
|
+
end
|
978
|
+
|
979
|
+
def value!
|
980
|
+
if tok?(/\{/)
|
981
|
+
str = Sass::Script::Tree::Literal.new(Sass::Script::Value::String.new(""))
|
982
|
+
str.line = source_position.line
|
983
|
+
str.source_range = range(source_position)
|
984
|
+
return str
|
985
|
+
end
|
986
|
+
|
987
|
+
start_pos = source_position
|
988
|
+
# This is a bit of a dirty trick:
|
989
|
+
# if the value is completely static,
|
990
|
+
# we don't parse it at all, and instead return a plain old string
|
991
|
+
# containing the value.
|
992
|
+
# This results in a dramatic speed increase.
|
993
|
+
if (val = tok(STATIC_VALUE))
|
994
|
+
# If val ends with escaped whitespace, leave it be.
|
995
|
+
str = Sass::Script::Tree::Literal.new(
|
996
|
+
Sass::Script::Value::String.new(
|
997
|
+
Sass::Util.strip_except_escapes(val)))
|
998
|
+
str.line = start_pos.line
|
999
|
+
str.source_range = range(start_pos)
|
1000
|
+
return str
|
1001
|
+
end
|
1002
|
+
sass_script(:parse)
|
1003
|
+
end
|
1004
|
+
|
1005
|
+
def nested_properties!(node)
|
1006
|
+
@expected = 'expression (e.g. 1px, bold) or "{"'
|
1007
|
+
block(node, :property)
|
1008
|
+
end
|
1009
|
+
|
1010
|
+
def expr(allow_var = true)
|
1011
|
+
t = term(allow_var)
|
1012
|
+
return unless t
|
1013
|
+
res = [t, str {ss}]
|
1014
|
+
|
1015
|
+
while (o = operator) && (t = term(allow_var))
|
1016
|
+
res << o << t << str {ss}
|
1017
|
+
end
|
1018
|
+
|
1019
|
+
res.flatten
|
1020
|
+
end
|
1021
|
+
|
1022
|
+
def term(allow_var)
|
1023
|
+
e = tok(NUMBER) ||
|
1024
|
+
interp_uri ||
|
1025
|
+
function(allow_var) ||
|
1026
|
+
interp_string ||
|
1027
|
+
tok(UNICODERANGE) ||
|
1028
|
+
interp_ident ||
|
1029
|
+
tok(HEXCOLOR) ||
|
1030
|
+
(allow_var && var_expr)
|
1031
|
+
return e if e
|
1032
|
+
|
1033
|
+
op = tok(/[+-]/)
|
1034
|
+
return unless op
|
1035
|
+
@expected = "number or function"
|
1036
|
+
[op,
|
1037
|
+
tok(NUMBER) || function(allow_var) || (allow_var && var_expr) || expr!(:interpolation)]
|
1038
|
+
end
|
1039
|
+
|
1040
|
+
def function(allow_var)
|
1041
|
+
name = tok(FUNCTION)
|
1042
|
+
return unless name
|
1043
|
+
if name == "expression(" || name == "calc("
|
1044
|
+
str, _ = Sass::Shared.balance(@scanner, ?(, ?), 1)
|
1045
|
+
[name, str]
|
1046
|
+
else
|
1047
|
+
[name, str {ss}, expr(allow_var), tok!(/\)/)]
|
1048
|
+
end
|
1049
|
+
end
|
1050
|
+
|
1051
|
+
def var_expr
|
1052
|
+
return unless tok(/\$/)
|
1053
|
+
line = @line
|
1054
|
+
var = Sass::Script::Tree::Variable.new(ident!)
|
1055
|
+
var.line = line
|
1056
|
+
var
|
1057
|
+
end
|
1058
|
+
|
1059
|
+
def interpolation(warn_for_color = false)
|
1060
|
+
return unless tok(INTERP_START)
|
1061
|
+
sass_script(:parse_interpolated, warn_for_color)
|
1062
|
+
end
|
1063
|
+
|
1064
|
+
def string
|
1065
|
+
return unless tok(STRING)
|
1066
|
+
Sass::Script::Value::String.value(@scanner[1] || @scanner[2])
|
1067
|
+
end
|
1068
|
+
|
1069
|
+
def interp_string
|
1070
|
+
_interp_string(:double) || _interp_string(:single)
|
1071
|
+
end
|
1072
|
+
|
1073
|
+
def interp_uri
|
1074
|
+
_interp_string(:uri)
|
1075
|
+
end
|
1076
|
+
|
1077
|
+
def _interp_string(type)
|
1078
|
+
start = tok(Sass::Script::Lexer::STRING_REGULAR_EXPRESSIONS[type][false])
|
1079
|
+
return unless start
|
1080
|
+
res = [start]
|
1081
|
+
|
1082
|
+
mid_re = Sass::Script::Lexer::STRING_REGULAR_EXPRESSIONS[type][true]
|
1083
|
+
# @scanner[2].empty? means we've started an interpolated section
|
1084
|
+
while @scanner[2] == '#{'
|
1085
|
+
@scanner.pos -= 2 # Don't consume the #{
|
1086
|
+
res.last.slice!(-2..-1)
|
1087
|
+
res << expr!(:interpolation) << tok(mid_re)
|
1088
|
+
end
|
1089
|
+
res
|
1090
|
+
end
|
1091
|
+
|
1092
|
+
def ident
|
1093
|
+
(ident = tok(IDENT)) && Sass::Util.normalize_ident_escapes(ident)
|
1094
|
+
end
|
1095
|
+
|
1096
|
+
def ident!
|
1097
|
+
Sass::Util.normalize_ident_escapes(tok!(IDENT))
|
1098
|
+
end
|
1099
|
+
|
1100
|
+
def name
|
1101
|
+
(name = tok(NAME)) && Sass::Util.normalize_ident_escapes(name)
|
1102
|
+
end
|
1103
|
+
|
1104
|
+
def name!
|
1105
|
+
Sass::Util.normalize_ident_escapes(tok!(NAME))
|
1106
|
+
end
|
1107
|
+
|
1108
|
+
def interp_ident
|
1109
|
+
val = ident || interpolation(:warn_for_color) || tok(IDENT_HYPHEN_INTERP)
|
1110
|
+
return unless val
|
1111
|
+
res = [val]
|
1112
|
+
while (val = name || interpolation(:warn_for_color))
|
1113
|
+
res << val
|
1114
|
+
end
|
1115
|
+
res
|
1116
|
+
end
|
1117
|
+
|
1118
|
+
def interp_ident_or_var
|
1119
|
+
id = interp_ident
|
1120
|
+
return id if id
|
1121
|
+
var = var_expr
|
1122
|
+
return [var] if var
|
1123
|
+
end
|
1124
|
+
|
1125
|
+
def str
|
1126
|
+
@strs.push String.new("")
|
1127
|
+
yield
|
1128
|
+
@strs.last
|
1129
|
+
ensure
|
1130
|
+
@strs.pop
|
1131
|
+
end
|
1132
|
+
|
1133
|
+
def str?
|
1134
|
+
pos = @scanner.pos
|
1135
|
+
line = @line
|
1136
|
+
offset = @offset
|
1137
|
+
@strs.push ""
|
1138
|
+
throw_error {yield} && @strs.last
|
1139
|
+
rescue Sass::SyntaxError
|
1140
|
+
@scanner.pos = pos
|
1141
|
+
@line = line
|
1142
|
+
@offset = offset
|
1143
|
+
nil
|
1144
|
+
ensure
|
1145
|
+
@strs.pop
|
1146
|
+
end
|
1147
|
+
|
1148
|
+
def node(node, start_pos, end_pos = source_position)
|
1149
|
+
node.line = start_pos.line
|
1150
|
+
node.source_range = range(start_pos, end_pos)
|
1151
|
+
node
|
1152
|
+
end
|
1153
|
+
|
1154
|
+
@sass_script_parser = Sass::Script::Parser
|
1155
|
+
|
1156
|
+
class << self
|
1157
|
+
# @private
|
1158
|
+
attr_accessor :sass_script_parser
|
1159
|
+
end
|
1160
|
+
|
1161
|
+
def sass_script(*args)
|
1162
|
+
parser = self.class.sass_script_parser.new(@scanner, @line, @offset,
|
1163
|
+
:filename => @filename, :importer => @importer, :allow_extra_text => true)
|
1164
|
+
result = parser.send(*args)
|
1165
|
+
unless @strs.empty?
|
1166
|
+
# Convert to CSS manually so that comments are ignored.
|
1167
|
+
src = result.to_sass
|
1168
|
+
@strs.each {|s| s << src}
|
1169
|
+
end
|
1170
|
+
@line = parser.line
|
1171
|
+
@offset = parser.offset
|
1172
|
+
result
|
1173
|
+
rescue Sass::SyntaxError => e
|
1174
|
+
throw(:_sass_parser_error, true) if @throw_error
|
1175
|
+
raise e
|
1176
|
+
end
|
1177
|
+
|
1178
|
+
def merge(arr)
|
1179
|
+
arr && Sass::Util.merge_adjacent_strings([arr].flatten)
|
1180
|
+
end
|
1181
|
+
|
1182
|
+
EXPR_NAMES = {
|
1183
|
+
:media_query => "media query (e.g. print, screen, print and screen)",
|
1184
|
+
:media_query_list => "media query (e.g. print, screen, print and screen)",
|
1185
|
+
:media_expr => "media expression (e.g. (min-device-width: 800px))",
|
1186
|
+
:at_root_query => "@at-root query (e.g. (without: media))",
|
1187
|
+
:at_root_directive_list => '* or identifier',
|
1188
|
+
:declaration_value => "expression (e.g. fr, 2n+1)",
|
1189
|
+
:interp_ident => "identifier",
|
1190
|
+
:qualified_name => "identifier",
|
1191
|
+
:expr => "expression (e.g. 1px, bold)",
|
1192
|
+
:selector_comma_sequence => "selector",
|
1193
|
+
:string => "string",
|
1194
|
+
:import_arg => "file to import (string or url())",
|
1195
|
+
:moz_document_function => "matching function (e.g. url-prefix(), domain())",
|
1196
|
+
:supports_condition => "@supports condition (e.g. (display: flexbox))",
|
1197
|
+
:supports_condition_in_parens => "@supports condition (e.g. (display: flexbox))",
|
1198
|
+
:a_n_plus_b => "An+B expression",
|
1199
|
+
:keyframes_selector_component => "from, to, or a percentage",
|
1200
|
+
:keyframes_selector => "keyframes selector (e.g. 10%)"
|
1201
|
+
}
|
1202
|
+
|
1203
|
+
TOK_NAMES = Hash[Sass::SCSS::RX.constants.map do |c|
|
1204
|
+
[Sass::SCSS::RX.const_get(c), c.downcase]
|
1205
|
+
end].merge(
|
1206
|
+
IDENT => "identifier",
|
1207
|
+
/[;{}]/ => '";"',
|
1208
|
+
/\b(without|with)\b/ => '"with" or "without"'
|
1209
|
+
)
|
1210
|
+
|
1211
|
+
def tok?(rx)
|
1212
|
+
@scanner.match?(rx)
|
1213
|
+
end
|
1214
|
+
|
1215
|
+
def expr!(name)
|
1216
|
+
e = send(name)
|
1217
|
+
return e if e
|
1218
|
+
expected(EXPR_NAMES[name] || name.to_s)
|
1219
|
+
end
|
1220
|
+
|
1221
|
+
def tok!(rx)
|
1222
|
+
t = tok(rx)
|
1223
|
+
return t if t
|
1224
|
+
name = TOK_NAMES[rx]
|
1225
|
+
|
1226
|
+
unless name
|
1227
|
+
# Display basic regexps as plain old strings
|
1228
|
+
source = rx.source.gsub(%r{\\/}, '/')
|
1229
|
+
string = rx.source.gsub(/\\(.)/, '\1')
|
1230
|
+
name = source == Regexp.escape(string) ? string.inspect : rx.inspect
|
1231
|
+
end
|
1232
|
+
|
1233
|
+
expected(name)
|
1234
|
+
end
|
1235
|
+
|
1236
|
+
def expected(name)
|
1237
|
+
throw(:_sass_parser_error, true) if @throw_error
|
1238
|
+
self.class.expected(@scanner, @expected || name, @line)
|
1239
|
+
end
|
1240
|
+
|
1241
|
+
def err(msg)
|
1242
|
+
throw(:_sass_parser_error, true) if @throw_error
|
1243
|
+
raise Sass::SyntaxError.new(msg, :line => @line)
|
1244
|
+
end
|
1245
|
+
|
1246
|
+
def throw_error
|
1247
|
+
old_throw_error, @throw_error = @throw_error, false
|
1248
|
+
yield
|
1249
|
+
ensure
|
1250
|
+
@throw_error = old_throw_error
|
1251
|
+
end
|
1252
|
+
|
1253
|
+
def catch_error(&block)
|
1254
|
+
old_throw_error, @throw_error = @throw_error, true
|
1255
|
+
pos = @scanner.pos
|
1256
|
+
line = @line
|
1257
|
+
offset = @offset
|
1258
|
+
expected = @expected
|
1259
|
+
|
1260
|
+
logger = Sass::Logger::Delayed.install!
|
1261
|
+
if catch(:_sass_parser_error) {yield; false}
|
1262
|
+
@scanner.pos = pos
|
1263
|
+
@line = line
|
1264
|
+
@offset = offset
|
1265
|
+
@expected = expected
|
1266
|
+
{:pos => pos, :line => line, :expected => @expected, :block => block}
|
1267
|
+
else
|
1268
|
+
logger.flush
|
1269
|
+
nil
|
1270
|
+
end
|
1271
|
+
ensure
|
1272
|
+
logger.uninstall! if logger
|
1273
|
+
@throw_error = old_throw_error
|
1274
|
+
end
|
1275
|
+
|
1276
|
+
def rethrow(err)
|
1277
|
+
if @throw_error
|
1278
|
+
throw :_sass_parser_error, err
|
1279
|
+
else
|
1280
|
+
@scanner = Sass::Util::MultibyteStringScanner.new(@scanner.string)
|
1281
|
+
@scanner.pos = err[:pos]
|
1282
|
+
@line = err[:line]
|
1283
|
+
@expected = err[:expected]
|
1284
|
+
err[:block].call
|
1285
|
+
end
|
1286
|
+
end
|
1287
|
+
|
1288
|
+
# @private
|
1289
|
+
def self.expected(scanner, expected, line)
|
1290
|
+
pos = scanner.pos
|
1291
|
+
|
1292
|
+
after = scanner.string[0...pos]
|
1293
|
+
# Get rid of whitespace between pos and the last token,
|
1294
|
+
# but only if there's a newline in there
|
1295
|
+
after.gsub!(/\s*\n\s*$/, '')
|
1296
|
+
# Also get rid of stuff before the last newline
|
1297
|
+
after.gsub!(/.*\n/, '')
|
1298
|
+
after = "..." + after[-15..-1] if after.size > 18
|
1299
|
+
|
1300
|
+
was = scanner.rest.dup
|
1301
|
+
# Get rid of whitespace between pos and the next token,
|
1302
|
+
# but only if there's a newline in there
|
1303
|
+
was.gsub!(/^\s*\n\s*/, '')
|
1304
|
+
# Also get rid of stuff after the next newline
|
1305
|
+
was.gsub!(/\n.*/, '')
|
1306
|
+
was = was[0...15] + "..." if was.size > 18
|
1307
|
+
|
1308
|
+
raise Sass::SyntaxError.new(
|
1309
|
+
"Invalid CSS after \"#{after}\": expected #{expected}, was \"#{was}\"",
|
1310
|
+
:line => line)
|
1311
|
+
end
|
1312
|
+
|
1313
|
+
# Avoid allocating lots of new strings for `#tok`.
|
1314
|
+
# This is important because `#tok` is called all the time.
|
1315
|
+
NEWLINE = "\n"
|
1316
|
+
|
1317
|
+
def tok(rx)
|
1318
|
+
res = @scanner.scan(rx)
|
1319
|
+
|
1320
|
+
return unless res
|
1321
|
+
|
1322
|
+
newline_count = res.count(NEWLINE)
|
1323
|
+
if newline_count > 0
|
1324
|
+
@line += newline_count
|
1325
|
+
@offset = res[res.rindex(NEWLINE)..-1].size
|
1326
|
+
else
|
1327
|
+
@offset += res.size
|
1328
|
+
end
|
1329
|
+
|
1330
|
+
@expected = nil
|
1331
|
+
if !@strs.empty? && rx != COMMENT && rx != SINGLE_LINE_COMMENT
|
1332
|
+
@strs.each {|s| s << res}
|
1333
|
+
end
|
1334
|
+
res
|
1335
|
+
end
|
1336
|
+
|
1337
|
+
# Remove a vendor prefix from `str`.
|
1338
|
+
def deprefix(str)
|
1339
|
+
str.gsub(/^-[a-zA-Z0-9]+-/, '')
|
1340
|
+
end
|
1341
|
+
end
|
1342
|
+
end
|
1343
|
+
end
|