xass 0.1.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 +11 -0
- data/CONTRIBUTING +3 -0
- data/MIT-LICENSE +20 -0
- data/README.md +201 -0
- data/Rakefile +349 -0
- data/VERSION +1 -0
- data/VERSION_NAME +1 -0
- data/bin/push +13 -0
- data/bin/sass +13 -0
- data/bin/sass-convert +12 -0
- data/bin/scss +13 -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 +33 -0
- data/lib/sass/cache_stores/filesystem.rb +64 -0
- data/lib/sass/cache_stores/memory.rb +47 -0
- data/lib/sass/cache_stores/null.rb +25 -0
- data/lib/sass/cache_stores.rb +15 -0
- data/lib/sass/callbacks.rb +66 -0
- data/lib/sass/css.rb +409 -0
- data/lib/sass/engine.rb +930 -0
- data/lib/sass/environment.rb +101 -0
- data/lib/sass/error.rb +201 -0
- data/lib/sass/exec.rb +707 -0
- data/lib/sass/importers/base.rb +139 -0
- data/lib/sass/importers/filesystem.rb +186 -0
- data/lib/sass/importers.rb +22 -0
- data/lib/sass/logger/base.rb +32 -0
- data/lib/sass/logger/log_level.rb +49 -0
- data/lib/sass/logger.rb +15 -0
- data/lib/sass/media.rb +213 -0
- data/lib/sass/plugin/compiler.rb +406 -0
- data/lib/sass/plugin/configuration.rb +123 -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 +133 -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/arg_list.rb +52 -0
- data/lib/sass/script/bool.rb +18 -0
- data/lib/sass/script/color.rb +606 -0
- data/lib/sass/script/css_lexer.rb +29 -0
- data/lib/sass/script/css_parser.rb +31 -0
- data/lib/sass/script/funcall.rb +245 -0
- data/lib/sass/script/functions.rb +1543 -0
- data/lib/sass/script/interpolation.rb +79 -0
- data/lib/sass/script/lexer.rb +345 -0
- data/lib/sass/script/list.rb +85 -0
- data/lib/sass/script/literal.rb +221 -0
- data/lib/sass/script/node.rb +99 -0
- data/lib/sass/script/null.rb +37 -0
- data/lib/sass/script/number.rb +453 -0
- data/lib/sass/script/operation.rb +110 -0
- data/lib/sass/script/parser.rb +502 -0
- data/lib/sass/script/string.rb +51 -0
- data/lib/sass/script/string_interpolation.rb +103 -0
- data/lib/sass/script/unary_operation.rb +69 -0
- data/lib/sass/script/variable.rb +58 -0
- data/lib/sass/script.rb +39 -0
- data/lib/sass/scss/css_parser.rb +36 -0
- data/lib/sass/scss/parser.rb +1180 -0
- data/lib/sass/scss/rx.rb +133 -0
- data/lib/sass/scss/script_lexer.rb +15 -0
- data/lib/sass/scss/script_parser.rb +25 -0
- data/lib/sass/scss/static_parser.rb +54 -0
- data/lib/sass/scss.rb +16 -0
- data/lib/sass/selector/abstract_sequence.rb +94 -0
- data/lib/sass/selector/comma_sequence.rb +92 -0
- data/lib/sass/selector/sequence.rb +507 -0
- data/lib/sass/selector/simple.rb +119 -0
- data/lib/sass/selector/simple_sequence.rb +215 -0
- data/lib/sass/selector.rb +452 -0
- data/lib/sass/shared.rb +76 -0
- data/lib/sass/supports.rb +229 -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 +60 -0
- data/lib/sass/tree/debug_node.rb +18 -0
- data/lib/sass/tree/directive_node.rb +42 -0
- data/lib/sass/tree/each_node.rb +24 -0
- data/lib/sass/tree/extend_node.rb +36 -0
- data/lib/sass/tree/for_node.rb +36 -0
- data/lib/sass/tree/function_node.rb +34 -0
- data/lib/sass/tree/if_node.rb +52 -0
- data/lib/sass/tree/import_node.rb +75 -0
- data/lib/sass/tree/media_node.rb +58 -0
- data/lib/sass/tree/mixin_def_node.rb +38 -0
- data/lib/sass/tree/mixin_node.rb +39 -0
- data/lib/sass/tree/node.rb +196 -0
- data/lib/sass/tree/prop_node.rb +152 -0
- data/lib/sass/tree/return_node.rb +18 -0
- data/lib/sass/tree/root_node.rb +78 -0
- data/lib/sass/tree/rule_node.rb +132 -0
- data/lib/sass/tree/supports_node.rb +51 -0
- data/lib/sass/tree/trace_node.rb +32 -0
- data/lib/sass/tree/variable_node.rb +30 -0
- data/lib/sass/tree/visitors/base.rb +75 -0
- data/lib/sass/tree/visitors/check_nesting.rb +147 -0
- data/lib/sass/tree/visitors/convert.rb +316 -0
- data/lib/sass/tree/visitors/cssize.rb +241 -0
- data/lib/sass/tree/visitors/deep_copy.rb +102 -0
- data/lib/sass/tree/visitors/extend.rb +68 -0
- data/lib/sass/tree/visitors/perform.rb +446 -0
- data/lib/sass/tree/visitors/set_options.rb +125 -0
- data/lib/sass/tree/visitors/to_css.rb +228 -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 +155 -0
- data/lib/sass/util/subset_map.rb +109 -0
- data/lib/sass/util/test.rb +10 -0
- data/lib/sass/util.rb +948 -0
- data/lib/sass/version.rb +126 -0
- data/lib/sass.rb +95 -0
- data/rails/init.rb +1 -0
- data/test/Gemfile +3 -0
- data/test/Gemfile.lock +10 -0
- data/test/sass/cache_test.rb +89 -0
- data/test/sass/callbacks_test.rb +61 -0
- data/test/sass/conversion_test.rb +1760 -0
- data/test/sass/css2sass_test.rb +458 -0
- data/test/sass/data/hsl-rgb.txt +319 -0
- data/test/sass/engine_test.rb +3244 -0
- data/test/sass/exec_test.rb +86 -0
- data/test/sass/extend_test.rb +1482 -0
- data/test/sass/fixtures/test_staleness_check_across_importers.css +1 -0
- data/test/sass/fixtures/test_staleness_check_across_importers.scss +1 -0
- data/test/sass/functions_test.rb +1139 -0
- data/test/sass/importer_test.rb +192 -0
- data/test/sass/logger_test.rb +58 -0
- data/test/sass/mock_importer.rb +49 -0
- data/test/sass/more_results/more1.css +9 -0
- data/test/sass/more_results/more1_with_line_comments.css +26 -0
- data/test/sass/more_results/more_import.css +29 -0
- data/test/sass/more_templates/_more_partial.sass +2 -0
- data/test/sass/more_templates/more1.sass +23 -0
- data/test/sass/more_templates/more_import.sass +11 -0
- data/test/sass/plugin_test.rb +564 -0
- data/test/sass/results/alt.css +4 -0
- data/test/sass/results/basic.css +9 -0
- data/test/sass/results/cached_import_option.css +3 -0
- data/test/sass/results/compact.css +5 -0
- data/test/sass/results/complex.css +86 -0
- data/test/sass/results/compressed.css +1 -0
- data/test/sass/results/expanded.css +19 -0
- data/test/sass/results/filename_fn.css +3 -0
- data/test/sass/results/if.css +3 -0
- data/test/sass/results/import.css +31 -0
- data/test/sass/results/import_charset.css +5 -0
- data/test/sass/results/import_charset_1_8.css +5 -0
- data/test/sass/results/import_charset_ibm866.css +5 -0
- data/test/sass/results/import_content.css +1 -0
- data/test/sass/results/line_numbers.css +49 -0
- data/test/sass/results/mixins.css +95 -0
- data/test/sass/results/multiline.css +24 -0
- data/test/sass/results/nested.css +22 -0
- data/test/sass/results/options.css +1 -0
- data/test/sass/results/parent_ref.css +13 -0
- data/test/sass/results/script.css +16 -0
- data/test/sass/results/scss_import.css +31 -0
- data/test/sass/results/scss_importee.css +2 -0
- data/test/sass/results/subdir/nested_subdir/nested_subdir.css +1 -0
- data/test/sass/results/subdir/subdir.css +3 -0
- data/test/sass/results/units.css +11 -0
- data/test/sass/results/warn.css +0 -0
- data/test/sass/results/warn_imported.css +0 -0
- data/test/sass/script_conversion_test.rb +299 -0
- data/test/sass/script_test.rb +622 -0
- data/test/sass/scss/css_test.rb +1100 -0
- data/test/sass/scss/rx_test.rb +156 -0
- data/test/sass/scss/scss_test.rb +2106 -0
- data/test/sass/scss/test_helper.rb +37 -0
- data/test/sass/templates/_cached_import_option_partial.scss +1 -0
- data/test/sass/templates/_double_import_loop2.sass +1 -0
- data/test/sass/templates/_filename_fn_import.scss +11 -0
- data/test/sass/templates/_imported_charset_ibm866.sass +4 -0
- data/test/sass/templates/_imported_charset_utf8.sass +4 -0
- data/test/sass/templates/_imported_content.sass +3 -0
- data/test/sass/templates/_partial.sass +2 -0
- data/test/sass/templates/_same_name_different_partiality.scss +1 -0
- data/test/sass/templates/alt.sass +16 -0
- data/test/sass/templates/basic.sass +23 -0
- data/test/sass/templates/bork1.sass +2 -0
- data/test/sass/templates/bork2.sass +2 -0
- data/test/sass/templates/bork3.sass +2 -0
- data/test/sass/templates/bork4.sass +2 -0
- data/test/sass/templates/bork5.sass +3 -0
- data/test/sass/templates/cached_import_option.scss +3 -0
- data/test/sass/templates/compact.sass +17 -0
- data/test/sass/templates/complex.sass +305 -0
- data/test/sass/templates/compressed.sass +15 -0
- data/test/sass/templates/double_import_loop1.sass +1 -0
- data/test/sass/templates/expanded.sass +17 -0
- data/test/sass/templates/filename_fn.scss +18 -0
- data/test/sass/templates/if.sass +11 -0
- data/test/sass/templates/import.sass +12 -0
- data/test/sass/templates/import_charset.sass +9 -0
- data/test/sass/templates/import_charset_1_8.sass +6 -0
- data/test/sass/templates/import_charset_ibm866.sass +11 -0
- data/test/sass/templates/import_content.sass +4 -0
- data/test/sass/templates/importee.less +2 -0
- data/test/sass/templates/importee.sass +19 -0
- data/test/sass/templates/line_numbers.sass +13 -0
- data/test/sass/templates/mixin_bork.sass +5 -0
- data/test/sass/templates/mixins.sass +76 -0
- data/test/sass/templates/multiline.sass +20 -0
- data/test/sass/templates/nested.sass +25 -0
- data/test/sass/templates/nested_bork1.sass +2 -0
- data/test/sass/templates/nested_bork2.sass +2 -0
- data/test/sass/templates/nested_bork3.sass +2 -0
- data/test/sass/templates/nested_bork4.sass +2 -0
- data/test/sass/templates/nested_import.sass +2 -0
- data/test/sass/templates/nested_mixin_bork.sass +6 -0
- data/test/sass/templates/options.sass +2 -0
- data/test/sass/templates/parent_ref.sass +25 -0
- data/test/sass/templates/same_name_different_ext.sass +2 -0
- data/test/sass/templates/same_name_different_ext.scss +1 -0
- data/test/sass/templates/same_name_different_partiality.scss +1 -0
- data/test/sass/templates/script.sass +101 -0
- data/test/sass/templates/scss_import.scss +11 -0
- data/test/sass/templates/scss_importee.scss +1 -0
- data/test/sass/templates/single_import_loop.sass +1 -0
- data/test/sass/templates/subdir/import_up1.scss +1 -0
- data/test/sass/templates/subdir/import_up2.scss +1 -0
- data/test/sass/templates/subdir/nested_subdir/_nested_partial.sass +2 -0
- data/test/sass/templates/subdir/nested_subdir/nested_subdir.sass +3 -0
- data/test/sass/templates/subdir/subdir.sass +6 -0
- data/test/sass/templates/units.sass +11 -0
- data/test/sass/templates/warn.sass +3 -0
- data/test/sass/templates/warn_imported.sass +4 -0
- data/test/sass/test_helper.rb +8 -0
- data/test/sass/util/multibyte_string_scanner_test.rb +147 -0
- data/test/sass/util/subset_map_test.rb +91 -0
- data/test/sass/util_test.rb +382 -0
- data/test/test_helper.rb +80 -0
- metadata +354 -0
|
@@ -0,0 +1,1180 @@
|
|
|
1
|
+
require 'set'
|
|
2
|
+
|
|
3
|
+
module Sass
|
|
4
|
+
module SCSS
|
|
5
|
+
# The parser for SCSS.
|
|
6
|
+
# It parses a string of code into a tree of {Sass::Tree::Node}s.
|
|
7
|
+
class Parser
|
|
8
|
+
# @param str [String, StringScanner] The source document to parse.
|
|
9
|
+
# Note that `Parser` *won't* raise a nice error message if this isn't properly parsed;
|
|
10
|
+
# for that, you should use the higher-level {Sass::Engine} or {Sass::CSS}.
|
|
11
|
+
# @param filename [String] The name of the file being parsed. Used for warnings.
|
|
12
|
+
# @param line [Fixnum] The line on which the source string appeared,
|
|
13
|
+
# if it's part of another document.
|
|
14
|
+
def initialize(str, filename, line = 1)
|
|
15
|
+
@template = str
|
|
16
|
+
@filename = filename
|
|
17
|
+
@line = line
|
|
18
|
+
@strs = []
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Parses an SCSS document.
|
|
22
|
+
#
|
|
23
|
+
# @return [Sass::Tree::RootNode] The root node of the document tree
|
|
24
|
+
# @raise [Sass::SyntaxError] if there's a syntax error in the document
|
|
25
|
+
def parse
|
|
26
|
+
init_scanner!
|
|
27
|
+
root = stylesheet
|
|
28
|
+
expected("selector or at-rule") unless @scanner.eos?
|
|
29
|
+
root
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Parses an identifier with interpolation.
|
|
33
|
+
# Note that this won't assert that the identifier takes up the entire input string;
|
|
34
|
+
# it's meant to be used with `StringScanner`s as part of other parsers.
|
|
35
|
+
#
|
|
36
|
+
# @return [Array<String, Sass::Script::Node>, nil]
|
|
37
|
+
# The interpolated identifier, or nil if none could be parsed
|
|
38
|
+
def parse_interp_ident
|
|
39
|
+
init_scanner!
|
|
40
|
+
interp_ident
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Parses a media query list.
|
|
44
|
+
#
|
|
45
|
+
# @return [Sass::Media::QueryList] The parsed query list
|
|
46
|
+
# @raise [Sass::SyntaxError] if there's a syntax error in the query list,
|
|
47
|
+
# or if it doesn't take up the entire input string.
|
|
48
|
+
def parse_media_query_list
|
|
49
|
+
init_scanner!
|
|
50
|
+
ql = media_query_list
|
|
51
|
+
expected("media query list") unless @scanner.eos?
|
|
52
|
+
ql
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Parses a supports query condition.
|
|
56
|
+
#
|
|
57
|
+
# @return [Sass::Supports::Condition] The parsed condition
|
|
58
|
+
# @raise [Sass::SyntaxError] if there's a syntax error in the condition,
|
|
59
|
+
# or if it doesn't take up the entire input string.
|
|
60
|
+
def parse_supports_condition
|
|
61
|
+
init_scanner!
|
|
62
|
+
condition = supports_condition
|
|
63
|
+
expected("supports condition") unless @scanner.eos?
|
|
64
|
+
condition
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
include Sass::SCSS::RX
|
|
70
|
+
|
|
71
|
+
def init_scanner!
|
|
72
|
+
@scanner =
|
|
73
|
+
if @template.is_a?(StringScanner)
|
|
74
|
+
@template
|
|
75
|
+
else
|
|
76
|
+
Sass::Util::MultibyteStringScanner.new(@template.gsub("\r", ""))
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def stylesheet
|
|
81
|
+
node = node(Sass::Tree::RootNode.new(@scanner.string))
|
|
82
|
+
block_contents(node, :stylesheet) {s(node)}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def s(node)
|
|
86
|
+
while tok(S) || tok(CDC) || tok(CDO) || (c = tok(SINGLE_LINE_COMMENT)) || (c = tok(COMMENT))
|
|
87
|
+
next unless c
|
|
88
|
+
process_comment c, node
|
|
89
|
+
c = nil
|
|
90
|
+
end
|
|
91
|
+
true
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def ss
|
|
95
|
+
nil while tok(S) || tok(SINGLE_LINE_COMMENT) || tok(COMMENT)
|
|
96
|
+
true
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def ss_comments(node)
|
|
100
|
+
while tok(S) || (c = tok(SINGLE_LINE_COMMENT)) || (c = tok(COMMENT))
|
|
101
|
+
next unless c
|
|
102
|
+
process_comment c, node
|
|
103
|
+
c = nil
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
true
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def whitespace
|
|
110
|
+
return unless tok(S) || tok(SINGLE_LINE_COMMENT) || tok(COMMENT)
|
|
111
|
+
ss
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def process_comment(text, node)
|
|
115
|
+
silent = text =~ /^\/\//
|
|
116
|
+
loud = !silent && text =~ %r{^/[/*]!}
|
|
117
|
+
line = @line - text.count("\n")
|
|
118
|
+
|
|
119
|
+
if silent
|
|
120
|
+
value = [text.sub(/^\s*\/\//, '/*').gsub(/^\s*\/\//, ' *') + ' */']
|
|
121
|
+
else
|
|
122
|
+
value = Sass::Engine.parse_interp(text, line, @scanner.pos - text.size, :filename => @filename)
|
|
123
|
+
value.unshift(@scanner.
|
|
124
|
+
string[0...@scanner.pos].
|
|
125
|
+
reverse[/.*?\*\/(.*?)($|\Z)/, 1].
|
|
126
|
+
reverse.gsub(/[^\s]/, ' '))
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
type = if silent then :silent elsif loud then :loud else :normal end
|
|
130
|
+
comment = Sass::Tree::CommentNode.new(value, type)
|
|
131
|
+
comment.line = line
|
|
132
|
+
node << comment
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
DIRECTIVES = Set[:mixin, :include, :function, :return, :debug, :warn, :for,
|
|
136
|
+
:each, :while, :if, :else, :extend, :import, :media, :charset, :content,
|
|
137
|
+
:_moz_document]
|
|
138
|
+
|
|
139
|
+
PREFIXED_DIRECTIVES = Set[:supports]
|
|
140
|
+
|
|
141
|
+
def directive
|
|
142
|
+
return unless tok(/@/)
|
|
143
|
+
name = tok!(IDENT)
|
|
144
|
+
ss
|
|
145
|
+
|
|
146
|
+
if dir = special_directive(name)
|
|
147
|
+
return dir
|
|
148
|
+
elsif dir = prefixed_directive(name)
|
|
149
|
+
return dir
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Most at-rules take expressions (e.g. @import),
|
|
153
|
+
# but some (e.g. @page) take selector-like arguments.
|
|
154
|
+
# Some take no arguments at all.
|
|
155
|
+
val = expr || selector
|
|
156
|
+
val = val ? ["@#{name} "] + Sass::Util.strip_string_array(val) : ["@#{name}"]
|
|
157
|
+
directive_body(val)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def directive_body(value)
|
|
161
|
+
node = node(Sass::Tree::DirectiveNode.new(value))
|
|
162
|
+
|
|
163
|
+
if tok(/\{/)
|
|
164
|
+
node.has_children = true
|
|
165
|
+
block_contents(node, :directive)
|
|
166
|
+
tok!(/\}/)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
node
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def special_directive(name)
|
|
173
|
+
sym = name.gsub('-', '_').to_sym
|
|
174
|
+
DIRECTIVES.include?(sym) && send("#{sym}_directive")
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def prefixed_directive(name)
|
|
178
|
+
sym = name.gsub(/^-[a-z0-9]+-/i, '').gsub('-', '_').to_sym
|
|
179
|
+
PREFIXED_DIRECTIVES.include?(sym) && send("#{sym}_directive", name)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def mixin_directive
|
|
183
|
+
name = tok! IDENT
|
|
184
|
+
args, splat = sass_script(:parse_mixin_definition_arglist)
|
|
185
|
+
ss
|
|
186
|
+
block(node(Sass::Tree::MixinDefNode.new(name, args, splat)), :directive)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def include_directive
|
|
190
|
+
name = tok! IDENT
|
|
191
|
+
args, keywords, splat = sass_script(:parse_mixin_include_arglist)
|
|
192
|
+
ss
|
|
193
|
+
include_node = node(Sass::Tree::MixinNode.new(name, args, keywords, splat))
|
|
194
|
+
if tok?(/\{/)
|
|
195
|
+
include_node.has_children = true
|
|
196
|
+
block(include_node, :directive)
|
|
197
|
+
else
|
|
198
|
+
include_node
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def content_directive
|
|
203
|
+
ss
|
|
204
|
+
node(Sass::Tree::ContentNode.new)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def function_directive
|
|
208
|
+
name = tok! IDENT
|
|
209
|
+
args, splat = sass_script(:parse_function_definition_arglist)
|
|
210
|
+
ss
|
|
211
|
+
block(node(Sass::Tree::FunctionNode.new(name, args, splat)), :function)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def return_directive
|
|
215
|
+
node(Sass::Tree::ReturnNode.new(sass_script(:parse)))
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def debug_directive
|
|
219
|
+
node(Sass::Tree::DebugNode.new(sass_script(:parse)))
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def warn_directive
|
|
223
|
+
node(Sass::Tree::WarnNode.new(sass_script(:parse)))
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def for_directive
|
|
227
|
+
tok!(/\$/)
|
|
228
|
+
var = tok! IDENT
|
|
229
|
+
ss
|
|
230
|
+
|
|
231
|
+
tok!(/from/)
|
|
232
|
+
from = sass_script(:parse_until, Set["to", "through"])
|
|
233
|
+
ss
|
|
234
|
+
|
|
235
|
+
@expected = '"to" or "through"'
|
|
236
|
+
exclusive = (tok(/to/) || tok!(/through/)) == 'to'
|
|
237
|
+
to = sass_script(:parse)
|
|
238
|
+
ss
|
|
239
|
+
|
|
240
|
+
block(node(Sass::Tree::ForNode.new(var, from, to, exclusive)), :directive)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def each_directive
|
|
244
|
+
tok!(/\$/)
|
|
245
|
+
var = tok! IDENT
|
|
246
|
+
ss
|
|
247
|
+
|
|
248
|
+
tok!(/in/)
|
|
249
|
+
list = sass_script(:parse)
|
|
250
|
+
ss
|
|
251
|
+
|
|
252
|
+
block(node(Sass::Tree::EachNode.new(var, list)), :directive)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def while_directive
|
|
256
|
+
expr = sass_script(:parse)
|
|
257
|
+
ss
|
|
258
|
+
block(node(Sass::Tree::WhileNode.new(expr)), :directive)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def if_directive
|
|
262
|
+
expr = sass_script(:parse)
|
|
263
|
+
ss
|
|
264
|
+
node = block(node(Sass::Tree::IfNode.new(expr)), :directive)
|
|
265
|
+
pos = @scanner.pos
|
|
266
|
+
line = @line
|
|
267
|
+
ss
|
|
268
|
+
|
|
269
|
+
else_block(node) ||
|
|
270
|
+
begin
|
|
271
|
+
# Backtrack in case there are any comments we want to parse
|
|
272
|
+
@scanner.pos = pos
|
|
273
|
+
@line = line
|
|
274
|
+
node
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def else_block(node)
|
|
279
|
+
return unless tok(/@else/)
|
|
280
|
+
ss
|
|
281
|
+
else_node = block(
|
|
282
|
+
Sass::Tree::IfNode.new((sass_script(:parse) if tok(/if/))),
|
|
283
|
+
:directive)
|
|
284
|
+
node.add_else(else_node)
|
|
285
|
+
pos = @scanner.pos
|
|
286
|
+
line = @line
|
|
287
|
+
ss
|
|
288
|
+
|
|
289
|
+
else_block(node) ||
|
|
290
|
+
begin
|
|
291
|
+
# Backtrack in case there are any comments we want to parse
|
|
292
|
+
@scanner.pos = pos
|
|
293
|
+
@line = line
|
|
294
|
+
node
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def else_directive
|
|
299
|
+
err("Invalid CSS: @else must come after @if")
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def extend_directive
|
|
303
|
+
selector = expr!(:selector_sequence)
|
|
304
|
+
optional = tok(OPTIONAL)
|
|
305
|
+
ss
|
|
306
|
+
node(Sass::Tree::ExtendNode.new(selector, !!optional))
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def import_directive
|
|
310
|
+
values = []
|
|
311
|
+
|
|
312
|
+
loop do
|
|
313
|
+
values << expr!(:import_arg)
|
|
314
|
+
break if use_css_import?
|
|
315
|
+
break unless tok(/,/)
|
|
316
|
+
ss
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
return values
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def import_arg
|
|
323
|
+
line = @line
|
|
324
|
+
return unless (str = tok(STRING)) || (uri = tok?(/url\(/i))
|
|
325
|
+
if uri
|
|
326
|
+
str = sass_script(:parse_string)
|
|
327
|
+
ss
|
|
328
|
+
media = media_query_list
|
|
329
|
+
ss
|
|
330
|
+
return node(Tree::CssImportNode.new(str, media.to_a))
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
path = @scanner[1] || @scanner[2]
|
|
334
|
+
ss
|
|
335
|
+
|
|
336
|
+
media = media_query_list
|
|
337
|
+
if path =~ /^(https?:)?\/\// || media || use_css_import?
|
|
338
|
+
node = Sass::Tree::CssImportNode.new(str, media.to_a)
|
|
339
|
+
else
|
|
340
|
+
node = Sass::Tree::ImportNode.new(path.strip)
|
|
341
|
+
end
|
|
342
|
+
node.line = line
|
|
343
|
+
node
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def use_css_import?; false; end
|
|
347
|
+
|
|
348
|
+
def media_directive
|
|
349
|
+
block(node(Sass::Tree::MediaNode.new(expr!(:media_query_list).to_a)), :directive)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# http://www.w3.org/TR/css3-mediaqueries/#syntax
|
|
353
|
+
def media_query_list
|
|
354
|
+
return unless query = media_query
|
|
355
|
+
queries = [query]
|
|
356
|
+
|
|
357
|
+
ss
|
|
358
|
+
while tok(/,/)
|
|
359
|
+
ss; queries << expr!(:media_query)
|
|
360
|
+
end
|
|
361
|
+
ss
|
|
362
|
+
|
|
363
|
+
Sass::Media::QueryList.new(queries)
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def media_query
|
|
367
|
+
if ident1 = interp_ident
|
|
368
|
+
ss
|
|
369
|
+
ident2 = interp_ident
|
|
370
|
+
ss
|
|
371
|
+
if ident2 && ident2.length == 1 && ident2[0].is_a?(String) && ident2[0].downcase == 'and'
|
|
372
|
+
query = Sass::Media::Query.new([], ident1, [])
|
|
373
|
+
else
|
|
374
|
+
if ident2
|
|
375
|
+
query = Sass::Media::Query.new(ident1, ident2, [])
|
|
376
|
+
else
|
|
377
|
+
query = Sass::Media::Query.new([], ident1, [])
|
|
378
|
+
end
|
|
379
|
+
return query unless tok(/and/i)
|
|
380
|
+
ss
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
if query
|
|
385
|
+
expr = expr!(:media_expr)
|
|
386
|
+
else
|
|
387
|
+
return unless expr = media_expr
|
|
388
|
+
end
|
|
389
|
+
query ||= Sass::Media::Query.new([], [], [])
|
|
390
|
+
query.expressions << expr
|
|
391
|
+
|
|
392
|
+
ss
|
|
393
|
+
while tok(/and/i)
|
|
394
|
+
ss; query.expressions << expr!(:media_expr)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
query
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def media_expr
|
|
401
|
+
interp = interpolation and return interp
|
|
402
|
+
return unless tok(/\(/)
|
|
403
|
+
res = ['(']
|
|
404
|
+
ss
|
|
405
|
+
res << sass_script(:parse)
|
|
406
|
+
|
|
407
|
+
if tok(/:/)
|
|
408
|
+
res << ': '
|
|
409
|
+
ss
|
|
410
|
+
res << sass_script(:parse)
|
|
411
|
+
end
|
|
412
|
+
res << tok!(/\)/)
|
|
413
|
+
ss
|
|
414
|
+
res
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def charset_directive
|
|
418
|
+
tok! STRING
|
|
419
|
+
name = @scanner[1] || @scanner[2]
|
|
420
|
+
ss
|
|
421
|
+
node(Sass::Tree::CharsetNode.new(name))
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# The document directive is specified in
|
|
425
|
+
# http://www.w3.org/TR/css3-conditional/, but Gecko allows the
|
|
426
|
+
# `url-prefix` and `domain` functions to omit quotation marks, contrary to
|
|
427
|
+
# the standard.
|
|
428
|
+
#
|
|
429
|
+
# We could parse all document directives according to Mozilla's syntax,
|
|
430
|
+
# but if someone's using e.g. @-webkit-document we don't want them to
|
|
431
|
+
# think WebKit works sans quotes.
|
|
432
|
+
def _moz_document_directive
|
|
433
|
+
res = ["@-moz-document "]
|
|
434
|
+
loop do
|
|
435
|
+
res << str{ss} << expr!(:moz_document_function)
|
|
436
|
+
break unless c = tok(/,/)
|
|
437
|
+
res << c
|
|
438
|
+
end
|
|
439
|
+
directive_body(res.flatten)
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def moz_document_function
|
|
443
|
+
return unless val = interp_uri || _interp_string(:url_prefix) ||
|
|
444
|
+
_interp_string(:domain) || function(!:allow_var) || interpolation
|
|
445
|
+
ss
|
|
446
|
+
val
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
# http://www.w3.org/TR/css3-conditional/
|
|
450
|
+
def supports_directive(name)
|
|
451
|
+
condition = expr!(:supports_condition)
|
|
452
|
+
node = node(Sass::Tree::SupportsNode.new(name, condition))
|
|
453
|
+
|
|
454
|
+
tok!(/\{/)
|
|
455
|
+
node.has_children = true
|
|
456
|
+
block_contents(node, :directive)
|
|
457
|
+
tok!(/\}/)
|
|
458
|
+
|
|
459
|
+
node
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def supports_condition
|
|
463
|
+
supports_negation || supports_operator || supports_interpolation
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
def supports_negation
|
|
467
|
+
return unless tok(/not/i)
|
|
468
|
+
ss
|
|
469
|
+
Sass::Supports::Negation.new(expr!(:supports_condition_in_parens))
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def supports_operator
|
|
473
|
+
return unless cond = supports_condition_in_parens
|
|
474
|
+
return cond unless op = tok(/and|or/i)
|
|
475
|
+
begin
|
|
476
|
+
ss
|
|
477
|
+
cond = Sass::Supports::Operator.new(
|
|
478
|
+
cond, expr!(:supports_condition_in_parens), op)
|
|
479
|
+
end while op = tok(/and|or/i)
|
|
480
|
+
cond
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def supports_condition_in_parens
|
|
484
|
+
interp = supports_interpolation and return interp
|
|
485
|
+
return unless tok(/\(/); ss
|
|
486
|
+
if cond = supports_condition
|
|
487
|
+
tok!(/\)/); ss
|
|
488
|
+
cond
|
|
489
|
+
else
|
|
490
|
+
name = sass_script(:parse)
|
|
491
|
+
tok!(/:/); ss
|
|
492
|
+
value = sass_script(:parse)
|
|
493
|
+
tok!(/\)/); ss
|
|
494
|
+
Sass::Supports::Declaration.new(name, value)
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def supports_declaration_condition
|
|
499
|
+
return unless tok(/\(/); ss
|
|
500
|
+
supports_declaration_body
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
def supports_interpolation
|
|
504
|
+
return unless interp = interpolation
|
|
505
|
+
ss
|
|
506
|
+
Sass::Supports::Interpolation.new(interp)
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def variable
|
|
510
|
+
return unless tok(/\$/)
|
|
511
|
+
name = tok!(IDENT)
|
|
512
|
+
ss; tok!(/:/); ss
|
|
513
|
+
|
|
514
|
+
expr = sass_script(:parse)
|
|
515
|
+
guarded = tok(DEFAULT)
|
|
516
|
+
node(Sass::Tree::VariableNode.new(name, expr, guarded))
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def operator
|
|
520
|
+
# Many of these operators (all except / and ,)
|
|
521
|
+
# are disallowed by the CSS spec,
|
|
522
|
+
# but they're included here for compatibility
|
|
523
|
+
# with some proprietary MS properties
|
|
524
|
+
str {ss if tok(/[\/,:.=]/)}
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def ruleset
|
|
528
|
+
return unless rules = selector_sequence
|
|
529
|
+
block(node(Sass::Tree::RuleNode.new(rules.flatten.compact)), :ruleset)
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def block(node, context)
|
|
533
|
+
node.has_children = true
|
|
534
|
+
tok!(/\{/)
|
|
535
|
+
block_contents(node, context)
|
|
536
|
+
tok!(/\}/)
|
|
537
|
+
node
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
# A block may contain declarations and/or rulesets
|
|
541
|
+
def block_contents(node, context)
|
|
542
|
+
block_given? ? yield : ss_comments(node)
|
|
543
|
+
node << (child = block_child(context))
|
|
544
|
+
while tok(/;/) || has_children?(child)
|
|
545
|
+
block_given? ? yield : ss_comments(node)
|
|
546
|
+
node << (child = block_child(context))
|
|
547
|
+
end
|
|
548
|
+
node
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def block_child(context)
|
|
552
|
+
return variable || directive if context == :function
|
|
553
|
+
return variable || directive || ruleset if context == :stylesheet
|
|
554
|
+
variable || directive || declaration_or_ruleset
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def has_children?(child_or_array)
|
|
558
|
+
return false unless child_or_array
|
|
559
|
+
return child_or_array.last.has_children if child_or_array.is_a?(Array)
|
|
560
|
+
return child_or_array.has_children
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
# This is a nasty hack, and the only place in the parser
|
|
564
|
+
# that requires a large amount of backtracking.
|
|
565
|
+
# The reason is that we can't figure out if certain strings
|
|
566
|
+
# are declarations or rulesets with fixed finite lookahead.
|
|
567
|
+
# For example, "foo:bar baz baz baz..." could be either a property
|
|
568
|
+
# or a selector.
|
|
569
|
+
#
|
|
570
|
+
# To handle this, we simply check if it works as a property
|
|
571
|
+
# (which is the most common case)
|
|
572
|
+
# and, if it doesn't, try it as a ruleset.
|
|
573
|
+
#
|
|
574
|
+
# We could eke some more efficiency out of this
|
|
575
|
+
# by handling some easy cases (first token isn't an identifier,
|
|
576
|
+
# no colon after the identifier, whitespace after the colon),
|
|
577
|
+
# but I'm not sure the gains would be worth the added complexity.
|
|
578
|
+
def declaration_or_ruleset
|
|
579
|
+
old_use_property_exception, @use_property_exception =
|
|
580
|
+
@use_property_exception, false
|
|
581
|
+
decl_err = catch_error do
|
|
582
|
+
decl = declaration
|
|
583
|
+
unless decl && decl.has_children
|
|
584
|
+
# We want an exception if it's not there,
|
|
585
|
+
# but we don't want to consume if it is
|
|
586
|
+
tok!(/[;}]/) unless tok?(/[;}]/)
|
|
587
|
+
end
|
|
588
|
+
return decl
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
ruleset_err = catch_error {return ruleset}
|
|
592
|
+
rethrow(@use_property_exception ? decl_err : ruleset_err)
|
|
593
|
+
ensure
|
|
594
|
+
@use_property_exception = old_use_property_exception
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
def selector_sequence
|
|
598
|
+
if sel = tok(STATIC_SELECTOR, true)
|
|
599
|
+
return [sel]
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
rules = []
|
|
603
|
+
return unless v = selector
|
|
604
|
+
rules.concat v
|
|
605
|
+
|
|
606
|
+
ws = ''
|
|
607
|
+
while tok(/,/)
|
|
608
|
+
ws << str {ss}
|
|
609
|
+
if v = selector
|
|
610
|
+
rules << ',' << ws
|
|
611
|
+
rules.concat v
|
|
612
|
+
ws = ''
|
|
613
|
+
end
|
|
614
|
+
end
|
|
615
|
+
rules
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def selector
|
|
619
|
+
return unless sel = _selector
|
|
620
|
+
sel.to_a
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
def selector_comma_sequence
|
|
624
|
+
return unless sel = _selector
|
|
625
|
+
selectors = [sel]
|
|
626
|
+
ws = ''
|
|
627
|
+
while tok(/,/)
|
|
628
|
+
ws << str{ss}
|
|
629
|
+
if sel = _selector
|
|
630
|
+
selectors << sel
|
|
631
|
+
selectors[-1] = Selector::Sequence.new(["\n"] + selectors.last.members) if ws.include?("\n")
|
|
632
|
+
ws = ''
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
Selector::CommaSequence.new(selectors)
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def _selector
|
|
639
|
+
# The combinator here allows the "> E" hack
|
|
640
|
+
return unless val = combinator || simple_selector_sequence
|
|
641
|
+
nl = str{ss}.include?("\n")
|
|
642
|
+
res = []
|
|
643
|
+
res << val
|
|
644
|
+
res << "\n" if nl
|
|
645
|
+
|
|
646
|
+
while val = combinator || simple_selector_sequence
|
|
647
|
+
res << val
|
|
648
|
+
res << "\n" if str{ss}.include?("\n")
|
|
649
|
+
end
|
|
650
|
+
Selector::Sequence.new(res.compact)
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
def combinator
|
|
654
|
+
tok(PLUS) || tok(GREATER) || tok(TILDE) || reference_combinator
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
def reference_combinator
|
|
658
|
+
return unless tok(/\//)
|
|
659
|
+
res = ['/']
|
|
660
|
+
ns, name = expr!(:qualified_name)
|
|
661
|
+
res << ns << '|' if ns
|
|
662
|
+
res << name << tok!(/\//)
|
|
663
|
+
res = res.flatten
|
|
664
|
+
res = res.join '' if res.all? {|e| e.is_a?(String)}
|
|
665
|
+
res
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
def simple_selector_sequence
|
|
669
|
+
# Returning expr by default allows for stuff like
|
|
670
|
+
# http://www.w3.org/TR/css3-animations/#keyframes-
|
|
671
|
+
return expr(!:allow_var) unless e = element_name || id_selector ||
|
|
672
|
+
class_selector || placeholder_selector || attrib || pseudo ||
|
|
673
|
+
parent_selector || interpolation_selector
|
|
674
|
+
res = [e]
|
|
675
|
+
|
|
676
|
+
# The tok(/\*/) allows the "E*" hack
|
|
677
|
+
while v = id_selector || class_selector || placeholder_selector || attrib ||
|
|
678
|
+
pseudo || interpolation_selector ||
|
|
679
|
+
(tok(/\*/) && Selector::Universal.new(nil))
|
|
680
|
+
res << v
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
pos = @scanner.pos
|
|
684
|
+
line = @line
|
|
685
|
+
if sel = str? {simple_selector_sequence}
|
|
686
|
+
@scanner.pos = pos
|
|
687
|
+
@line = line
|
|
688
|
+
begin
|
|
689
|
+
# If we see "*E", don't force a throw because this could be the
|
|
690
|
+
# "*prop: val" hack.
|
|
691
|
+
expected('"{"') if res.length == 1 && res[0].is_a?(Selector::Universal)
|
|
692
|
+
throw_error {expected('"{"')}
|
|
693
|
+
rescue Sass::SyntaxError => e
|
|
694
|
+
e.message << "\n\n\"#{sel}\" may only be used at the beginning of a compound selector."
|
|
695
|
+
raise e
|
|
696
|
+
end
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
Selector::SimpleSequence.new(res, tok(/!/))
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
def parent_selector
|
|
703
|
+
return unless tok(/&/)
|
|
704
|
+
Selector::Parent.new
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
def class_selector
|
|
708
|
+
return unless tok(/\./)
|
|
709
|
+
@expected = "class name"
|
|
710
|
+
Selector::Class.new(merge(expr!(:interp_ident)))
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
def id_selector
|
|
714
|
+
return unless tok(/#(?!\{)/)
|
|
715
|
+
@expected = "id name"
|
|
716
|
+
Selector::Id.new(merge(expr!(:interp_name)))
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
def placeholder_selector
|
|
720
|
+
return unless tok(/%/)
|
|
721
|
+
@expected = "placeholder name"
|
|
722
|
+
Selector::Placeholder.new(merge(expr!(:interp_ident)))
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
def element_name
|
|
726
|
+
ns, name = Sass::Util.destructure(qualified_name(:allow_star_name))
|
|
727
|
+
return unless ns || name
|
|
728
|
+
|
|
729
|
+
if name == '*'
|
|
730
|
+
Selector::Universal.new(merge(ns))
|
|
731
|
+
else
|
|
732
|
+
Selector::Element.new(merge(name), merge(ns))
|
|
733
|
+
end
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
def qualified_name(allow_star_name=false)
|
|
737
|
+
return unless name = interp_ident || tok(/\*/) || (tok?(/\|/) && "")
|
|
738
|
+
return nil, name unless tok(/\|/)
|
|
739
|
+
|
|
740
|
+
return name, expr!(:interp_ident) unless allow_star_name
|
|
741
|
+
@expected = "identifier or *"
|
|
742
|
+
return name, interp_ident || tok!(/\*/)
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
def interpolation_selector
|
|
746
|
+
return unless script = interpolation
|
|
747
|
+
Selector::Interpolation.new(script)
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
def attrib
|
|
751
|
+
return unless tok(/\[/)
|
|
752
|
+
ss
|
|
753
|
+
ns, name = attrib_name!
|
|
754
|
+
ss
|
|
755
|
+
|
|
756
|
+
if op = tok(/=/) ||
|
|
757
|
+
tok(INCLUDES) ||
|
|
758
|
+
tok(DASHMATCH) ||
|
|
759
|
+
tok(PREFIXMATCH) ||
|
|
760
|
+
tok(SUFFIXMATCH) ||
|
|
761
|
+
tok(SUBSTRINGMATCH)
|
|
762
|
+
@expected = "identifier or string"
|
|
763
|
+
ss
|
|
764
|
+
val = interp_ident || expr!(:interp_string)
|
|
765
|
+
ss
|
|
766
|
+
end
|
|
767
|
+
flags = interp_ident || interp_string
|
|
768
|
+
tok!(/\]/)
|
|
769
|
+
|
|
770
|
+
Selector::Attribute.new(merge(name), merge(ns), op, merge(val), merge(flags))
|
|
771
|
+
end
|
|
772
|
+
|
|
773
|
+
def attrib_name!
|
|
774
|
+
if name_or_ns = interp_ident
|
|
775
|
+
# E, E|E
|
|
776
|
+
if tok(/\|(?!=)/)
|
|
777
|
+
ns = name_or_ns
|
|
778
|
+
name = interp_ident
|
|
779
|
+
else
|
|
780
|
+
name = name_or_ns
|
|
781
|
+
end
|
|
782
|
+
else
|
|
783
|
+
# *|E or |E
|
|
784
|
+
ns = [tok(/\*/) || ""]
|
|
785
|
+
tok!(/\|/)
|
|
786
|
+
name = expr!(:interp_ident)
|
|
787
|
+
end
|
|
788
|
+
return ns, name
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
def pseudo
|
|
792
|
+
return unless s = tok(/::?/)
|
|
793
|
+
@expected = "pseudoclass or pseudoelement"
|
|
794
|
+
name = expr!(:interp_ident)
|
|
795
|
+
if tok(/\(/)
|
|
796
|
+
ss
|
|
797
|
+
arg = expr!(:pseudo_arg)
|
|
798
|
+
while tok(/,/)
|
|
799
|
+
arg << ',' << str{ss}
|
|
800
|
+
arg.concat expr!(:pseudo_arg)
|
|
801
|
+
end
|
|
802
|
+
tok!(/\)/)
|
|
803
|
+
end
|
|
804
|
+
Selector::Pseudo.new(s == ':' ? :class : :element, merge(name), merge(arg))
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
def pseudo_arg
|
|
808
|
+
# In the CSS spec, every pseudo-class/element either takes a pseudo
|
|
809
|
+
# expression or a selector comma sequence as an argument. However, we
|
|
810
|
+
# don't want to have to know which takes which, so we handle both at
|
|
811
|
+
# once.
|
|
812
|
+
#
|
|
813
|
+
# However, there are some ambiguities between the two. For instance, "n"
|
|
814
|
+
# could start a pseudo expression like "n+1", or it could start a
|
|
815
|
+
# selector like "n|m". In order to handle this, we must regrettably
|
|
816
|
+
# backtrack.
|
|
817
|
+
expr, sel = nil, nil
|
|
818
|
+
pseudo_err = catch_error do
|
|
819
|
+
expr = pseudo_expr
|
|
820
|
+
next if tok?(/[,)]/)
|
|
821
|
+
expr = nil
|
|
822
|
+
expected '")"'
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
return expr if expr
|
|
826
|
+
sel_err = catch_error {sel = selector}
|
|
827
|
+
return sel if sel
|
|
828
|
+
rethrow pseudo_err if pseudo_err
|
|
829
|
+
rethrow sel_err if sel_err
|
|
830
|
+
return
|
|
831
|
+
end
|
|
832
|
+
|
|
833
|
+
def pseudo_expr
|
|
834
|
+
return unless e = tok(PLUS) || tok(/[-*]/) || tok(NUMBER) ||
|
|
835
|
+
interp_string || tok(IDENT) || interpolation
|
|
836
|
+
res = [e, str{ss}]
|
|
837
|
+
while e = tok(PLUS) || tok(/[-*]/) || tok(NUMBER) ||
|
|
838
|
+
interp_string || tok(IDENT) || interpolation
|
|
839
|
+
res << e << str{ss}
|
|
840
|
+
end
|
|
841
|
+
res
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
def declaration
|
|
845
|
+
# This allows the "*prop: val", ":prop: val", and ".prop: val" hacks
|
|
846
|
+
if s = tok(/[:\*\.]|\#(?!\{)/)
|
|
847
|
+
@use_property_exception = s !~ /[\.\#]/
|
|
848
|
+
name = [s, str{ss}, *expr!(:interp_ident)]
|
|
849
|
+
else
|
|
850
|
+
return unless name = interp_ident
|
|
851
|
+
name = [name] if name.is_a?(String)
|
|
852
|
+
end
|
|
853
|
+
if comment = tok(COMMENT)
|
|
854
|
+
name << comment
|
|
855
|
+
end
|
|
856
|
+
ss
|
|
857
|
+
|
|
858
|
+
tok!(/:/)
|
|
859
|
+
space, value = value!
|
|
860
|
+
ss
|
|
861
|
+
require_block = tok?(/\{/)
|
|
862
|
+
|
|
863
|
+
node = node(Sass::Tree::PropNode.new(name.flatten.compact, value, :new))
|
|
864
|
+
|
|
865
|
+
return node unless require_block
|
|
866
|
+
nested_properties! node, space
|
|
867
|
+
end
|
|
868
|
+
|
|
869
|
+
def value!
|
|
870
|
+
space = !str {ss}.empty?
|
|
871
|
+
@use_property_exception ||= space || !tok?(IDENT)
|
|
872
|
+
|
|
873
|
+
return true, Sass::Script::String.new("") if tok?(/\{/)
|
|
874
|
+
# This is a bit of a dirty trick:
|
|
875
|
+
# if the value is completely static,
|
|
876
|
+
# we don't parse it at all, and instead return a plain old string
|
|
877
|
+
# containing the value.
|
|
878
|
+
# This results in a dramatic speed increase.
|
|
879
|
+
if val = tok(STATIC_VALUE, true)
|
|
880
|
+
return space, Sass::Script::String.new(val.strip)
|
|
881
|
+
end
|
|
882
|
+
return space, sass_script(:parse)
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
def nested_properties!(node, space)
|
|
886
|
+
err(<<MESSAGE) unless space
|
|
887
|
+
Invalid CSS: a space is required between a property and its definition
|
|
888
|
+
when it has other properties nested beneath it.
|
|
889
|
+
MESSAGE
|
|
890
|
+
|
|
891
|
+
@use_property_exception = true
|
|
892
|
+
@expected = 'expression (e.g. 1px, bold) or "{"'
|
|
893
|
+
block(node, :property)
|
|
894
|
+
end
|
|
895
|
+
|
|
896
|
+
def expr(allow_var = true)
|
|
897
|
+
return unless t = term(allow_var)
|
|
898
|
+
res = [t, str{ss}]
|
|
899
|
+
|
|
900
|
+
while (o = operator) && (t = term(allow_var))
|
|
901
|
+
res << o << t << str{ss}
|
|
902
|
+
end
|
|
903
|
+
|
|
904
|
+
res.flatten
|
|
905
|
+
end
|
|
906
|
+
|
|
907
|
+
def term(allow_var)
|
|
908
|
+
if e = tok(NUMBER) ||
|
|
909
|
+
interp_uri ||
|
|
910
|
+
function(allow_var) ||
|
|
911
|
+
interp_string ||
|
|
912
|
+
tok(UNICODERANGE) ||
|
|
913
|
+
interp_ident ||
|
|
914
|
+
tok(HEXCOLOR) ||
|
|
915
|
+
(allow_var && var_expr)
|
|
916
|
+
return e
|
|
917
|
+
end
|
|
918
|
+
|
|
919
|
+
return unless op = tok(/[+-]/)
|
|
920
|
+
@expected = "number or function"
|
|
921
|
+
return [op, tok(NUMBER) || function(allow_var) ||
|
|
922
|
+
(allow_var && var_expr) || expr!(:interpolation)]
|
|
923
|
+
end
|
|
924
|
+
|
|
925
|
+
def function(allow_var)
|
|
926
|
+
return unless name = tok(FUNCTION)
|
|
927
|
+
if name == "expression(" || name == "calc("
|
|
928
|
+
str, _ = Sass::Shared.balance(@scanner, ?(, ?), 1)
|
|
929
|
+
[name, str]
|
|
930
|
+
else
|
|
931
|
+
[name, str{ss}, expr(allow_var), tok!(/\)/)]
|
|
932
|
+
end
|
|
933
|
+
end
|
|
934
|
+
|
|
935
|
+
def var_expr
|
|
936
|
+
return unless tok(/\$/)
|
|
937
|
+
line = @line
|
|
938
|
+
var = Sass::Script::Variable.new(tok!(IDENT))
|
|
939
|
+
var.line = line
|
|
940
|
+
var
|
|
941
|
+
end
|
|
942
|
+
|
|
943
|
+
def interpolation
|
|
944
|
+
return unless tok(INTERP_START)
|
|
945
|
+
sass_script(:parse_interpolated)
|
|
946
|
+
end
|
|
947
|
+
|
|
948
|
+
def interp_string
|
|
949
|
+
_interp_string(:double) || _interp_string(:single)
|
|
950
|
+
end
|
|
951
|
+
|
|
952
|
+
def interp_uri
|
|
953
|
+
_interp_string(:uri)
|
|
954
|
+
end
|
|
955
|
+
|
|
956
|
+
def _interp_string(type)
|
|
957
|
+
return unless start = tok(Sass::Script::Lexer::STRING_REGULAR_EXPRESSIONS[type][false])
|
|
958
|
+
res = [start]
|
|
959
|
+
|
|
960
|
+
mid_re = Sass::Script::Lexer::STRING_REGULAR_EXPRESSIONS[type][true]
|
|
961
|
+
# @scanner[2].empty? means we've started an interpolated section
|
|
962
|
+
while @scanner[2] == '#{'
|
|
963
|
+
@scanner.pos -= 2 # Don't consume the #{
|
|
964
|
+
res.last.slice!(-2..-1)
|
|
965
|
+
res << expr!(:interpolation) << tok(mid_re)
|
|
966
|
+
end
|
|
967
|
+
res
|
|
968
|
+
end
|
|
969
|
+
|
|
970
|
+
def interp_ident(start = IDENT)
|
|
971
|
+
return unless val = tok(start) || interpolation || tok(IDENT_HYPHEN_INTERP, true)
|
|
972
|
+
res = [val]
|
|
973
|
+
while val = tok(NAME) || interpolation
|
|
974
|
+
res << val
|
|
975
|
+
end
|
|
976
|
+
res
|
|
977
|
+
end
|
|
978
|
+
|
|
979
|
+
def interp_ident_or_var
|
|
980
|
+
(id = interp_ident) and return id
|
|
981
|
+
(var = var_expr) and return [var]
|
|
982
|
+
end
|
|
983
|
+
|
|
984
|
+
def interp_name
|
|
985
|
+
interp_ident NAME
|
|
986
|
+
end
|
|
987
|
+
|
|
988
|
+
def str
|
|
989
|
+
@strs.push ""
|
|
990
|
+
yield
|
|
991
|
+
@strs.last
|
|
992
|
+
ensure
|
|
993
|
+
@strs.pop
|
|
994
|
+
end
|
|
995
|
+
|
|
996
|
+
def str?
|
|
997
|
+
pos = @scanner.pos
|
|
998
|
+
line = @line
|
|
999
|
+
@strs.push ""
|
|
1000
|
+
throw_error {yield} && @strs.last
|
|
1001
|
+
rescue Sass::SyntaxError
|
|
1002
|
+
@scanner.pos = pos
|
|
1003
|
+
@line = line
|
|
1004
|
+
nil
|
|
1005
|
+
ensure
|
|
1006
|
+
@strs.pop
|
|
1007
|
+
end
|
|
1008
|
+
|
|
1009
|
+
def node(node)
|
|
1010
|
+
node.line = @line
|
|
1011
|
+
node
|
|
1012
|
+
end
|
|
1013
|
+
|
|
1014
|
+
@sass_script_parser = Class.new(Sass::Script::Parser)
|
|
1015
|
+
@sass_script_parser.send(:include, ScriptParser)
|
|
1016
|
+
# @private
|
|
1017
|
+
def self.sass_script_parser; @sass_script_parser; end
|
|
1018
|
+
|
|
1019
|
+
def sass_script(*args)
|
|
1020
|
+
parser = self.class.sass_script_parser.new(@scanner, @line,
|
|
1021
|
+
@scanner.pos - (@scanner.string[0...@scanner.pos].rindex("\n") || 0))
|
|
1022
|
+
result = parser.send(*args)
|
|
1023
|
+
unless @strs.empty?
|
|
1024
|
+
# Convert to CSS manually so that comments are ignored.
|
|
1025
|
+
src = result.to_sass
|
|
1026
|
+
@strs.each {|s| s << src}
|
|
1027
|
+
end
|
|
1028
|
+
@line = parser.line
|
|
1029
|
+
result
|
|
1030
|
+
rescue Sass::SyntaxError => e
|
|
1031
|
+
throw(:_sass_parser_error, true) if @throw_error
|
|
1032
|
+
raise e
|
|
1033
|
+
end
|
|
1034
|
+
|
|
1035
|
+
def merge(arr)
|
|
1036
|
+
arr && Sass::Util.merge_adjacent_strings([arr].flatten)
|
|
1037
|
+
end
|
|
1038
|
+
|
|
1039
|
+
EXPR_NAMES = {
|
|
1040
|
+
:media_query => "media query (e.g. print, screen, print and screen)",
|
|
1041
|
+
:media_query_list => "media query (e.g. print, screen, print and screen)",
|
|
1042
|
+
:media_expr => "media expression (e.g. (min-device-width: 800px))",
|
|
1043
|
+
:pseudo_arg => "expression (e.g. fr, 2n+1)",
|
|
1044
|
+
:interp_ident => "identifier",
|
|
1045
|
+
:interp_name => "identifier",
|
|
1046
|
+
:qualified_name => "identifier",
|
|
1047
|
+
:expr => "expression (e.g. 1px, bold)",
|
|
1048
|
+
:_selector => "selector",
|
|
1049
|
+
:selector_comma_sequence => "selector",
|
|
1050
|
+
:simple_selector_sequence => "selector",
|
|
1051
|
+
:import_arg => "file to import (string or url())",
|
|
1052
|
+
:moz_document_function => "matching function (e.g. url-prefix(), domain())",
|
|
1053
|
+
:supports_condition => "@supports condition (e.g. (display: flexbox))",
|
|
1054
|
+
:supports_condition_in_parens => "@supports condition (e.g. (display: flexbox))",
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
TOK_NAMES = Sass::Util.to_hash(
|
|
1058
|
+
Sass::SCSS::RX.constants.map {|c| [Sass::SCSS::RX.const_get(c), c.downcase]}).
|
|
1059
|
+
merge(IDENT => "identifier", /[;}]/ => '";"')
|
|
1060
|
+
|
|
1061
|
+
def tok?(rx)
|
|
1062
|
+
@scanner.match?(rx)
|
|
1063
|
+
end
|
|
1064
|
+
|
|
1065
|
+
def expr!(name)
|
|
1066
|
+
(e = send(name)) && (return e)
|
|
1067
|
+
expected(EXPR_NAMES[name] || name.to_s)
|
|
1068
|
+
end
|
|
1069
|
+
|
|
1070
|
+
def tok!(rx)
|
|
1071
|
+
(t = tok(rx)) && (return t)
|
|
1072
|
+
name = TOK_NAMES[rx]
|
|
1073
|
+
|
|
1074
|
+
unless name
|
|
1075
|
+
# Display basic regexps as plain old strings
|
|
1076
|
+
string = rx.source.gsub(/\\(.)/, '\1')
|
|
1077
|
+
name = rx.source == Regexp.escape(string) ? string.inspect : rx.inspect
|
|
1078
|
+
end
|
|
1079
|
+
|
|
1080
|
+
expected(name)
|
|
1081
|
+
end
|
|
1082
|
+
|
|
1083
|
+
def expected(name)
|
|
1084
|
+
throw(:_sass_parser_error, true) if @throw_error
|
|
1085
|
+
self.class.expected(@scanner, @expected || name, @line)
|
|
1086
|
+
end
|
|
1087
|
+
|
|
1088
|
+
def err(msg)
|
|
1089
|
+
throw(:_sass_parser_error, true) if @throw_error
|
|
1090
|
+
raise Sass::SyntaxError.new(msg, :line => @line)
|
|
1091
|
+
end
|
|
1092
|
+
|
|
1093
|
+
def throw_error
|
|
1094
|
+
old_throw_error, @throw_error = @throw_error, false
|
|
1095
|
+
yield
|
|
1096
|
+
ensure
|
|
1097
|
+
@throw_error = old_throw_error
|
|
1098
|
+
end
|
|
1099
|
+
|
|
1100
|
+
def catch_error(&block)
|
|
1101
|
+
old_throw_error, @throw_error = @throw_error, true
|
|
1102
|
+
pos = @scanner.pos
|
|
1103
|
+
line = @line
|
|
1104
|
+
expected = @expected
|
|
1105
|
+
if catch(:_sass_parser_error) {yield; false}
|
|
1106
|
+
@scanner.pos = pos
|
|
1107
|
+
@line = line
|
|
1108
|
+
@expected = expected
|
|
1109
|
+
{:pos => pos, :line => line, :expected => @expected, :block => block}
|
|
1110
|
+
end
|
|
1111
|
+
ensure
|
|
1112
|
+
@throw_error = old_throw_error
|
|
1113
|
+
end
|
|
1114
|
+
|
|
1115
|
+
def rethrow(err)
|
|
1116
|
+
if @throw_error
|
|
1117
|
+
throw :_sass_parser_error, err
|
|
1118
|
+
else
|
|
1119
|
+
@scanner = Sass::Util::MultibyteStringScanner.new(@scanner.string)
|
|
1120
|
+
@scanner.pos = err[:pos]
|
|
1121
|
+
@line = err[:line]
|
|
1122
|
+
@expected = err[:expected]
|
|
1123
|
+
err[:block].call
|
|
1124
|
+
end
|
|
1125
|
+
end
|
|
1126
|
+
|
|
1127
|
+
# @private
|
|
1128
|
+
def self.expected(scanner, expected, line)
|
|
1129
|
+
pos = scanner.pos
|
|
1130
|
+
|
|
1131
|
+
after = scanner.string[0...pos]
|
|
1132
|
+
# Get rid of whitespace between pos and the last token,
|
|
1133
|
+
# but only if there's a newline in there
|
|
1134
|
+
after.gsub!(/\s*\n\s*$/, '')
|
|
1135
|
+
# Also get rid of stuff before the last newline
|
|
1136
|
+
after.gsub!(/.*\n/, '')
|
|
1137
|
+
after = "..." + after[-15..-1] if after.size > 18
|
|
1138
|
+
|
|
1139
|
+
was = scanner.rest.dup
|
|
1140
|
+
# Get rid of whitespace between pos and the next token,
|
|
1141
|
+
# but only if there's a newline in there
|
|
1142
|
+
was.gsub!(/^\s*\n\s*/, '')
|
|
1143
|
+
# Also get rid of stuff after the next newline
|
|
1144
|
+
was.gsub!(/\n.*/, '')
|
|
1145
|
+
was = was[0...15] + "..." if was.size > 18
|
|
1146
|
+
|
|
1147
|
+
raise Sass::SyntaxError.new(
|
|
1148
|
+
"Invalid CSS after \"#{after}\": expected #{expected}, was \"#{was}\"",
|
|
1149
|
+
:line => line)
|
|
1150
|
+
end
|
|
1151
|
+
|
|
1152
|
+
# Avoid allocating lots of new strings for `#tok`.
|
|
1153
|
+
# This is important because `#tok` is called all the time.
|
|
1154
|
+
NEWLINE = "\n"
|
|
1155
|
+
|
|
1156
|
+
def tok(rx, last_group_lookahead = false)
|
|
1157
|
+
res = @scanner.scan(rx)
|
|
1158
|
+
if res
|
|
1159
|
+
# This fixes https://github.com/nex3/sass/issues/104, which affects
|
|
1160
|
+
# Ruby 1.8.7 and REE. This fix is to replace the ?= zero-width
|
|
1161
|
+
# positive lookahead operator in the Regexp (which matches without
|
|
1162
|
+
# consuming the matched group), with a match that does consume the
|
|
1163
|
+
# group, but then rewinds the scanner and removes the group from the
|
|
1164
|
+
# end of the matched string. This fix makes the assumption that the
|
|
1165
|
+
# matched group will always occur at the end of the match.
|
|
1166
|
+
if last_group_lookahead && @scanner[-1]
|
|
1167
|
+
@scanner.pos -= @scanner[-1].length
|
|
1168
|
+
res.slice!(-@scanner[-1].length..-1)
|
|
1169
|
+
end
|
|
1170
|
+
@line += res.count(NEWLINE)
|
|
1171
|
+
@expected = nil
|
|
1172
|
+
if !@strs.empty? && rx != COMMENT && rx != SINGLE_LINE_COMMENT
|
|
1173
|
+
@strs.each {|s| s << res}
|
|
1174
|
+
end
|
|
1175
|
+
res
|
|
1176
|
+
end
|
|
1177
|
+
end
|
|
1178
|
+
end
|
|
1179
|
+
end
|
|
1180
|
+
end
|