xass 0.1.0

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