oreorenasass 3.4.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 +221 -0
- data/Rakefile +370 -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/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 +47 -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/engine.rb +1181 -0
- data/lib/sass/environment.rb +191 -0
- data/lib/sass/error.rb +198 -0
- data/lib/sass/exec/base.rb +187 -0
- data/lib/sass/exec/sass_convert.rb +264 -0
- data/lib/sass/exec/sass_scss.rb +424 -0
- data/lib/sass/exec.rb +9 -0
- data/lib/sass/features.rb +47 -0
- data/lib/sass/importers/base.rb +182 -0
- data/lib/sass/importers/filesystem.rb +211 -0
- data/lib/sass/importers.rb +22 -0
- data/lib/sass/logger/base.rb +30 -0
- data/lib/sass/logger/log_level.rb +45 -0
- data/lib/sass/logger.rb +12 -0
- data/lib/sass/media.rb +210 -0
- data/lib/sass/plugin/compiler.rb +565 -0
- data/lib/sass/plugin/configuration.rb +118 -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/css_lexer.rb +33 -0
- data/lib/sass/script/css_parser.rb +34 -0
- data/lib/sass/script/functions.rb +2626 -0
- data/lib/sass/script/lexer.rb +449 -0
- data/lib/sass/script/parser.rb +637 -0
- data/lib/sass/script/tree/funcall.rb +306 -0
- data/lib/sass/script/tree/interpolation.rb +118 -0
- data/lib/sass/script/tree/list_literal.rb +77 -0
- data/lib/sass/script/tree/literal.rb +45 -0
- data/lib/sass/script/tree/map_literal.rb +64 -0
- data/lib/sass/script/tree/node.rb +109 -0
- data/lib/sass/script/tree/operation.rb +103 -0
- data/lib/sass/script/tree/selector.rb +26 -0
- data/lib/sass/script/tree/string_interpolation.rb +104 -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 +240 -0
- data/lib/sass/script/value/bool.rb +35 -0
- data/lib/sass/script/value/color.rb +680 -0
- data/lib/sass/script/value/helpers.rb +262 -0
- data/lib/sass/script/value/list.rb +113 -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 +530 -0
- data/lib/sass/script/value/string.rb +97 -0
- data/lib/sass/script/value.rb +11 -0
- data/lib/sass/script.rb +66 -0
- data/lib/sass/scss/css_parser.rb +42 -0
- data/lib/sass/scss/parser.rb +1209 -0
- data/lib/sass/scss/rx.rb +141 -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 +368 -0
- data/lib/sass/scss.rb +16 -0
- data/lib/sass/selector/abstract_sequence.rb +109 -0
- data/lib/sass/selector/comma_sequence.rb +175 -0
- data/lib/sass/selector/pseudo.rb +256 -0
- data/lib/sass/selector/sequence.rb +600 -0
- data/lib/sass/selector/simple.rb +117 -0
- data/lib/sass/selector/simple_sequence.rb +325 -0
- data/lib/sass/selector.rb +326 -0
- data/lib/sass/shared.rb +76 -0
- data/lib/sass/source/map.rb +210 -0
- data/lib/sass/source/position.rb +39 -0
- data/lib/sass/source/range.rb +41 -0
- data/lib/sass/stack.rb +120 -0
- data/lib/sass/supports.rb +227 -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 +60 -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 +39 -0
- data/lib/sass/tree/if_node.rb +52 -0
- data/lib/sass/tree/import_node.rb +74 -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 +238 -0
- data/lib/sass/tree/prop_node.rb +171 -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 +145 -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 +177 -0
- data/lib/sass/tree/visitors/convert.rb +334 -0
- data/lib/sass/tree/visitors/cssize.rb +369 -0
- data/lib/sass/tree/visitors/deep_copy.rb +107 -0
- data/lib/sass/tree/visitors/extend.rb +68 -0
- data/lib/sass/tree/visitors/perform.rb +539 -0
- data/lib/sass/tree/visitors/set_options.rb +139 -0
- data/lib/sass/tree/visitors/to_css.rb +381 -0
- data/lib/sass/tree/warn_node.rb +18 -0
- data/lib/sass/tree/while_node.rb +18 -0
- data/lib/sass/util/cross_platform_random.rb +19 -0
- data/lib/sass/util/multibyte_string_scanner.rb +157 -0
- data/lib/sass/util/normalized_map.rb +130 -0
- data/lib/sass/util/ordered_hash.rb +192 -0
- data/lib/sass/util/subset_map.rb +110 -0
- data/lib/sass/util/test.rb +9 -0
- data/lib/sass/util.rb +1318 -0
- data/lib/sass/version.rb +124 -0
- data/lib/sass.rb +102 -0
- data/rails/init.rb +1 -0
- data/test/sass/cache_test.rb +131 -0
- data/test/sass/callbacks_test.rb +61 -0
- data/test/sass/compiler_test.rb +232 -0
- data/test/sass/conversion_test.rb +2054 -0
- data/test/sass/css2sass_test.rb +477 -0
- data/test/sass/data/hsl-rgb.txt +319 -0
- data/test/sass/encoding_test.rb +219 -0
- data/test/sass/engine_test.rb +3301 -0
- data/test/sass/exec_test.rb +86 -0
- data/test/sass/extend_test.rb +1661 -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 +1926 -0
- data/test/sass/importer_test.rb +412 -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 +554 -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 +328 -0
- data/test/sass/script_test.rb +1054 -0
- data/test/sass/scss/css_test.rb +1215 -0
- data/test/sass/scss/rx_test.rb +156 -0
- data/test/sass/scss/scss_test.rb +3900 -0
- data/test/sass/scss/test_helper.rb +37 -0
- data/test/sass/source_map_test.rb +977 -0
- data/test/sass/superselector_test.rb +191 -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 +12 -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/normalized_map_test.rb +51 -0
- data/test/sass/util/subset_map_test.rb +91 -0
- data/test/sass/util_test.rb +467 -0
- data/test/sass/value_helpers_test.rb +179 -0
- data/test/test_helper.rb +109 -0
- metadata +386 -0
@@ -0,0 +1,449 @@
|
|
1
|
+
require 'sass/scss/rx'
|
2
|
+
|
3
|
+
module Sass
|
4
|
+
module Script
|
5
|
+
# The lexical analyzer for SassScript.
|
6
|
+
# It takes a raw string and converts it to individual tokens
|
7
|
+
# that are easier to parse.
|
8
|
+
class Lexer
|
9
|
+
include Sass::SCSS::RX
|
10
|
+
|
11
|
+
# A struct containing information about an individual token.
|
12
|
+
#
|
13
|
+
# `type`: \[`Symbol`\]
|
14
|
+
# : The type of token.
|
15
|
+
#
|
16
|
+
# `value`: \[`Object`\]
|
17
|
+
# : The Ruby object corresponding to the value of the token.
|
18
|
+
#
|
19
|
+
# `source_range`: \[`Sass::Source::Range`\]
|
20
|
+
# : The range in the source file in which the token appeared.
|
21
|
+
#
|
22
|
+
# `pos`: \[`Fixnum`\]
|
23
|
+
# : The scanner position at which the SassScript token appeared.
|
24
|
+
Token = Struct.new(:type, :value, :source_range, :pos)
|
25
|
+
|
26
|
+
# The line number of the lexer's current position.
|
27
|
+
#
|
28
|
+
# @return [Fixnum]
|
29
|
+
def line
|
30
|
+
return @line unless @tok
|
31
|
+
@tok.source_range.start_pos.line
|
32
|
+
end
|
33
|
+
|
34
|
+
# The number of bytes into the current line
|
35
|
+
# of the lexer's current position (1-based).
|
36
|
+
#
|
37
|
+
# @return [Fixnum]
|
38
|
+
def offset
|
39
|
+
return @offset unless @tok
|
40
|
+
@tok.source_range.start_pos.offset
|
41
|
+
end
|
42
|
+
|
43
|
+
# A hash from operator strings to the corresponding token types.
|
44
|
+
OPERATORS = {
|
45
|
+
'+' => :plus,
|
46
|
+
'-' => :minus,
|
47
|
+
'*' => :times,
|
48
|
+
'/' => :div,
|
49
|
+
'%' => :mod,
|
50
|
+
'=' => :single_eq,
|
51
|
+
':' => :colon,
|
52
|
+
'(' => :lparen,
|
53
|
+
')' => :rparen,
|
54
|
+
',' => :comma,
|
55
|
+
'and' => :and,
|
56
|
+
'or' => :or,
|
57
|
+
'not' => :not,
|
58
|
+
'==' => :eq,
|
59
|
+
'!=' => :neq,
|
60
|
+
'>=' => :gte,
|
61
|
+
'<=' => :lte,
|
62
|
+
'>' => :gt,
|
63
|
+
'<' => :lt,
|
64
|
+
'#{' => :begin_interpolation,
|
65
|
+
'}' => :end_interpolation,
|
66
|
+
';' => :semicolon,
|
67
|
+
'{' => :lcurly,
|
68
|
+
'...' => :splat,
|
69
|
+
}
|
70
|
+
|
71
|
+
OPERATORS_REVERSE = Sass::Util.map_hash(OPERATORS) {|k, v| [v, k]}
|
72
|
+
|
73
|
+
TOKEN_NAMES = Sass::Util.map_hash(OPERATORS_REVERSE) {|k, v| [k, v.inspect]}.merge(
|
74
|
+
:const => "variable (e.g. $foo)",
|
75
|
+
:ident => "identifier (e.g. middle)")
|
76
|
+
|
77
|
+
# A list of operator strings ordered with longer names first
|
78
|
+
# so that `>` and `<` don't clobber `>=` and `<=`.
|
79
|
+
OP_NAMES = OPERATORS.keys.sort_by {|o| -o.size}
|
80
|
+
|
81
|
+
# A sub-list of {OP_NAMES} that only includes operators
|
82
|
+
# with identifier names.
|
83
|
+
IDENT_OP_NAMES = OP_NAMES.select {|k, v| k =~ /^\w+/}
|
84
|
+
|
85
|
+
PARSEABLE_NUMBER = /(?:(\d*\.\d+)|(\d+))(?:[eE]([+-]?\d+))?(#{UNIT})?/
|
86
|
+
|
87
|
+
# A hash of regular expressions that are used for tokenizing.
|
88
|
+
REGULAR_EXPRESSIONS = {
|
89
|
+
:whitespace => /\s+/,
|
90
|
+
:comment => COMMENT,
|
91
|
+
:single_line_comment => SINGLE_LINE_COMMENT,
|
92
|
+
:variable => /(\$)(#{IDENT})/,
|
93
|
+
:ident => /(#{IDENT})(\()?/,
|
94
|
+
:number => PARSEABLE_NUMBER,
|
95
|
+
:unary_minus_number => /-#{PARSEABLE_NUMBER}/,
|
96
|
+
:color => HEXCOLOR,
|
97
|
+
:id => /##{IDENT}/,
|
98
|
+
:selector => /&/,
|
99
|
+
:ident_op => /(#{Regexp.union(*IDENT_OP_NAMES.map do |s|
|
100
|
+
Regexp.new(Regexp.escape(s) + "(?!#{NMCHAR}|\Z)")
|
101
|
+
end)})/,
|
102
|
+
:op => /(#{Regexp.union(*OP_NAMES)})/,
|
103
|
+
}
|
104
|
+
|
105
|
+
class << self
|
106
|
+
private
|
107
|
+
|
108
|
+
def string_re(open, close)
|
109
|
+
/#{open}((?:\\.|\#(?!\{)|[^#{close}\\#])*)(#{close}|#\{)/m
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# A hash of regular expressions that are used for tokenizing strings.
|
114
|
+
#
|
115
|
+
# The key is a `[Symbol, Boolean]` pair.
|
116
|
+
# The symbol represents which style of quotation to use,
|
117
|
+
# while the boolean represents whether or not the string
|
118
|
+
# is following an interpolated segment.
|
119
|
+
STRING_REGULAR_EXPRESSIONS = {
|
120
|
+
:double => {
|
121
|
+
false => string_re('"', '"'),
|
122
|
+
true => string_re('', '"')
|
123
|
+
},
|
124
|
+
:single => {
|
125
|
+
false => string_re("'", "'"),
|
126
|
+
true => string_re('', "'")
|
127
|
+
},
|
128
|
+
:uri => {
|
129
|
+
false => /url\(#{W}(#{URLCHAR}*?)(#{W}\)|#\{)/,
|
130
|
+
true => /(#{URLCHAR}*?)(#{W}\)|#\{)/
|
131
|
+
},
|
132
|
+
# Defined in https://developer.mozilla.org/en/CSS/@-moz-document as a
|
133
|
+
# non-standard version of http://www.w3.org/TR/css3-conditional/
|
134
|
+
:url_prefix => {
|
135
|
+
false => /url-prefix\(#{W}(#{URLCHAR}*?)(#{W}\)|#\{)/,
|
136
|
+
true => /(#{URLCHAR}*?)(#{W}\)|#\{)/
|
137
|
+
},
|
138
|
+
:domain => {
|
139
|
+
false => /domain\(#{W}(#{URLCHAR}*?)(#{W}\)|#\{)/,
|
140
|
+
true => /(#{URLCHAR}*?)(#{W}\)|#\{)/
|
141
|
+
}
|
142
|
+
}
|
143
|
+
|
144
|
+
# @param str [String, StringScanner] The source text to lex
|
145
|
+
# @param line [Fixnum] The 1-based line on which the SassScript appears.
|
146
|
+
# Used for error reporting and sourcemap building
|
147
|
+
# @param offset [Fixnum] The 1-based character (not byte) offset in the line in the source.
|
148
|
+
# Used for error reporting and sourcemap building
|
149
|
+
# @param options [{Symbol => Object}] An options hash;
|
150
|
+
# see {file:SASS_REFERENCE.md#sass_options the Sass options documentation}
|
151
|
+
def initialize(str, line, offset, options)
|
152
|
+
@scanner = str.is_a?(StringScanner) ? str : Sass::Util::MultibyteStringScanner.new(str)
|
153
|
+
@line = line
|
154
|
+
@offset = offset
|
155
|
+
@options = options
|
156
|
+
@interpolation_stack = []
|
157
|
+
@prev = nil
|
158
|
+
end
|
159
|
+
|
160
|
+
# Moves the lexer forward one token.
|
161
|
+
#
|
162
|
+
# @return [Token] The token that was moved past
|
163
|
+
def next
|
164
|
+
@tok ||= read_token
|
165
|
+
@tok, tok = nil, @tok
|
166
|
+
@prev = tok
|
167
|
+
tok
|
168
|
+
end
|
169
|
+
|
170
|
+
# Returns whether or not there's whitespace before the next token.
|
171
|
+
#
|
172
|
+
# @return [Boolean]
|
173
|
+
def whitespace?(tok = @tok)
|
174
|
+
if tok
|
175
|
+
@scanner.string[0...tok.pos] =~ /\s\Z/
|
176
|
+
else
|
177
|
+
@scanner.string[@scanner.pos, 1] =~ /^\s/ ||
|
178
|
+
@scanner.string[@scanner.pos - 1, 1] =~ /\s\Z/
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# Returns the next token without moving the lexer forward.
|
183
|
+
#
|
184
|
+
# @return [Token] The next token
|
185
|
+
def peek
|
186
|
+
@tok ||= read_token
|
187
|
+
end
|
188
|
+
|
189
|
+
# Rewinds the underlying StringScanner
|
190
|
+
# to before the token returned by \{#peek}.
|
191
|
+
def unpeek!
|
192
|
+
if @tok
|
193
|
+
@scanner.pos = @tok.pos
|
194
|
+
@line = @tok.source_range.start_pos.line
|
195
|
+
@offset = @tok.source_range.start_pos.offset
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# @return [Boolean] Whether or not there's more source text to lex.
|
200
|
+
def done?
|
201
|
+
whitespace unless after_interpolation? && @interpolation_stack.last
|
202
|
+
@scanner.eos? && @tok.nil?
|
203
|
+
end
|
204
|
+
|
205
|
+
# @return [Boolean] Whether or not the last token lexed was `:end_interpolation`.
|
206
|
+
def after_interpolation?
|
207
|
+
@prev && @prev.type == :end_interpolation
|
208
|
+
end
|
209
|
+
|
210
|
+
# Raise an error to the effect that `name` was expected in the input stream
|
211
|
+
# and wasn't found.
|
212
|
+
#
|
213
|
+
# This calls \{#unpeek!} to rewind the scanner to immediately after
|
214
|
+
# the last returned token.
|
215
|
+
#
|
216
|
+
# @param name [String] The name of the entity that was expected but not found
|
217
|
+
# @raise [Sass::SyntaxError]
|
218
|
+
def expected!(name)
|
219
|
+
unpeek!
|
220
|
+
Sass::SCSS::Parser.expected(@scanner, name, @line)
|
221
|
+
end
|
222
|
+
|
223
|
+
# Records all non-comment text the lexer consumes within the block
|
224
|
+
# and returns it as a string.
|
225
|
+
#
|
226
|
+
# @yield A block in which text is recorded
|
227
|
+
# @return [String]
|
228
|
+
def str
|
229
|
+
old_pos = @tok ? @tok.pos : @scanner.pos
|
230
|
+
yield
|
231
|
+
new_pos = @tok ? @tok.pos : @scanner.pos
|
232
|
+
@scanner.string[old_pos...new_pos]
|
233
|
+
end
|
234
|
+
|
235
|
+
private
|
236
|
+
|
237
|
+
def read_token
|
238
|
+
return if done?
|
239
|
+
start_pos = source_position
|
240
|
+
value = token
|
241
|
+
return unless value
|
242
|
+
type, val = value
|
243
|
+
Token.new(type, val, range(start_pos), @scanner.pos - @scanner.matched_size)
|
244
|
+
end
|
245
|
+
|
246
|
+
def whitespace
|
247
|
+
nil while scan(REGULAR_EXPRESSIONS[:whitespace]) ||
|
248
|
+
scan(REGULAR_EXPRESSIONS[:comment]) ||
|
249
|
+
scan(REGULAR_EXPRESSIONS[:single_line_comment])
|
250
|
+
end
|
251
|
+
|
252
|
+
def token
|
253
|
+
if after_interpolation? && (interp = @interpolation_stack.pop)
|
254
|
+
interp_type, interp_value = interp
|
255
|
+
if interp_type == :special_fun
|
256
|
+
return special_fun_body(interp_value)
|
257
|
+
else
|
258
|
+
raise "[BUG]: Unknown interp_type #{interp_type}" unless interp_type == :string
|
259
|
+
return string(interp_value, true)
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
variable || string(:double, false) || string(:single, false) || number || id || color ||
|
264
|
+
selector || string(:uri, false) || raw(UNICODERANGE) || special_fun || special_val ||
|
265
|
+
ident_op || ident || op
|
266
|
+
end
|
267
|
+
|
268
|
+
def variable
|
269
|
+
_variable(REGULAR_EXPRESSIONS[:variable])
|
270
|
+
end
|
271
|
+
|
272
|
+
def _variable(rx)
|
273
|
+
return unless scan(rx)
|
274
|
+
|
275
|
+
[:const, @scanner[2]]
|
276
|
+
end
|
277
|
+
|
278
|
+
def ident
|
279
|
+
return unless scan(REGULAR_EXPRESSIONS[:ident])
|
280
|
+
[@scanner[2] ? :funcall : :ident, @scanner[1]]
|
281
|
+
end
|
282
|
+
|
283
|
+
def string(re, open)
|
284
|
+
line, offset = @line, @offset
|
285
|
+
return unless scan(STRING_REGULAR_EXPRESSIONS[re][open])
|
286
|
+
if @scanner[0] =~ /([^\\]|^)\n/
|
287
|
+
filename = @options[:filename]
|
288
|
+
Sass::Util.sass_warn <<MESSAGE
|
289
|
+
DEPRECATION WARNING on line #{line}, column #{offset}#{" of #{filename}" if filename}:
|
290
|
+
Unescaped multiline strings are deprecated and will be removed in a future version of Sass.
|
291
|
+
To include a newline in a string, use "\\a" or "\\a " as in CSS.
|
292
|
+
MESSAGE
|
293
|
+
end
|
294
|
+
|
295
|
+
if @scanner[2] == '#{' # '
|
296
|
+
@scanner.pos -= 2 # Don't actually consume the #{
|
297
|
+
@offset -= 2
|
298
|
+
@interpolation_stack << [:string, re]
|
299
|
+
end
|
300
|
+
str =
|
301
|
+
if re == :uri
|
302
|
+
url = "#{'url(' unless open}#{@scanner[1]}#{')' unless @scanner[2] == '#{'}"
|
303
|
+
Script::Value::String.new(url)
|
304
|
+
else
|
305
|
+
Script::Value::String.new(Sass::Script::Value::String.value(@scanner[1]), :string)
|
306
|
+
end
|
307
|
+
[:string, str]
|
308
|
+
end
|
309
|
+
|
310
|
+
def number
|
311
|
+
# Handling unary minus is complicated by the fact that whitespace is an
|
312
|
+
# operator in SassScript. We want "1-2" to be parsed as "1 - 2", but we
|
313
|
+
# want "1 -2" to be parsed as "1 (-2)". To accomplish this, we only
|
314
|
+
# parse a unary minus as part of a number literal if there's whitespace
|
315
|
+
# before and not after it. Cases like "(-2)" are handled by the unary
|
316
|
+
# minus logic in the parser instead.
|
317
|
+
if @scanner.peek(1) == '-'
|
318
|
+
return if @scanner.pos == 0
|
319
|
+
unary_minus_allowed =
|
320
|
+
case @scanner.string[@scanner.pos - 1, 1]
|
321
|
+
when /\s/; true
|
322
|
+
when '/'; @scanner.pos != 1 && @scanner.string[@scanner.pos - 2, 1] == '*'
|
323
|
+
else; false
|
324
|
+
end
|
325
|
+
|
326
|
+
return unless unary_minus_allowed
|
327
|
+
return unless scan(REGULAR_EXPRESSIONS[:unary_minus_number])
|
328
|
+
minus = true
|
329
|
+
else
|
330
|
+
return unless scan(REGULAR_EXPRESSIONS[:number])
|
331
|
+
minus = false
|
332
|
+
end
|
333
|
+
|
334
|
+
value = (@scanner[1] ? @scanner[1].to_f : @scanner[2].to_i) * (minus ? -1 : 1)
|
335
|
+
value *= 10**@scanner[3].to_i if @scanner[3]
|
336
|
+
script_number = Script::Value::Number.new(value, Array(@scanner[4]))
|
337
|
+
[:number, script_number]
|
338
|
+
end
|
339
|
+
|
340
|
+
def id
|
341
|
+
# Colors and ids are tough to tell apart, because they overlap but
|
342
|
+
# neither is a superset of the other. "#xyz" is an id but not a color,
|
343
|
+
# "#000" is a color but not an id, "#abc" is both, and "#0" is neither.
|
344
|
+
# We need to handle all these cases correctly.
|
345
|
+
#
|
346
|
+
# To do so, we first try to parse something as an id. If this works and
|
347
|
+
# the id is also a valid color, we return the color. Otherwise, we
|
348
|
+
# return the id. If it didn't parse as an id, we then try to parse it as
|
349
|
+
# a color. If *this* works, we return the color, and if it doesn't we
|
350
|
+
# give up and throw an error.
|
351
|
+
#
|
352
|
+
# IDs in properties are used in the Basic User Interface Module
|
353
|
+
# (http://www.w3.org/TR/css3-ui/).
|
354
|
+
return unless scan(REGULAR_EXPRESSIONS[:id])
|
355
|
+
if @scanner[0] =~ /^\#[0-9a-fA-F]+$/ && (@scanner[0].length == 4 || @scanner[0].length == 7)
|
356
|
+
return [:color, Script::Value::Color.from_hex(@scanner[0])]
|
357
|
+
end
|
358
|
+
[:ident, @scanner[0]]
|
359
|
+
end
|
360
|
+
|
361
|
+
def color
|
362
|
+
return unless @scanner.match?(REGULAR_EXPRESSIONS[:color])
|
363
|
+
return unless @scanner[0].length == 4 || @scanner[0].length == 7
|
364
|
+
script_color = Script::Value::Color.from_hex(scan(REGULAR_EXPRESSIONS[:color]))
|
365
|
+
[:color, script_color]
|
366
|
+
end
|
367
|
+
|
368
|
+
def selector
|
369
|
+
start_pos = source_position
|
370
|
+
return unless scan(REGULAR_EXPRESSIONS[:selector])
|
371
|
+
script_selector = Script::Tree::Selector.new
|
372
|
+
script_selector.source_range = range(start_pos)
|
373
|
+
[:selector, script_selector]
|
374
|
+
end
|
375
|
+
|
376
|
+
def special_fun
|
377
|
+
prefix = scan(/((-[\w-]+-)?(calc|element)|expression|progid:[a-z\.]*)\(/i)
|
378
|
+
return unless prefix
|
379
|
+
special_fun_body(1, prefix)
|
380
|
+
end
|
381
|
+
|
382
|
+
def special_fun_body(parens, prefix = nil)
|
383
|
+
str = prefix || ''
|
384
|
+
while (scanned = scan(/.*?([()]|\#\{)/m))
|
385
|
+
str << scanned
|
386
|
+
if scanned[-1] == ?(
|
387
|
+
parens += 1
|
388
|
+
next
|
389
|
+
elsif scanned[-1] == ?)
|
390
|
+
parens -= 1
|
391
|
+
next unless parens == 0
|
392
|
+
else
|
393
|
+
raise "[BUG] Unreachable" unless @scanner[1] == '#{' # '
|
394
|
+
str.slice!(-2..-1)
|
395
|
+
@scanner.pos -= 2 # Don't actually consume the #{
|
396
|
+
@offset -= 2
|
397
|
+
@interpolation_stack << [:special_fun, parens]
|
398
|
+
end
|
399
|
+
|
400
|
+
return [:special_fun, Sass::Script::Value::String.new(str)]
|
401
|
+
end
|
402
|
+
|
403
|
+
scan(/.*/)
|
404
|
+
expected!('")"')
|
405
|
+
end
|
406
|
+
|
407
|
+
def special_val
|
408
|
+
return unless scan(/!important/i)
|
409
|
+
[:string, Script::Value::String.new("!important")]
|
410
|
+
end
|
411
|
+
|
412
|
+
def ident_op
|
413
|
+
op = scan(REGULAR_EXPRESSIONS[:ident_op])
|
414
|
+
return unless op
|
415
|
+
[OPERATORS[op]]
|
416
|
+
end
|
417
|
+
|
418
|
+
def op
|
419
|
+
op = scan(REGULAR_EXPRESSIONS[:op])
|
420
|
+
return unless op
|
421
|
+
@interpolation_stack << nil if op == :begin_interpolation
|
422
|
+
[OPERATORS[op]]
|
423
|
+
end
|
424
|
+
|
425
|
+
def raw(rx)
|
426
|
+
val = scan(rx)
|
427
|
+
return unless val
|
428
|
+
[:raw, val]
|
429
|
+
end
|
430
|
+
|
431
|
+
def scan(re)
|
432
|
+
str = @scanner.scan(re)
|
433
|
+
return unless str
|
434
|
+
c = str.count("\n")
|
435
|
+
@line += c
|
436
|
+
@offset = (c == 0 ? @offset + str.size : str.size - str.rindex("\n"))
|
437
|
+
str
|
438
|
+
end
|
439
|
+
|
440
|
+
def range(start_pos, end_pos = source_position)
|
441
|
+
Sass::Source::Range.new(start_pos, end_pos, @options[:filename], @options[:importer])
|
442
|
+
end
|
443
|
+
|
444
|
+
def source_position
|
445
|
+
Sass::Source::Position.new(@line, @offset)
|
446
|
+
end
|
447
|
+
end
|
448
|
+
end
|
449
|
+
end
|
@@ -0,0 +1,637 @@
|
|
1
|
+
require 'sass/script/lexer'
|
2
|
+
|
3
|
+
module Sass
|
4
|
+
module Script
|
5
|
+
# The parser for SassScript.
|
6
|
+
# It parses a string of code into a tree of {Script::Tree::Node}s.
|
7
|
+
class Parser
|
8
|
+
# The line number of the parser's current position.
|
9
|
+
#
|
10
|
+
# @return [Fixnum]
|
11
|
+
def line
|
12
|
+
@lexer.line
|
13
|
+
end
|
14
|
+
|
15
|
+
# The column number of the parser's current position.
|
16
|
+
#
|
17
|
+
# @return [Fixnum]
|
18
|
+
def offset
|
19
|
+
@lexer.offset
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param str [String, StringScanner] The source text to parse
|
23
|
+
# @param line [Fixnum] The line on which the SassScript appears.
|
24
|
+
# Used for error reporting and sourcemap building
|
25
|
+
# @param offset [Fixnum] The character (not byte) offset where the script starts in the line.
|
26
|
+
# Used for error reporting and sourcemap building
|
27
|
+
# @param options [{Symbol => Object}] An options hash;
|
28
|
+
# see {file:SASS_REFERENCE.md#sass_options the Sass options documentation}
|
29
|
+
def initialize(str, line, offset, options = {})
|
30
|
+
@options = options
|
31
|
+
@lexer = lexer_class.new(str, line, offset, options)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Parses a SassScript expression within an interpolated segment (`#{}`).
|
35
|
+
# This means that it stops when it comes across an unmatched `}`,
|
36
|
+
# which signals the end of an interpolated segment,
|
37
|
+
# it returns rather than throwing an error.
|
38
|
+
#
|
39
|
+
# @param warn_for_color [Boolean] Whether raw color values passed to
|
40
|
+
# interoplation should cause a warning.
|
41
|
+
# @return [Script::Tree::Node] The root node of the parse tree
|
42
|
+
# @raise [Sass::SyntaxError] if the expression isn't valid SassScript
|
43
|
+
def parse_interpolated(warn_for_color = false)
|
44
|
+
# Start two characters back to compensate for #{
|
45
|
+
start_pos = Sass::Source::Position.new(line, offset - 2)
|
46
|
+
expr = assert_expr :expr
|
47
|
+
assert_tok :end_interpolation
|
48
|
+
expr = Sass::Script::Tree::Interpolation.new(
|
49
|
+
nil, expr, nil, !:wb, !:wa, !:originally_text, warn_for_color)
|
50
|
+
expr.options = @options
|
51
|
+
node(expr, start_pos)
|
52
|
+
rescue Sass::SyntaxError => e
|
53
|
+
e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
|
54
|
+
raise e
|
55
|
+
end
|
56
|
+
|
57
|
+
# Parses a SassScript expression.
|
58
|
+
#
|
59
|
+
# @return [Script::Tree::Node] The root node of the parse tree
|
60
|
+
# @raise [Sass::SyntaxError] if the expression isn't valid SassScript
|
61
|
+
def parse
|
62
|
+
expr = assert_expr :expr
|
63
|
+
assert_done
|
64
|
+
expr.options = @options
|
65
|
+
expr
|
66
|
+
rescue Sass::SyntaxError => e
|
67
|
+
e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
|
68
|
+
raise e
|
69
|
+
end
|
70
|
+
|
71
|
+
# Parses a SassScript expression,
|
72
|
+
# ending it when it encounters one of the given identifier tokens.
|
73
|
+
#
|
74
|
+
# @param tokens [#include?(String)] A set of strings that delimit the expression.
|
75
|
+
# @return [Script::Tree::Node] The root node of the parse tree
|
76
|
+
# @raise [Sass::SyntaxError] if the expression isn't valid SassScript
|
77
|
+
def parse_until(tokens)
|
78
|
+
@stop_at = tokens
|
79
|
+
expr = assert_expr :expr
|
80
|
+
assert_done
|
81
|
+
expr.options = @options
|
82
|
+
expr
|
83
|
+
rescue Sass::SyntaxError => e
|
84
|
+
e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
|
85
|
+
raise e
|
86
|
+
end
|
87
|
+
|
88
|
+
# Parses the argument list for a mixin include.
|
89
|
+
#
|
90
|
+
# @return [(Array<Script::Tree::Node>,
|
91
|
+
# {String => Script::Tree::Node},
|
92
|
+
# Script::Tree::Node,
|
93
|
+
# Script::Tree::Node)]
|
94
|
+
# The root nodes of the positional arguments, keyword arguments, and
|
95
|
+
# splat argument(s). Keyword arguments are in a hash from names to values.
|
96
|
+
# @raise [Sass::SyntaxError] if the argument list isn't valid SassScript
|
97
|
+
def parse_mixin_include_arglist
|
98
|
+
args, keywords = [], {}
|
99
|
+
if try_tok(:lparen)
|
100
|
+
args, keywords, splat, kwarg_splat = mixin_arglist
|
101
|
+
assert_tok(:rparen)
|
102
|
+
end
|
103
|
+
assert_done
|
104
|
+
|
105
|
+
args.each {|a| a.options = @options}
|
106
|
+
keywords.each {|k, v| v.options = @options}
|
107
|
+
splat.options = @options if splat
|
108
|
+
kwarg_splat.options = @options if kwarg_splat
|
109
|
+
return args, keywords, splat, kwarg_splat
|
110
|
+
rescue Sass::SyntaxError => e
|
111
|
+
e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
|
112
|
+
raise e
|
113
|
+
end
|
114
|
+
|
115
|
+
# Parses the argument list for a mixin definition.
|
116
|
+
#
|
117
|
+
# @return [(Array<Script::Tree::Node>, Script::Tree::Node)]
|
118
|
+
# The root nodes of the arguments, and the splat argument.
|
119
|
+
# @raise [Sass::SyntaxError] if the argument list isn't valid SassScript
|
120
|
+
def parse_mixin_definition_arglist
|
121
|
+
args, splat = defn_arglist!(false)
|
122
|
+
assert_done
|
123
|
+
|
124
|
+
args.each do |k, v|
|
125
|
+
k.options = @options
|
126
|
+
v.options = @options if v
|
127
|
+
end
|
128
|
+
splat.options = @options if splat
|
129
|
+
return args, splat
|
130
|
+
rescue Sass::SyntaxError => e
|
131
|
+
e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
|
132
|
+
raise e
|
133
|
+
end
|
134
|
+
|
135
|
+
# Parses the argument list for a function definition.
|
136
|
+
#
|
137
|
+
# @return [(Array<Script::Tree::Node>, Script::Tree::Node)]
|
138
|
+
# The root nodes of the arguments, and the splat argument.
|
139
|
+
# @raise [Sass::SyntaxError] if the argument list isn't valid SassScript
|
140
|
+
def parse_function_definition_arglist
|
141
|
+
args, splat = defn_arglist!(true)
|
142
|
+
assert_done
|
143
|
+
|
144
|
+
args.each do |k, v|
|
145
|
+
k.options = @options
|
146
|
+
v.options = @options if v
|
147
|
+
end
|
148
|
+
splat.options = @options if splat
|
149
|
+
return args, splat
|
150
|
+
rescue Sass::SyntaxError => e
|
151
|
+
e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
|
152
|
+
raise e
|
153
|
+
end
|
154
|
+
|
155
|
+
# Parse a single string value, possibly containing interpolation.
|
156
|
+
# Doesn't assert that the scanner is finished after parsing.
|
157
|
+
#
|
158
|
+
# @return [Script::Tree::Node] The root node of the parse tree.
|
159
|
+
# @raise [Sass::SyntaxError] if the string isn't valid SassScript
|
160
|
+
def parse_string
|
161
|
+
unless (peek = @lexer.peek) &&
|
162
|
+
(peek.type == :string ||
|
163
|
+
(peek.type == :funcall && peek.value.downcase == 'url'))
|
164
|
+
lexer.expected!("string")
|
165
|
+
end
|
166
|
+
|
167
|
+
expr = assert_expr :funcall
|
168
|
+
expr.options = @options
|
169
|
+
@lexer.unpeek!
|
170
|
+
expr
|
171
|
+
rescue Sass::SyntaxError => e
|
172
|
+
e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
|
173
|
+
raise e
|
174
|
+
end
|
175
|
+
|
176
|
+
# Parses a SassScript expression.
|
177
|
+
#
|
178
|
+
# @overload parse(str, line, offset, filename = nil)
|
179
|
+
# @return [Script::Tree::Node] The root node of the parse tree
|
180
|
+
# @see Parser#initialize
|
181
|
+
# @see Parser#parse
|
182
|
+
def self.parse(*args)
|
183
|
+
new(*args).parse
|
184
|
+
end
|
185
|
+
|
186
|
+
PRECEDENCE = [
|
187
|
+
:comma, :single_eq, :space, :or, :and,
|
188
|
+
[:eq, :neq],
|
189
|
+
[:gt, :gte, :lt, :lte],
|
190
|
+
[:plus, :minus],
|
191
|
+
[:times, :div, :mod],
|
192
|
+
]
|
193
|
+
|
194
|
+
ASSOCIATIVE = [:plus, :times]
|
195
|
+
|
196
|
+
class << self
|
197
|
+
# Returns an integer representing the precedence
|
198
|
+
# of the given operator.
|
199
|
+
# A lower integer indicates a looser binding.
|
200
|
+
#
|
201
|
+
# @private
|
202
|
+
def precedence_of(op)
|
203
|
+
PRECEDENCE.each_with_index do |e, i|
|
204
|
+
return i if Array(e).include?(op)
|
205
|
+
end
|
206
|
+
raise "[BUG] Unknown operator #{op.inspect}"
|
207
|
+
end
|
208
|
+
|
209
|
+
# Returns whether or not the given operation is associative.
|
210
|
+
#
|
211
|
+
# @private
|
212
|
+
def associative?(op)
|
213
|
+
ASSOCIATIVE.include?(op)
|
214
|
+
end
|
215
|
+
|
216
|
+
private
|
217
|
+
|
218
|
+
# Defines a simple left-associative production.
|
219
|
+
# name is the name of the production,
|
220
|
+
# sub is the name of the production beneath it,
|
221
|
+
# and ops is a list of operators for this precedence level
|
222
|
+
def production(name, sub, *ops)
|
223
|
+
class_eval <<RUBY, __FILE__, __LINE__ + 1
|
224
|
+
def #{name}
|
225
|
+
interp = try_ops_after_interp(#{ops.inspect}, #{name.inspect})
|
226
|
+
return interp if interp
|
227
|
+
return unless e = #{sub}
|
228
|
+
while tok = try_toks(#{ops.map {|o| o.inspect}.join(', ')})
|
229
|
+
if interp = try_op_before_interp(tok, e)
|
230
|
+
other_interp = try_ops_after_interp(#{ops.inspect}, #{name.inspect}, interp)
|
231
|
+
return interp unless other_interp
|
232
|
+
return other_interp
|
233
|
+
end
|
234
|
+
|
235
|
+
e = node(Tree::Operation.new(e, assert_expr(#{sub.inspect}), tok.type),
|
236
|
+
e.source_range.start_pos)
|
237
|
+
end
|
238
|
+
e
|
239
|
+
end
|
240
|
+
RUBY
|
241
|
+
end
|
242
|
+
|
243
|
+
def unary(op, sub)
|
244
|
+
class_eval <<RUBY, __FILE__, __LINE__ + 1
|
245
|
+
def unary_#{op}
|
246
|
+
return #{sub} unless tok = try_tok(:#{op})
|
247
|
+
interp = try_op_before_interp(tok)
|
248
|
+
return interp if interp
|
249
|
+
start_pos = source_position
|
250
|
+
node(Tree::UnaryOperation.new(assert_expr(:unary_#{op}), :#{op}), start_pos)
|
251
|
+
end
|
252
|
+
RUBY
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
private
|
257
|
+
|
258
|
+
def source_position
|
259
|
+
Sass::Source::Position.new(line, offset)
|
260
|
+
end
|
261
|
+
|
262
|
+
def range(start_pos, end_pos = source_position)
|
263
|
+
Sass::Source::Range.new(start_pos, end_pos, @options[:filename], @options[:importer])
|
264
|
+
end
|
265
|
+
|
266
|
+
# @private
|
267
|
+
def lexer_class; Lexer; end
|
268
|
+
|
269
|
+
def map
|
270
|
+
start_pos = source_position
|
271
|
+
e = interpolation
|
272
|
+
return unless e
|
273
|
+
return list e, start_pos unless @lexer.peek && @lexer.peek.type == :colon
|
274
|
+
|
275
|
+
pair = map_pair(e)
|
276
|
+
map = node(Sass::Script::Tree::MapLiteral.new([pair]), start_pos)
|
277
|
+
while try_tok(:comma)
|
278
|
+
pair = map_pair
|
279
|
+
return map unless pair
|
280
|
+
map.pairs << pair
|
281
|
+
end
|
282
|
+
map
|
283
|
+
end
|
284
|
+
|
285
|
+
def map_pair(key = nil)
|
286
|
+
return unless key ||= interpolation
|
287
|
+
assert_tok :colon
|
288
|
+
return key, assert_expr(:interpolation)
|
289
|
+
end
|
290
|
+
|
291
|
+
def expr
|
292
|
+
start_pos = source_position
|
293
|
+
e = interpolation
|
294
|
+
return unless e
|
295
|
+
list e, start_pos
|
296
|
+
end
|
297
|
+
|
298
|
+
def list(first, start_pos)
|
299
|
+
return first unless @lexer.peek && @lexer.peek.type == :comma
|
300
|
+
|
301
|
+
list = node(Sass::Script::Tree::ListLiteral.new([first], :comma), start_pos)
|
302
|
+
while (tok = try_tok(:comma))
|
303
|
+
element_before_interp = list.elements.length == 1 ? list.elements.first : list
|
304
|
+
if (interp = try_op_before_interp(tok, element_before_interp))
|
305
|
+
other_interp = try_ops_after_interp([:comma], :expr, interp)
|
306
|
+
return interp unless other_interp
|
307
|
+
return other_interp
|
308
|
+
end
|
309
|
+
return list unless (e = interpolation)
|
310
|
+
list.elements << e
|
311
|
+
end
|
312
|
+
list
|
313
|
+
end
|
314
|
+
|
315
|
+
production :equals, :interpolation, :single_eq
|
316
|
+
|
317
|
+
def try_op_before_interp(op, prev = nil)
|
318
|
+
return unless @lexer.peek && @lexer.peek.type == :begin_interpolation
|
319
|
+
wb = @lexer.whitespace?(op)
|
320
|
+
str = literal_node(Script::Value::String.new(Lexer::OPERATORS_REVERSE[op.type]),
|
321
|
+
op.source_range)
|
322
|
+
interp = node(
|
323
|
+
Script::Tree::Interpolation.new(prev, str, nil, wb, !:wa, :originally_text),
|
324
|
+
(prev || str).source_range.start_pos)
|
325
|
+
interpolation(interp)
|
326
|
+
end
|
327
|
+
|
328
|
+
def try_ops_after_interp(ops, name, prev = nil)
|
329
|
+
return unless @lexer.after_interpolation?
|
330
|
+
op = try_toks(*ops)
|
331
|
+
return unless op
|
332
|
+
interp = try_op_before_interp(op, prev)
|
333
|
+
return interp if interp
|
334
|
+
|
335
|
+
wa = @lexer.whitespace?
|
336
|
+
str = literal_node(Script::Value::String.new(Lexer::OPERATORS_REVERSE[op.type]),
|
337
|
+
op.source_range)
|
338
|
+
str.line = @lexer.line
|
339
|
+
interp = node(
|
340
|
+
Script::Tree::Interpolation.new(prev, str, assert_expr(name), !:wb, wa, :originally_text),
|
341
|
+
(prev || str).source_range.start_pos)
|
342
|
+
interp
|
343
|
+
end
|
344
|
+
|
345
|
+
def interpolation(first = space)
|
346
|
+
e = first
|
347
|
+
while (interp = try_tok(:begin_interpolation))
|
348
|
+
wb = @lexer.whitespace?(interp)
|
349
|
+
mid = assert_expr :expr
|
350
|
+
assert_tok :end_interpolation
|
351
|
+
wa = @lexer.whitespace?
|
352
|
+
e = node(
|
353
|
+
Script::Tree::Interpolation.new(e, mid, space, wb, wa),
|
354
|
+
(e || mid).source_range.start_pos)
|
355
|
+
end
|
356
|
+
e
|
357
|
+
end
|
358
|
+
|
359
|
+
def space
|
360
|
+
start_pos = source_position
|
361
|
+
e = or_expr
|
362
|
+
return unless e
|
363
|
+
arr = [e]
|
364
|
+
while (e = or_expr)
|
365
|
+
arr << e
|
366
|
+
end
|
367
|
+
if arr.size == 1
|
368
|
+
arr.first
|
369
|
+
else
|
370
|
+
node(Sass::Script::Tree::ListLiteral.new(arr, :space), start_pos)
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
production :or_expr, :and_expr, :or
|
375
|
+
production :and_expr, :eq_or_neq, :and
|
376
|
+
production :eq_or_neq, :relational, :eq, :neq
|
377
|
+
production :relational, :plus_or_minus, :gt, :gte, :lt, :lte
|
378
|
+
production :plus_or_minus, :times_div_or_mod, :plus, :minus
|
379
|
+
production :times_div_or_mod, :unary_plus, :times, :div, :mod
|
380
|
+
|
381
|
+
unary :plus, :unary_minus
|
382
|
+
unary :minus, :unary_div
|
383
|
+
unary :div, :unary_not # For strings, so /foo/bar works
|
384
|
+
unary :not, :ident
|
385
|
+
|
386
|
+
def ident
|
387
|
+
return funcall unless @lexer.peek && @lexer.peek.type == :ident
|
388
|
+
return if @stop_at && @stop_at.include?(@lexer.peek.value)
|
389
|
+
|
390
|
+
name = @lexer.next
|
391
|
+
if (color = Sass::Script::Value::Color::COLOR_NAMES[name.value.downcase])
|
392
|
+
literal_node(Sass::Script::Value::Color.new(color, name.value), name.source_range)
|
393
|
+
elsif name.value == "true"
|
394
|
+
literal_node(Sass::Script::Value::Bool.new(true), name.source_range)
|
395
|
+
elsif name.value == "false"
|
396
|
+
literal_node(Sass::Script::Value::Bool.new(false), name.source_range)
|
397
|
+
elsif name.value == "null"
|
398
|
+
literal_node(Sass::Script::Value::Null.new, name.source_range)
|
399
|
+
else
|
400
|
+
literal_node(Sass::Script::Value::String.new(name.value, :identifier), name.source_range)
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
404
|
+
def funcall
|
405
|
+
tok = try_tok(:funcall)
|
406
|
+
return raw unless tok
|
407
|
+
args, keywords, splat, kwarg_splat = fn_arglist
|
408
|
+
assert_tok(:rparen)
|
409
|
+
node(Script::Tree::Funcall.new(tok.value, args, keywords, splat, kwarg_splat),
|
410
|
+
tok.source_range.start_pos, source_position)
|
411
|
+
end
|
412
|
+
|
413
|
+
def defn_arglist!(must_have_parens)
|
414
|
+
if must_have_parens
|
415
|
+
assert_tok(:lparen)
|
416
|
+
else
|
417
|
+
return [], nil unless try_tok(:lparen)
|
418
|
+
end
|
419
|
+
return [], nil if try_tok(:rparen)
|
420
|
+
|
421
|
+
res = []
|
422
|
+
splat = nil
|
423
|
+
must_have_default = false
|
424
|
+
loop do
|
425
|
+
c = assert_tok(:const)
|
426
|
+
var = node(Script::Tree::Variable.new(c.value), c.source_range)
|
427
|
+
if try_tok(:colon)
|
428
|
+
val = assert_expr(:space)
|
429
|
+
must_have_default = true
|
430
|
+
elsif try_tok(:splat)
|
431
|
+
splat = var
|
432
|
+
break
|
433
|
+
elsif must_have_default
|
434
|
+
raise SyntaxError.new(
|
435
|
+
"Required argument #{var.inspect} must come before any optional arguments.")
|
436
|
+
end
|
437
|
+
res << [var, val]
|
438
|
+
break unless try_tok(:comma)
|
439
|
+
end
|
440
|
+
assert_tok(:rparen)
|
441
|
+
return res, splat
|
442
|
+
end
|
443
|
+
|
444
|
+
def fn_arglist
|
445
|
+
arglist(:equals, "function argument")
|
446
|
+
end
|
447
|
+
|
448
|
+
def mixin_arglist
|
449
|
+
arglist(:interpolation, "mixin argument")
|
450
|
+
end
|
451
|
+
|
452
|
+
def arglist(subexpr, description)
|
453
|
+
args = []
|
454
|
+
keywords = Sass::Util::NormalizedMap.new
|
455
|
+
e = send(subexpr)
|
456
|
+
|
457
|
+
return [args, keywords] unless e
|
458
|
+
|
459
|
+
splat = nil
|
460
|
+
loop do
|
461
|
+
if @lexer.peek && @lexer.peek.type == :colon
|
462
|
+
name = e
|
463
|
+
@lexer.expected!("comma") unless name.is_a?(Tree::Variable)
|
464
|
+
assert_tok(:colon)
|
465
|
+
value = assert_expr(subexpr, description)
|
466
|
+
|
467
|
+
if keywords[name.name]
|
468
|
+
raise SyntaxError.new("Keyword argument \"#{name.to_sass}\" passed more than once")
|
469
|
+
end
|
470
|
+
|
471
|
+
keywords[name.name] = value
|
472
|
+
else
|
473
|
+
if try_tok(:splat)
|
474
|
+
return args, keywords, splat, e if splat
|
475
|
+
splat, e = e, nil
|
476
|
+
elsif splat
|
477
|
+
raise SyntaxError.new("Only keyword arguments may follow variable arguments (...).")
|
478
|
+
elsif !keywords.empty?
|
479
|
+
raise SyntaxError.new("Positional arguments must come before keyword arguments.")
|
480
|
+
end
|
481
|
+
|
482
|
+
args << e if e
|
483
|
+
end
|
484
|
+
|
485
|
+
return args, keywords, splat unless try_tok(:comma)
|
486
|
+
e = assert_expr(subexpr, description)
|
487
|
+
end
|
488
|
+
end
|
489
|
+
|
490
|
+
def raw
|
491
|
+
tok = try_tok(:raw)
|
492
|
+
return special_fun unless tok
|
493
|
+
literal_node(Script::Value::String.new(tok.value), tok.source_range)
|
494
|
+
end
|
495
|
+
|
496
|
+
def special_fun
|
497
|
+
first = try_tok(:special_fun)
|
498
|
+
return paren unless first
|
499
|
+
str = literal_node(first.value, first.source_range)
|
500
|
+
return str unless try_tok(:begin_interpolation)
|
501
|
+
mid = parse_interpolated
|
502
|
+
last = assert_expr(:special_fun)
|
503
|
+
node(Tree::Interpolation.new(str, mid, last, false, false),
|
504
|
+
first.source_range.start_pos)
|
505
|
+
end
|
506
|
+
|
507
|
+
def paren
|
508
|
+
return variable unless try_tok(:lparen)
|
509
|
+
was_in_parens = @in_parens
|
510
|
+
@in_parens = true
|
511
|
+
start_pos = source_position
|
512
|
+
e = map
|
513
|
+
end_pos = source_position
|
514
|
+
assert_tok(:rparen)
|
515
|
+
return e || node(Sass::Script::Tree::ListLiteral.new([], nil), start_pos, end_pos)
|
516
|
+
ensure
|
517
|
+
@in_parens = was_in_parens
|
518
|
+
end
|
519
|
+
|
520
|
+
def variable
|
521
|
+
start_pos = source_position
|
522
|
+
c = try_tok(:const)
|
523
|
+
return string unless c
|
524
|
+
node(Tree::Variable.new(*c.value), start_pos)
|
525
|
+
end
|
526
|
+
|
527
|
+
def string
|
528
|
+
first = try_tok(:string)
|
529
|
+
return number unless first
|
530
|
+
str = literal_node(first.value, first.source_range)
|
531
|
+
return str unless try_tok(:begin_interpolation)
|
532
|
+
mid = assert_expr :expr
|
533
|
+
assert_tok :end_interpolation
|
534
|
+
last = assert_expr(:string)
|
535
|
+
node(Tree::StringInterpolation.new(str, mid, last), first.source_range.start_pos)
|
536
|
+
end
|
537
|
+
|
538
|
+
def number
|
539
|
+
tok = try_tok(:number)
|
540
|
+
return selector unless tok
|
541
|
+
num = tok.value
|
542
|
+
num.original = num.to_s unless @in_parens
|
543
|
+
literal_node(num, tok.source_range.start_pos)
|
544
|
+
end
|
545
|
+
|
546
|
+
def selector
|
547
|
+
tok = try_tok(:selector)
|
548
|
+
return literal unless tok
|
549
|
+
node(tok.value, tok.source_range.start_pos)
|
550
|
+
end
|
551
|
+
|
552
|
+
def literal
|
553
|
+
t = try_tok(:color)
|
554
|
+
return literal_node(t.value, t.source_range) if t
|
555
|
+
end
|
556
|
+
|
557
|
+
# It would be possible to have unified #assert and #try methods,
|
558
|
+
# but detecting the method/token difference turns out to be quite expensive.
|
559
|
+
|
560
|
+
EXPR_NAMES = {
|
561
|
+
:string => "string",
|
562
|
+
:default => "expression (e.g. 1px, bold)",
|
563
|
+
:mixin_arglist => "mixin argument",
|
564
|
+
:fn_arglist => "function argument",
|
565
|
+
:splat => "...",
|
566
|
+
:special_fun => '")"',
|
567
|
+
}
|
568
|
+
|
569
|
+
def assert_expr(name, expected = nil)
|
570
|
+
e = send(name)
|
571
|
+
return e if e
|
572
|
+
@lexer.expected!(expected || EXPR_NAMES[name] || EXPR_NAMES[:default])
|
573
|
+
end
|
574
|
+
|
575
|
+
def assert_tok(name)
|
576
|
+
# Avoids an array allocation caused by argument globbing in assert_toks.
|
577
|
+
t = try_tok(name)
|
578
|
+
return t if t
|
579
|
+
@lexer.expected!(Lexer::TOKEN_NAMES[name] || name.to_s)
|
580
|
+
end
|
581
|
+
|
582
|
+
def assert_toks(*names)
|
583
|
+
t = try_toks(*names)
|
584
|
+
return t if t
|
585
|
+
@lexer.expected!(names.map {|tok| Lexer::TOKEN_NAMES[tok] || tok}.join(" or "))
|
586
|
+
end
|
587
|
+
|
588
|
+
def try_tok(name)
|
589
|
+
# Avoids an array allocation caused by argument globbing in the try_toks method.
|
590
|
+
peeked = @lexer.peek
|
591
|
+
peeked && name == peeked.type && @lexer.next
|
592
|
+
end
|
593
|
+
|
594
|
+
def try_toks(*names)
|
595
|
+
peeked = @lexer.peek
|
596
|
+
peeked && names.include?(peeked.type) && @lexer.next
|
597
|
+
end
|
598
|
+
|
599
|
+
def assert_done
|
600
|
+
return if @lexer.done?
|
601
|
+
@lexer.expected!(EXPR_NAMES[:default])
|
602
|
+
end
|
603
|
+
|
604
|
+
# @overload node(value, source_range)
|
605
|
+
# @param value [Sass::Script::Value::Base]
|
606
|
+
# @param source_range [Sass::Source::Range]
|
607
|
+
# @overload node(value, start_pos, end_pos = source_position)
|
608
|
+
# @param value [Sass::Script::Value::Base]
|
609
|
+
# @param start_pos [Sass::Source::Position]
|
610
|
+
# @param end_pos [Sass::Source::Position]
|
611
|
+
def literal_node(value, source_range_or_start_pos, end_pos = source_position)
|
612
|
+
node(Sass::Script::Tree::Literal.new(value), source_range_or_start_pos, end_pos)
|
613
|
+
end
|
614
|
+
|
615
|
+
# @overload node(node, source_range)
|
616
|
+
# @param node [Sass::Script::Tree::Node]
|
617
|
+
# @param source_range [Sass::Source::Range]
|
618
|
+
# @overload node(node, start_pos, end_pos = source_position)
|
619
|
+
# @param node [Sass::Script::Tree::Node]
|
620
|
+
# @param start_pos [Sass::Source::Position]
|
621
|
+
# @param end_pos [Sass::Source::Position]
|
622
|
+
def node(node, source_range_or_start_pos, end_pos = source_position)
|
623
|
+
source_range =
|
624
|
+
if source_range_or_start_pos.is_a?(Sass::Source::Range)
|
625
|
+
source_range_or_start_pos
|
626
|
+
else
|
627
|
+
range(source_range_or_start_pos, end_pos)
|
628
|
+
end
|
629
|
+
|
630
|
+
node.line = source_range.start_pos.line
|
631
|
+
node.source_range = source_range
|
632
|
+
node.filename = @options[:filename]
|
633
|
+
node
|
634
|
+
end
|
635
|
+
end
|
636
|
+
end
|
637
|
+
end
|