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,502 @@
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::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
+ # @param str [String, StringScanner] The source text to parse
16
+ # @param line [Fixnum] The line on which the SassScript appears.
17
+ # Used for error reporting
18
+ # @param offset [Fixnum] The number of characters in on which the SassScript appears.
19
+ # Used for error reporting
20
+ # @param options [{Symbol => Object}] An options hash;
21
+ # see {file:SASS_REFERENCE.md#sass_options the Sass options documentation}
22
+ def initialize(str, line, offset, options = {})
23
+ @options = options
24
+ @lexer = lexer_class.new(str, line, offset, options)
25
+ end
26
+
27
+ # Parses a SassScript expression within an interpolated segment (`#{}`).
28
+ # This means that it stops when it comes across an unmatched `}`,
29
+ # which signals the end of an interpolated segment,
30
+ # it returns rather than throwing an error.
31
+ #
32
+ # @return [Script::Node] The root node of the parse tree
33
+ # @raise [Sass::SyntaxError] if the expression isn't valid SassScript
34
+ def parse_interpolated
35
+ expr = assert_expr :expr
36
+ assert_tok :end_interpolation
37
+ expr.options = @options
38
+ expr
39
+ rescue Sass::SyntaxError => e
40
+ e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
41
+ raise e
42
+ end
43
+
44
+ # Parses a SassScript expression.
45
+ #
46
+ # @return [Script::Node] The root node of the parse tree
47
+ # @raise [Sass::SyntaxError] if the expression isn't valid SassScript
48
+ def parse
49
+ expr = assert_expr :expr
50
+ assert_done
51
+ expr.options = @options
52
+ expr
53
+ rescue Sass::SyntaxError => e
54
+ e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
55
+ raise e
56
+ end
57
+
58
+ # Parses a SassScript expression,
59
+ # ending it when it encounters one of the given identifier tokens.
60
+ #
61
+ # @param [#include?(String)] A set of strings that delimit the expression.
62
+ # @return [Script::Node] The root node of the parse tree
63
+ # @raise [Sass::SyntaxError] if the expression isn't valid SassScript
64
+ def parse_until(tokens)
65
+ @stop_at = tokens
66
+ expr = assert_expr :expr
67
+ assert_done
68
+ expr.options = @options
69
+ expr
70
+ rescue Sass::SyntaxError => e
71
+ e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
72
+ raise e
73
+ end
74
+
75
+ # Parses the argument list for a mixin include.
76
+ #
77
+ # @return [(Array<Script::Node>, {String => Script::Node}, Script::Node)]
78
+ # The root nodes of the positional arguments, keyword arguments, and
79
+ # splat argument. Keyword arguments are in a hash from names to values.
80
+ # @raise [Sass::SyntaxError] if the argument list isn't valid SassScript
81
+ def parse_mixin_include_arglist
82
+ args, keywords = [], {}
83
+ if try_tok(:lparen)
84
+ args, keywords, splat = mixin_arglist || [[], {}]
85
+ assert_tok(:rparen)
86
+ end
87
+ assert_done
88
+
89
+ args.each {|a| a.options = @options}
90
+ keywords.each {|k, v| v.options = @options}
91
+ splat.options = @options if splat
92
+ return args, keywords, splat
93
+ rescue Sass::SyntaxError => e
94
+ e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
95
+ raise e
96
+ end
97
+
98
+ # Parses the argument list for a mixin definition.
99
+ #
100
+ # @return [(Array<Script::Node>, Script::Node)]
101
+ # The root nodes of the arguments, and the splat argument.
102
+ # @raise [Sass::SyntaxError] if the argument list isn't valid SassScript
103
+ def parse_mixin_definition_arglist
104
+ args, splat = defn_arglist!(false)
105
+ assert_done
106
+
107
+ args.each do |k, v|
108
+ k.options = @options
109
+ v.options = @options if v
110
+ end
111
+ splat.options = @options if splat
112
+ return args, splat
113
+ rescue Sass::SyntaxError => e
114
+ e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
115
+ raise e
116
+ end
117
+
118
+ # Parses the argument list for a function definition.
119
+ #
120
+ # @return [(Array<Script::Node>, Script::Node)]
121
+ # The root nodes of the arguments, and the splat argument.
122
+ # @raise [Sass::SyntaxError] if the argument list isn't valid SassScript
123
+ def parse_function_definition_arglist
124
+ args, splat = defn_arglist!(true)
125
+ assert_done
126
+
127
+ args.each do |k, v|
128
+ k.options = @options
129
+ v.options = @options if v
130
+ end
131
+ splat.options = @options if splat
132
+ return args, splat
133
+ rescue Sass::SyntaxError => e
134
+ e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
135
+ raise e
136
+ end
137
+
138
+ # Parse a single string value, possibly containing interpolation.
139
+ # Doesn't assert that the scanner is finished after parsing.
140
+ #
141
+ # @return [Script::Node] The root node of the parse tree.
142
+ # @raise [Sass::SyntaxError] if the string isn't valid SassScript
143
+ def parse_string
144
+ unless (peek = @lexer.peek) &&
145
+ (peek.type == :string ||
146
+ (peek.type == :funcall && peek.value.downcase == 'url'))
147
+ lexer.expected!("string")
148
+ end
149
+
150
+ expr = assert_expr :funcall
151
+ expr.options = @options
152
+ @lexer.unpeek!
153
+ expr
154
+ rescue Sass::SyntaxError => e
155
+ e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
156
+ raise e
157
+ end
158
+
159
+ # Parses a SassScript expression.
160
+ #
161
+ # @overload parse(str, line, offset, filename = nil)
162
+ # @return [Script::Node] The root node of the parse tree
163
+ # @see Parser#initialize
164
+ # @see Parser#parse
165
+ def self.parse(*args)
166
+ new(*args).parse
167
+ end
168
+
169
+ PRECEDENCE = [
170
+ :comma, :single_eq, :space, :or, :and,
171
+ [:eq, :neq],
172
+ [:gt, :gte, :lt, :lte],
173
+ [:plus, :minus],
174
+ [:times, :div, :mod],
175
+ ]
176
+
177
+ ASSOCIATIVE = [:plus, :times]
178
+
179
+ class << self
180
+ # Returns an integer representing the precedence
181
+ # of the given operator.
182
+ # A lower integer indicates a looser binding.
183
+ #
184
+ # @private
185
+ def precedence_of(op)
186
+ PRECEDENCE.each_with_index do |e, i|
187
+ return i if Array(e).include?(op)
188
+ end
189
+ raise "[BUG] Unknown operator #{op}"
190
+ end
191
+
192
+ # Returns whether or not the given operation is associative.
193
+ #
194
+ # @private
195
+ def associative?(op)
196
+ ASSOCIATIVE.include?(op)
197
+ end
198
+
199
+ private
200
+
201
+ # Defines a simple left-associative production.
202
+ # name is the name of the production,
203
+ # sub is the name of the production beneath it,
204
+ # and ops is a list of operators for this precedence level
205
+ def production(name, sub, *ops)
206
+ class_eval <<RUBY, __FILE__, __LINE__ + 1
207
+ def #{name}
208
+ interp = try_ops_after_interp(#{ops.inspect}, #{name.inspect}) and return interp
209
+ return unless e = #{sub}
210
+ while tok = try_tok(#{ops.map {|o| o.inspect}.join(', ')})
211
+ if interp = try_op_before_interp(tok, e)
212
+ return interp unless other_interp = try_ops_after_interp(#{ops.inspect}, #{name.inspect}, interp)
213
+ return other_interp
214
+ end
215
+
216
+ line = @lexer.line
217
+ e = Operation.new(e, assert_expr(#{sub.inspect}), tok.type)
218
+ e.line = line
219
+ end
220
+ e
221
+ end
222
+ RUBY
223
+ end
224
+
225
+ def unary(op, sub)
226
+ class_eval <<RUBY, __FILE__, __LINE__ + 1
227
+ def unary_#{op}
228
+ return #{sub} unless tok = try_tok(:#{op})
229
+ interp = try_op_before_interp(tok) and return interp
230
+ line = @lexer.line
231
+ op = UnaryOperation.new(assert_expr(:unary_#{op}), :#{op})
232
+ op.line = line
233
+ op
234
+ end
235
+ RUBY
236
+ end
237
+ end
238
+
239
+ private
240
+
241
+ # @private
242
+ def lexer_class; Lexer; end
243
+
244
+ def expr
245
+ line = @lexer.line
246
+ return unless e = interpolation
247
+ list = node(List.new([e], :comma), line)
248
+ while tok = try_tok(:comma)
249
+ if interp = try_op_before_interp(tok, list)
250
+ return interp unless other_interp = try_ops_after_interp([:comma], :expr, interp)
251
+ return other_interp
252
+ end
253
+ list.value << assert_expr(:interpolation)
254
+ end
255
+ list.value.size == 1 ? list.value.first : list
256
+ end
257
+
258
+ production :equals, :interpolation, :single_eq
259
+
260
+ def try_op_before_interp(op, prev = nil)
261
+ return unless @lexer.peek && @lexer.peek.type == :begin_interpolation
262
+ wb = @lexer.whitespace?(op)
263
+ str = Script::String.new(Lexer::OPERATORS_REVERSE[op.type])
264
+ str.line = @lexer.line
265
+ interp = Script::Interpolation.new(prev, str, nil, wb, !:wa, :originally_text)
266
+ interp.line = @lexer.line
267
+ interpolation(interp)
268
+ end
269
+
270
+ def try_ops_after_interp(ops, name, prev = nil)
271
+ return unless @lexer.after_interpolation?
272
+ return unless op = try_tok(*ops)
273
+ interp = try_op_before_interp(op, prev) and return interp
274
+
275
+ wa = @lexer.whitespace?
276
+ str = Script::String.new(Lexer::OPERATORS_REVERSE[op.type])
277
+ str.line = @lexer.line
278
+ interp = Script::Interpolation.new(prev, str, assert_expr(name), !:wb, wa, :originally_text)
279
+ interp.line = @lexer.line
280
+ return interp
281
+ end
282
+
283
+ def interpolation(first = space)
284
+ e = first
285
+ while interp = try_tok(:begin_interpolation)
286
+ wb = @lexer.whitespace?(interp)
287
+ line = @lexer.line
288
+ mid = parse_interpolated
289
+ wa = @lexer.whitespace?
290
+ e = Script::Interpolation.new(e, mid, space, wb, wa)
291
+ e.line = line
292
+ end
293
+ e
294
+ end
295
+
296
+ def space
297
+ line = @lexer.line
298
+ return unless e = or_expr
299
+ arr = [e]
300
+ while e = or_expr
301
+ arr << e
302
+ end
303
+ arr.size == 1 ? arr.first : node(List.new(arr, :space), line)
304
+ end
305
+
306
+ production :or_expr, :and_expr, :or
307
+ production :and_expr, :eq_or_neq, :and
308
+ production :eq_or_neq, :relational, :eq, :neq
309
+ production :relational, :plus_or_minus, :gt, :gte, :lt, :lte
310
+ production :plus_or_minus, :times_div_or_mod, :plus, :minus
311
+ production :times_div_or_mod, :unary_plus, :times, :div, :mod
312
+
313
+ unary :plus, :unary_minus
314
+ unary :minus, :unary_div
315
+ unary :div, :unary_not # For strings, so /foo/bar works
316
+ unary :not, :ident
317
+
318
+ def ident
319
+ return funcall unless @lexer.peek && @lexer.peek.type == :ident
320
+ return if @stop_at && @stop_at.include?(@lexer.peek.value)
321
+
322
+ name = @lexer.next
323
+ if color = Color::COLOR_NAMES[name.value.downcase]
324
+ node(Color.new(color))
325
+ elsif name.value == "true"
326
+ node(Script::Bool.new(true))
327
+ elsif name.value == "false"
328
+ node(Script::Bool.new(false))
329
+ elsif name.value == "null"
330
+ node(Script::Null.new)
331
+ else
332
+ node(Script::String.new(name.value, :identifier))
333
+ end
334
+ end
335
+
336
+ def funcall
337
+ return raw unless tok = try_tok(:funcall)
338
+ args, keywords, splat = fn_arglist || [[], {}]
339
+ assert_tok(:rparen)
340
+ node(Script::Funcall.new(tok.value, args, keywords, splat))
341
+ end
342
+
343
+ def defn_arglist!(must_have_parens)
344
+ if must_have_parens
345
+ assert_tok(:lparen)
346
+ else
347
+ return [], nil unless try_tok(:lparen)
348
+ end
349
+ return [], nil if try_tok(:rparen)
350
+
351
+ res = []
352
+ splat = nil
353
+ must_have_default = false
354
+ loop do
355
+ c = assert_tok(:const)
356
+ var = Script::Variable.new(c.value)
357
+ if try_tok(:colon)
358
+ val = assert_expr(:space)
359
+ must_have_default = true
360
+ elsif must_have_default
361
+ raise SyntaxError.new("Required argument #{var.inspect} must come before any optional arguments.")
362
+ elsif try_tok(:splat)
363
+ splat = var
364
+ break
365
+ end
366
+ res << [var, val]
367
+ break unless try_tok(:comma)
368
+ end
369
+ assert_tok(:rparen)
370
+ return res, splat
371
+ end
372
+
373
+ def fn_arglist
374
+ arglist(:equals, "function argument")
375
+ end
376
+
377
+ def mixin_arglist
378
+ arglist(:interpolation, "mixin argument")
379
+ end
380
+
381
+ def arglist(subexpr, description)
382
+ return unless e = send(subexpr)
383
+
384
+ args = []
385
+ keywords = {}
386
+ loop do
387
+ if @lexer.peek && @lexer.peek.type == :colon
388
+ name = e
389
+ @lexer.expected!("comma") unless name.is_a?(Variable)
390
+ assert_tok(:colon)
391
+ value = assert_expr(subexpr, description)
392
+
393
+ if keywords[name.underscored_name]
394
+ raise SyntaxError.new("Keyword argument \"#{name.to_sass}\" passed more than once")
395
+ end
396
+
397
+ keywords[name.underscored_name] = value
398
+ else
399
+ if !keywords.empty?
400
+ raise SyntaxError.new("Positional arguments must come before keyword arguments.")
401
+ end
402
+
403
+ return args, keywords, e if try_tok(:splat)
404
+ args << e
405
+ end
406
+
407
+ return args, keywords unless try_tok(:comma)
408
+ e = assert_expr(subexpr, description)
409
+ end
410
+ end
411
+
412
+ def raw
413
+ return special_fun unless tok = try_tok(:raw)
414
+ node(Script::String.new(tok.value))
415
+ end
416
+
417
+ def special_fun
418
+ return paren unless tok = try_tok(:special_fun)
419
+ first = node(Script::String.new(tok.value.first))
420
+ Sass::Util.enum_slice(tok.value[1..-1], 2).inject(first) do |l, (i, r)|
421
+ Script::Interpolation.new(
422
+ l, i, r && node(Script::String.new(r)),
423
+ false, false)
424
+ end
425
+ end
426
+
427
+ def paren
428
+ return variable unless try_tok(:lparen)
429
+ was_in_parens = @in_parens
430
+ @in_parens = true
431
+ line = @lexer.line
432
+ e = expr
433
+ assert_tok(:rparen)
434
+ return e || node(List.new([], :space), line)
435
+ ensure
436
+ @in_parens = was_in_parens
437
+ end
438
+
439
+ def variable
440
+ return string unless c = try_tok(:const)
441
+ node(Variable.new(*c.value))
442
+ end
443
+
444
+ def string
445
+ return number unless first = try_tok(:string)
446
+ return first.value unless try_tok(:begin_interpolation)
447
+ line = @lexer.line
448
+ mid = parse_interpolated
449
+ last = assert_expr(:string)
450
+ interp = StringInterpolation.new(first.value, mid, last)
451
+ interp.line = line
452
+ interp
453
+ end
454
+
455
+ def number
456
+ return literal unless tok = try_tok(:number)
457
+ num = tok.value
458
+ num.original = num.to_s unless @in_parens
459
+ num
460
+ end
461
+
462
+ def literal
463
+ (t = try_tok(:color)) && (return t.value)
464
+ end
465
+
466
+ # It would be possible to have unified #assert and #try methods,
467
+ # but detecting the method/token difference turns out to be quite expensive.
468
+
469
+ EXPR_NAMES = {
470
+ :string => "string",
471
+ :default => "expression (e.g. 1px, bold)",
472
+ :mixin_arglist => "mixin argument",
473
+ :fn_arglist => "function argument",
474
+ }
475
+
476
+ def assert_expr(name, expected = nil)
477
+ (e = send(name)) && (return e)
478
+ @lexer.expected!(expected || EXPR_NAMES[name] || EXPR_NAMES[:default])
479
+ end
480
+
481
+ def assert_tok(*names)
482
+ (t = try_tok(*names)) && (return t)
483
+ @lexer.expected!(names.map {|tok| Lexer::TOKEN_NAMES[tok] || tok}.join(" or "))
484
+ end
485
+
486
+ def try_tok(*names)
487
+ peeked = @lexer.peek
488
+ peeked && names.include?(peeked.type) && @lexer.next
489
+ end
490
+
491
+ def assert_done
492
+ return if @lexer.done?
493
+ @lexer.expected!(EXPR_NAMES[:default])
494
+ end
495
+
496
+ def node(node, line = @lexer.line)
497
+ node.line = line
498
+ node
499
+ end
500
+ end
501
+ end
502
+ end
@@ -0,0 +1,51 @@
1
+ require 'sass/script/literal'
2
+
3
+ module Sass::Script
4
+ # A SassScript object representing a CSS string *or* a CSS identifier.
5
+ class String < Literal
6
+ # The Ruby value of the string.
7
+ #
8
+ # @return [String]
9
+ attr_reader :value
10
+
11
+ # Whether this is a CSS string or a CSS identifier.
12
+ # The difference is that strings are written with double-quotes,
13
+ # while identifiers aren't.
14
+ #
15
+ # @return [Symbol] `:string` or `:identifier`
16
+ attr_reader :type
17
+
18
+ # Creates a new string.
19
+ #
20
+ # @param value [String] See \{#value}
21
+ # @param type [Symbol] See \{#type}
22
+ def initialize(value, type = :identifier)
23
+ super(value)
24
+ @type = type
25
+ end
26
+
27
+ # @see Literal#plus
28
+ def plus(other)
29
+ other_str = other.is_a?(Sass::Script::String) ? other.value : other.to_s
30
+ Sass::Script::String.new(self.value + other_str, self.type)
31
+ end
32
+
33
+ # @see Node#to_s
34
+ def to_s(opts = {})
35
+ if @type == :identifier
36
+ return @value.gsub(/\n\s*/, " ")
37
+ end
38
+
39
+ return "\"#{value.gsub('"', "\\\"")}\"" if opts[:quote] == %q{"}
40
+ return "'#{value.gsub("'", "\\'")}'" if opts[:quote] == %q{'}
41
+ return "\"#{value}\"" unless value.include?('"')
42
+ return "'#{value}'" unless value.include?("'")
43
+ "\"#{value.gsub('"', "\\\"")}\"" #'
44
+ end
45
+
46
+ # @see Node#to_sass
47
+ def to_sass(opts = {})
48
+ to_s
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,103 @@
1
+ module Sass::Script
2
+ # A SassScript object representing `#{}` interpolation within a string.
3
+ #
4
+ # @see Interpolation
5
+ class StringInterpolation < Node
6
+ # Interpolation in a string is of the form `"before #{mid} after"`,
7
+ # where `before` and `after` may include more interpolation.
8
+ #
9
+ # @param before [Node] The string before the interpolation
10
+ # @param mid [Node] The SassScript within the interpolation
11
+ # @param after [Node] The string after the interpolation
12
+ def initialize(before, mid, after)
13
+ @before = before
14
+ @mid = mid
15
+ @after = after
16
+ end
17
+
18
+ # @return [String] A human-readable s-expression representation of the interpolation
19
+ def inspect
20
+ "(string_interpolation #{@before.inspect} #{@mid.inspect} #{@after.inspect})"
21
+ end
22
+
23
+ # @see Node#to_sass
24
+ def to_sass(opts = {})
25
+ # We can get rid of all of this when we remove the deprecated :equals context
26
+ # XXX CE: It's gone now but I'm not sure what can be removed now.
27
+ before_unquote, before_quote_char, before_str = parse_str(@before.to_sass(opts))
28
+ after_unquote, after_quote_char, after_str = parse_str(@after.to_sass(opts))
29
+ unquote = before_unquote || after_unquote ||
30
+ (before_quote_char && !after_quote_char && !after_str.empty?) ||
31
+ (!before_quote_char && after_quote_char && !before_str.empty?)
32
+ quote_char =
33
+ if before_quote_char && after_quote_char && before_quote_char != after_quote_char
34
+ before_str.gsub!("\\'", "'")
35
+ before_str.gsub!('"', "\\\"")
36
+ after_str.gsub!("\\'", "'")
37
+ after_str.gsub!('"', "\\\"")
38
+ '"'
39
+ else
40
+ before_quote_char || after_quote_char
41
+ end
42
+
43
+ res = ""
44
+ res << 'unquote(' if unquote
45
+ res << quote_char if quote_char
46
+ res << before_str
47
+ res << '#{' << @mid.to_sass(opts) << '}'
48
+ res << after_str
49
+ res << quote_char if quote_char
50
+ res << ')' if unquote
51
+ res
52
+ end
53
+
54
+ # Returns the three components of the interpolation, `before`, `mid`, and `after`.
55
+ #
56
+ # @return [Array<Node>]
57
+ # @see #initialize
58
+ # @see Node#children
59
+ def children
60
+ [@before, @mid, @after].compact
61
+ end
62
+
63
+ # @see Node#deep_copy
64
+ def deep_copy
65
+ node = dup
66
+ node.instance_variable_set('@before', @before.deep_copy) if @before
67
+ node.instance_variable_set('@mid', @mid.deep_copy)
68
+ node.instance_variable_set('@after', @after.deep_copy) if @after
69
+ node
70
+ end
71
+
72
+ protected
73
+
74
+ # Evaluates the interpolation.
75
+ #
76
+ # @param environment [Sass::Environment] The environment in which to evaluate the SassScript
77
+ # @return [Sass::Script::String] The SassScript string that is the value of the interpolation
78
+ def _perform(environment)
79
+ res = ""
80
+ before = @before.perform(environment)
81
+ res << before.value
82
+ mid = @mid.perform(environment)
83
+ res << (mid.is_a?(Sass::Script::String) ? mid.value : mid.to_s)
84
+ res << @after.perform(environment).value
85
+ opts(Sass::Script::String.new(res, before.type))
86
+ end
87
+
88
+ private
89
+
90
+ def parse_str(str)
91
+ case str
92
+ when /^unquote\((["'])(.*)\1\)$/
93
+ return true, $1, $2
94
+ when '""'
95
+ return false, nil, ""
96
+ when /^(["'])(.*)\1$/
97
+ return false, $1, $2
98
+ else
99
+ return false, nil, str
100
+ end
101
+ end
102
+ end
103
+ end