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.
Files changed (268) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +11 -0
  3. data/CONTRIBUTING +3 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.md +221 -0
  6. data/Rakefile +370 -0
  7. data/VERSION +1 -0
  8. data/VERSION_NAME +1 -0
  9. data/bin/sass +13 -0
  10. data/bin/sass-convert +12 -0
  11. data/bin/scss +13 -0
  12. data/extra/update_watch.rb +13 -0
  13. data/init.rb +18 -0
  14. data/lib/sass/cache_stores/base.rb +88 -0
  15. data/lib/sass/cache_stores/chain.rb +34 -0
  16. data/lib/sass/cache_stores/filesystem.rb +60 -0
  17. data/lib/sass/cache_stores/memory.rb +47 -0
  18. data/lib/sass/cache_stores/null.rb +25 -0
  19. data/lib/sass/cache_stores.rb +15 -0
  20. data/lib/sass/callbacks.rb +67 -0
  21. data/lib/sass/css.rb +407 -0
  22. data/lib/sass/engine.rb +1181 -0
  23. data/lib/sass/environment.rb +191 -0
  24. data/lib/sass/error.rb +198 -0
  25. data/lib/sass/exec/base.rb +187 -0
  26. data/lib/sass/exec/sass_convert.rb +264 -0
  27. data/lib/sass/exec/sass_scss.rb +424 -0
  28. data/lib/sass/exec.rb +9 -0
  29. data/lib/sass/features.rb +47 -0
  30. data/lib/sass/importers/base.rb +182 -0
  31. data/lib/sass/importers/filesystem.rb +211 -0
  32. data/lib/sass/importers.rb +22 -0
  33. data/lib/sass/logger/base.rb +30 -0
  34. data/lib/sass/logger/log_level.rb +45 -0
  35. data/lib/sass/logger.rb +12 -0
  36. data/lib/sass/media.rb +210 -0
  37. data/lib/sass/plugin/compiler.rb +565 -0
  38. data/lib/sass/plugin/configuration.rb +118 -0
  39. data/lib/sass/plugin/generic.rb +15 -0
  40. data/lib/sass/plugin/merb.rb +48 -0
  41. data/lib/sass/plugin/rack.rb +60 -0
  42. data/lib/sass/plugin/rails.rb +47 -0
  43. data/lib/sass/plugin/staleness_checker.rb +199 -0
  44. data/lib/sass/plugin.rb +133 -0
  45. data/lib/sass/railtie.rb +10 -0
  46. data/lib/sass/repl.rb +57 -0
  47. data/lib/sass/root.rb +7 -0
  48. data/lib/sass/script/css_lexer.rb +33 -0
  49. data/lib/sass/script/css_parser.rb +34 -0
  50. data/lib/sass/script/functions.rb +2626 -0
  51. data/lib/sass/script/lexer.rb +449 -0
  52. data/lib/sass/script/parser.rb +637 -0
  53. data/lib/sass/script/tree/funcall.rb +306 -0
  54. data/lib/sass/script/tree/interpolation.rb +118 -0
  55. data/lib/sass/script/tree/list_literal.rb +77 -0
  56. data/lib/sass/script/tree/literal.rb +45 -0
  57. data/lib/sass/script/tree/map_literal.rb +64 -0
  58. data/lib/sass/script/tree/node.rb +109 -0
  59. data/lib/sass/script/tree/operation.rb +103 -0
  60. data/lib/sass/script/tree/selector.rb +26 -0
  61. data/lib/sass/script/tree/string_interpolation.rb +104 -0
  62. data/lib/sass/script/tree/unary_operation.rb +69 -0
  63. data/lib/sass/script/tree/variable.rb +57 -0
  64. data/lib/sass/script/tree.rb +16 -0
  65. data/lib/sass/script/value/arg_list.rb +36 -0
  66. data/lib/sass/script/value/base.rb +240 -0
  67. data/lib/sass/script/value/bool.rb +35 -0
  68. data/lib/sass/script/value/color.rb +680 -0
  69. data/lib/sass/script/value/helpers.rb +262 -0
  70. data/lib/sass/script/value/list.rb +113 -0
  71. data/lib/sass/script/value/map.rb +70 -0
  72. data/lib/sass/script/value/null.rb +44 -0
  73. data/lib/sass/script/value/number.rb +530 -0
  74. data/lib/sass/script/value/string.rb +97 -0
  75. data/lib/sass/script/value.rb +11 -0
  76. data/lib/sass/script.rb +66 -0
  77. data/lib/sass/scss/css_parser.rb +42 -0
  78. data/lib/sass/scss/parser.rb +1209 -0
  79. data/lib/sass/scss/rx.rb +141 -0
  80. data/lib/sass/scss/script_lexer.rb +15 -0
  81. data/lib/sass/scss/script_parser.rb +25 -0
  82. data/lib/sass/scss/static_parser.rb +368 -0
  83. data/lib/sass/scss.rb +16 -0
  84. data/lib/sass/selector/abstract_sequence.rb +109 -0
  85. data/lib/sass/selector/comma_sequence.rb +175 -0
  86. data/lib/sass/selector/pseudo.rb +256 -0
  87. data/lib/sass/selector/sequence.rb +600 -0
  88. data/lib/sass/selector/simple.rb +117 -0
  89. data/lib/sass/selector/simple_sequence.rb +325 -0
  90. data/lib/sass/selector.rb +326 -0
  91. data/lib/sass/shared.rb +76 -0
  92. data/lib/sass/source/map.rb +210 -0
  93. data/lib/sass/source/position.rb +39 -0
  94. data/lib/sass/source/range.rb +41 -0
  95. data/lib/sass/stack.rb +120 -0
  96. data/lib/sass/supports.rb +227 -0
  97. data/lib/sass/tree/at_root_node.rb +83 -0
  98. data/lib/sass/tree/charset_node.rb +22 -0
  99. data/lib/sass/tree/comment_node.rb +82 -0
  100. data/lib/sass/tree/content_node.rb +9 -0
  101. data/lib/sass/tree/css_import_node.rb +60 -0
  102. data/lib/sass/tree/debug_node.rb +18 -0
  103. data/lib/sass/tree/directive_node.rb +59 -0
  104. data/lib/sass/tree/each_node.rb +24 -0
  105. data/lib/sass/tree/error_node.rb +18 -0
  106. data/lib/sass/tree/extend_node.rb +43 -0
  107. data/lib/sass/tree/for_node.rb +36 -0
  108. data/lib/sass/tree/function_node.rb +39 -0
  109. data/lib/sass/tree/if_node.rb +52 -0
  110. data/lib/sass/tree/import_node.rb +74 -0
  111. data/lib/sass/tree/keyframe_rule_node.rb +15 -0
  112. data/lib/sass/tree/media_node.rb +48 -0
  113. data/lib/sass/tree/mixin_def_node.rb +38 -0
  114. data/lib/sass/tree/mixin_node.rb +52 -0
  115. data/lib/sass/tree/node.rb +238 -0
  116. data/lib/sass/tree/prop_node.rb +171 -0
  117. data/lib/sass/tree/return_node.rb +19 -0
  118. data/lib/sass/tree/root_node.rb +44 -0
  119. data/lib/sass/tree/rule_node.rb +145 -0
  120. data/lib/sass/tree/supports_node.rb +38 -0
  121. data/lib/sass/tree/trace_node.rb +33 -0
  122. data/lib/sass/tree/variable_node.rb +36 -0
  123. data/lib/sass/tree/visitors/base.rb +72 -0
  124. data/lib/sass/tree/visitors/check_nesting.rb +177 -0
  125. data/lib/sass/tree/visitors/convert.rb +334 -0
  126. data/lib/sass/tree/visitors/cssize.rb +369 -0
  127. data/lib/sass/tree/visitors/deep_copy.rb +107 -0
  128. data/lib/sass/tree/visitors/extend.rb +68 -0
  129. data/lib/sass/tree/visitors/perform.rb +539 -0
  130. data/lib/sass/tree/visitors/set_options.rb +139 -0
  131. data/lib/sass/tree/visitors/to_css.rb +381 -0
  132. data/lib/sass/tree/warn_node.rb +18 -0
  133. data/lib/sass/tree/while_node.rb +18 -0
  134. data/lib/sass/util/cross_platform_random.rb +19 -0
  135. data/lib/sass/util/multibyte_string_scanner.rb +157 -0
  136. data/lib/sass/util/normalized_map.rb +130 -0
  137. data/lib/sass/util/ordered_hash.rb +192 -0
  138. data/lib/sass/util/subset_map.rb +110 -0
  139. data/lib/sass/util/test.rb +9 -0
  140. data/lib/sass/util.rb +1318 -0
  141. data/lib/sass/version.rb +124 -0
  142. data/lib/sass.rb +102 -0
  143. data/rails/init.rb +1 -0
  144. data/test/sass/cache_test.rb +131 -0
  145. data/test/sass/callbacks_test.rb +61 -0
  146. data/test/sass/compiler_test.rb +232 -0
  147. data/test/sass/conversion_test.rb +2054 -0
  148. data/test/sass/css2sass_test.rb +477 -0
  149. data/test/sass/data/hsl-rgb.txt +319 -0
  150. data/test/sass/encoding_test.rb +219 -0
  151. data/test/sass/engine_test.rb +3301 -0
  152. data/test/sass/exec_test.rb +86 -0
  153. data/test/sass/extend_test.rb +1661 -0
  154. data/test/sass/fixtures/test_staleness_check_across_importers.css +1 -0
  155. data/test/sass/fixtures/test_staleness_check_across_importers.scss +1 -0
  156. data/test/sass/functions_test.rb +1926 -0
  157. data/test/sass/importer_test.rb +412 -0
  158. data/test/sass/logger_test.rb +58 -0
  159. data/test/sass/mock_importer.rb +49 -0
  160. data/test/sass/more_results/more1.css +9 -0
  161. data/test/sass/more_results/more1_with_line_comments.css +26 -0
  162. data/test/sass/more_results/more_import.css +29 -0
  163. data/test/sass/more_templates/_more_partial.sass +2 -0
  164. data/test/sass/more_templates/more1.sass +23 -0
  165. data/test/sass/more_templates/more_import.sass +11 -0
  166. data/test/sass/plugin_test.rb +554 -0
  167. data/test/sass/results/alt.css +4 -0
  168. data/test/sass/results/basic.css +9 -0
  169. data/test/sass/results/cached_import_option.css +3 -0
  170. data/test/sass/results/compact.css +5 -0
  171. data/test/sass/results/complex.css +86 -0
  172. data/test/sass/results/compressed.css +1 -0
  173. data/test/sass/results/expanded.css +19 -0
  174. data/test/sass/results/filename_fn.css +3 -0
  175. data/test/sass/results/if.css +3 -0
  176. data/test/sass/results/import.css +31 -0
  177. data/test/sass/results/import_charset.css +5 -0
  178. data/test/sass/results/import_charset_1_8.css +5 -0
  179. data/test/sass/results/import_charset_ibm866.css +5 -0
  180. data/test/sass/results/import_content.css +1 -0
  181. data/test/sass/results/line_numbers.css +49 -0
  182. data/test/sass/results/mixins.css +95 -0
  183. data/test/sass/results/multiline.css +24 -0
  184. data/test/sass/results/nested.css +22 -0
  185. data/test/sass/results/options.css +1 -0
  186. data/test/sass/results/parent_ref.css +13 -0
  187. data/test/sass/results/script.css +16 -0
  188. data/test/sass/results/scss_import.css +31 -0
  189. data/test/sass/results/scss_importee.css +2 -0
  190. data/test/sass/results/subdir/nested_subdir/nested_subdir.css +1 -0
  191. data/test/sass/results/subdir/subdir.css +3 -0
  192. data/test/sass/results/units.css +11 -0
  193. data/test/sass/results/warn.css +0 -0
  194. data/test/sass/results/warn_imported.css +0 -0
  195. data/test/sass/script_conversion_test.rb +328 -0
  196. data/test/sass/script_test.rb +1054 -0
  197. data/test/sass/scss/css_test.rb +1215 -0
  198. data/test/sass/scss/rx_test.rb +156 -0
  199. data/test/sass/scss/scss_test.rb +3900 -0
  200. data/test/sass/scss/test_helper.rb +37 -0
  201. data/test/sass/source_map_test.rb +977 -0
  202. data/test/sass/superselector_test.rb +191 -0
  203. data/test/sass/templates/_cached_import_option_partial.scss +1 -0
  204. data/test/sass/templates/_double_import_loop2.sass +1 -0
  205. data/test/sass/templates/_filename_fn_import.scss +11 -0
  206. data/test/sass/templates/_imported_charset_ibm866.sass +4 -0
  207. data/test/sass/templates/_imported_charset_utf8.sass +4 -0
  208. data/test/sass/templates/_imported_content.sass +3 -0
  209. data/test/sass/templates/_partial.sass +2 -0
  210. data/test/sass/templates/_same_name_different_partiality.scss +1 -0
  211. data/test/sass/templates/alt.sass +16 -0
  212. data/test/sass/templates/basic.sass +23 -0
  213. data/test/sass/templates/bork1.sass +2 -0
  214. data/test/sass/templates/bork2.sass +2 -0
  215. data/test/sass/templates/bork3.sass +2 -0
  216. data/test/sass/templates/bork4.sass +2 -0
  217. data/test/sass/templates/bork5.sass +3 -0
  218. data/test/sass/templates/cached_import_option.scss +3 -0
  219. data/test/sass/templates/compact.sass +17 -0
  220. data/test/sass/templates/complex.sass +305 -0
  221. data/test/sass/templates/compressed.sass +15 -0
  222. data/test/sass/templates/double_import_loop1.sass +1 -0
  223. data/test/sass/templates/expanded.sass +17 -0
  224. data/test/sass/templates/filename_fn.scss +18 -0
  225. data/test/sass/templates/if.sass +11 -0
  226. data/test/sass/templates/import.sass +12 -0
  227. data/test/sass/templates/import_charset.sass +9 -0
  228. data/test/sass/templates/import_charset_1_8.sass +6 -0
  229. data/test/sass/templates/import_charset_ibm866.sass +11 -0
  230. data/test/sass/templates/import_content.sass +4 -0
  231. data/test/sass/templates/importee.less +2 -0
  232. data/test/sass/templates/importee.sass +19 -0
  233. data/test/sass/templates/line_numbers.sass +13 -0
  234. data/test/sass/templates/mixin_bork.sass +5 -0
  235. data/test/sass/templates/mixins.sass +76 -0
  236. data/test/sass/templates/multiline.sass +20 -0
  237. data/test/sass/templates/nested.sass +25 -0
  238. data/test/sass/templates/nested_bork1.sass +2 -0
  239. data/test/sass/templates/nested_bork2.sass +2 -0
  240. data/test/sass/templates/nested_bork3.sass +2 -0
  241. data/test/sass/templates/nested_bork4.sass +2 -0
  242. data/test/sass/templates/nested_import.sass +2 -0
  243. data/test/sass/templates/nested_mixin_bork.sass +6 -0
  244. data/test/sass/templates/options.sass +2 -0
  245. data/test/sass/templates/parent_ref.sass +25 -0
  246. data/test/sass/templates/same_name_different_ext.sass +2 -0
  247. data/test/sass/templates/same_name_different_ext.scss +1 -0
  248. data/test/sass/templates/same_name_different_partiality.scss +1 -0
  249. data/test/sass/templates/script.sass +101 -0
  250. data/test/sass/templates/scss_import.scss +12 -0
  251. data/test/sass/templates/scss_importee.scss +1 -0
  252. data/test/sass/templates/single_import_loop.sass +1 -0
  253. data/test/sass/templates/subdir/import_up1.scss +1 -0
  254. data/test/sass/templates/subdir/import_up2.scss +1 -0
  255. data/test/sass/templates/subdir/nested_subdir/_nested_partial.sass +2 -0
  256. data/test/sass/templates/subdir/nested_subdir/nested_subdir.sass +3 -0
  257. data/test/sass/templates/subdir/subdir.sass +6 -0
  258. data/test/sass/templates/units.sass +11 -0
  259. data/test/sass/templates/warn.sass +3 -0
  260. data/test/sass/templates/warn_imported.sass +4 -0
  261. data/test/sass/test_helper.rb +8 -0
  262. data/test/sass/util/multibyte_string_scanner_test.rb +147 -0
  263. data/test/sass/util/normalized_map_test.rb +51 -0
  264. data/test/sass/util/subset_map_test.rb +91 -0
  265. data/test/sass/util_test.rb +467 -0
  266. data/test/sass/value_helpers_test.rb +179 -0
  267. data/test/test_helper.rb +109 -0
  268. 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