aliddle-sass 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 (238) hide show
  1. data/.yardopts +11 -0
  2. data/CONTRIBUTING +3 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +201 -0
  5. data/Rakefile +347 -0
  6. data/VERSION +1 -0
  7. data/VERSION_NAME +1 -0
  8. data/bin/sass +9 -0
  9. data/bin/sass-convert +8 -0
  10. data/bin/scss +9 -0
  11. data/extra/update_watch.rb +13 -0
  12. data/init.rb +18 -0
  13. data/lib/sass.rb +95 -0
  14. data/lib/sass/cache_stores.rb +15 -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 +60 -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/callbacks.rb +66 -0
  21. data/lib/sass/css.rb +409 -0
  22. data/lib/sass/engine.rb +928 -0
  23. data/lib/sass/environment.rb +101 -0
  24. data/lib/sass/error.rb +201 -0
  25. data/lib/sass/exec.rb +707 -0
  26. data/lib/sass/importers.rb +22 -0
  27. data/lib/sass/importers/base.rb +139 -0
  28. data/lib/sass/importers/filesystem.rb +190 -0
  29. data/lib/sass/logger.rb +15 -0
  30. data/lib/sass/logger/base.rb +32 -0
  31. data/lib/sass/logger/log_level.rb +49 -0
  32. data/lib/sass/media.rb +213 -0
  33. data/lib/sass/plugin.rb +132 -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 +183 -0
  41. data/lib/sass/railtie.rb +9 -0
  42. data/lib/sass/repl.rb +57 -0
  43. data/lib/sass/root.rb +7 -0
  44. data/lib/sass/script.rb +39 -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 +237 -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 +348 -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 +495 -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/scss.rb +16 -0
  66. data/lib/sass/scss/css_parser.rb +36 -0
  67. data/lib/sass/scss/parser.rb +1179 -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/selector.rb +452 -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 +212 -0
  78. data/lib/sass/shared.rb +76 -0
  79. data/lib/sass/supports.rb +229 -0
  80. data/lib/sass/tree/charset_node.rb +22 -0
  81. data/lib/sass/tree/comment_node.rb +82 -0
  82. data/lib/sass/tree/content_node.rb +9 -0
  83. data/lib/sass/tree/css_import_node.rb +60 -0
  84. data/lib/sass/tree/debug_node.rb +18 -0
  85. data/lib/sass/tree/directive_node.rb +42 -0
  86. data/lib/sass/tree/each_node.rb +24 -0
  87. data/lib/sass/tree/extend_node.rb +36 -0
  88. data/lib/sass/tree/for_node.rb +36 -0
  89. data/lib/sass/tree/function_node.rb +34 -0
  90. data/lib/sass/tree/if_node.rb +52 -0
  91. data/lib/sass/tree/import_node.rb +75 -0
  92. data/lib/sass/tree/media_node.rb +58 -0
  93. data/lib/sass/tree/mixin_def_node.rb +38 -0
  94. data/lib/sass/tree/mixin_node.rb +39 -0
  95. data/lib/sass/tree/node.rb +196 -0
  96. data/lib/sass/tree/prop_node.rb +152 -0
  97. data/lib/sass/tree/return_node.rb +18 -0
  98. data/lib/sass/tree/root_node.rb +28 -0
  99. data/lib/sass/tree/rule_node.rb +132 -0
  100. data/lib/sass/tree/supports_node.rb +51 -0
  101. data/lib/sass/tree/trace_node.rb +32 -0
  102. data/lib/sass/tree/variable_node.rb +30 -0
  103. data/lib/sass/tree/visitors/base.rb +75 -0
  104. data/lib/sass/tree/visitors/check_nesting.rb +147 -0
  105. data/lib/sass/tree/visitors/convert.rb +316 -0
  106. data/lib/sass/tree/visitors/cssize.rb +229 -0
  107. data/lib/sass/tree/visitors/deep_copy.rb +102 -0
  108. data/lib/sass/tree/visitors/extend.rb +68 -0
  109. data/lib/sass/tree/visitors/perform.rb +446 -0
  110. data/lib/sass/tree/visitors/set_options.rb +125 -0
  111. data/lib/sass/tree/visitors/to_css.rb +230 -0
  112. data/lib/sass/tree/warn_node.rb +18 -0
  113. data/lib/sass/tree/while_node.rb +18 -0
  114. data/lib/sass/util.rb +906 -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/version.rb +126 -0
  119. data/rails/init.rb +1 -0
  120. data/test/Gemfile +3 -0
  121. data/test/Gemfile.lock +10 -0
  122. data/test/sass/cache_test.rb +89 -0
  123. data/test/sass/callbacks_test.rb +61 -0
  124. data/test/sass/conversion_test.rb +1760 -0
  125. data/test/sass/css2sass_test.rb +439 -0
  126. data/test/sass/data/hsl-rgb.txt +319 -0
  127. data/test/sass/engine_test.rb +3243 -0
  128. data/test/sass/exec_test.rb +86 -0
  129. data/test/sass/extend_test.rb +1461 -0
  130. data/test/sass/fixtures/test_staleness_check_across_importers.css +1 -0
  131. data/test/sass/fixtures/test_staleness_check_across_importers.scss +1 -0
  132. data/test/sass/functions_test.rb +1139 -0
  133. data/test/sass/importer_test.rb +192 -0
  134. data/test/sass/logger_test.rb +58 -0
  135. data/test/sass/mock_importer.rb +49 -0
  136. data/test/sass/more_results/more1.css +9 -0
  137. data/test/sass/more_results/more1_with_line_comments.css +26 -0
  138. data/test/sass/more_results/more_import.css +29 -0
  139. data/test/sass/more_templates/_more_partial.sass +2 -0
  140. data/test/sass/more_templates/more1.sass +23 -0
  141. data/test/sass/more_templates/more_import.sass +11 -0
  142. data/test/sass/plugin_test.rb +550 -0
  143. data/test/sass/results/alt.css +4 -0
  144. data/test/sass/results/basic.css +9 -0
  145. data/test/sass/results/cached_import_option.css +3 -0
  146. data/test/sass/results/compact.css +5 -0
  147. data/test/sass/results/complex.css +86 -0
  148. data/test/sass/results/compressed.css +1 -0
  149. data/test/sass/results/expanded.css +19 -0
  150. data/test/sass/results/filename_fn.css +3 -0
  151. data/test/sass/results/if.css +3 -0
  152. data/test/sass/results/import.css +31 -0
  153. data/test/sass/results/import_charset.css +5 -0
  154. data/test/sass/results/import_charset_1_8.css +5 -0
  155. data/test/sass/results/import_charset_ibm866.css +5 -0
  156. data/test/sass/results/import_content.css +1 -0
  157. data/test/sass/results/line_numbers.css +49 -0
  158. data/test/sass/results/mixins.css +95 -0
  159. data/test/sass/results/multiline.css +24 -0
  160. data/test/sass/results/nested.css +22 -0
  161. data/test/sass/results/options.css +1 -0
  162. data/test/sass/results/parent_ref.css +13 -0
  163. data/test/sass/results/script.css +16 -0
  164. data/test/sass/results/scss_import.css +31 -0
  165. data/test/sass/results/scss_importee.css +2 -0
  166. data/test/sass/results/subdir/nested_subdir/nested_subdir.css +1 -0
  167. data/test/sass/results/subdir/subdir.css +3 -0
  168. data/test/sass/results/units.css +11 -0
  169. data/test/sass/results/warn.css +0 -0
  170. data/test/sass/results/warn_imported.css +0 -0
  171. data/test/sass/script_conversion_test.rb +299 -0
  172. data/test/sass/script_test.rb +591 -0
  173. data/test/sass/scss/css_test.rb +1093 -0
  174. data/test/sass/scss/rx_test.rb +156 -0
  175. data/test/sass/scss/scss_test.rb +2043 -0
  176. data/test/sass/scss/test_helper.rb +37 -0
  177. data/test/sass/templates/_cached_import_option_partial.scss +1 -0
  178. data/test/sass/templates/_double_import_loop2.sass +1 -0
  179. data/test/sass/templates/_filename_fn_import.scss +11 -0
  180. data/test/sass/templates/_imported_charset_ibm866.sass +4 -0
  181. data/test/sass/templates/_imported_charset_utf8.sass +4 -0
  182. data/test/sass/templates/_imported_content.sass +3 -0
  183. data/test/sass/templates/_partial.sass +2 -0
  184. data/test/sass/templates/_same_name_different_partiality.scss +1 -0
  185. data/test/sass/templates/alt.sass +16 -0
  186. data/test/sass/templates/basic.sass +23 -0
  187. data/test/sass/templates/bork1.sass +2 -0
  188. data/test/sass/templates/bork2.sass +2 -0
  189. data/test/sass/templates/bork3.sass +2 -0
  190. data/test/sass/templates/bork4.sass +2 -0
  191. data/test/sass/templates/bork5.sass +3 -0
  192. data/test/sass/templates/cached_import_option.scss +3 -0
  193. data/test/sass/templates/compact.sass +17 -0
  194. data/test/sass/templates/complex.sass +305 -0
  195. data/test/sass/templates/compressed.sass +15 -0
  196. data/test/sass/templates/double_import_loop1.sass +1 -0
  197. data/test/sass/templates/expanded.sass +17 -0
  198. data/test/sass/templates/filename_fn.scss +18 -0
  199. data/test/sass/templates/if.sass +11 -0
  200. data/test/sass/templates/import.sass +12 -0
  201. data/test/sass/templates/import_charset.sass +9 -0
  202. data/test/sass/templates/import_charset_1_8.sass +6 -0
  203. data/test/sass/templates/import_charset_ibm866.sass +11 -0
  204. data/test/sass/templates/import_content.sass +4 -0
  205. data/test/sass/templates/importee.less +2 -0
  206. data/test/sass/templates/importee.sass +19 -0
  207. data/test/sass/templates/line_numbers.sass +13 -0
  208. data/test/sass/templates/mixin_bork.sass +5 -0
  209. data/test/sass/templates/mixins.sass +76 -0
  210. data/test/sass/templates/multiline.sass +20 -0
  211. data/test/sass/templates/nested.sass +25 -0
  212. data/test/sass/templates/nested_bork1.sass +2 -0
  213. data/test/sass/templates/nested_bork2.sass +2 -0
  214. data/test/sass/templates/nested_bork3.sass +2 -0
  215. data/test/sass/templates/nested_bork4.sass +2 -0
  216. data/test/sass/templates/nested_import.sass +2 -0
  217. data/test/sass/templates/nested_mixin_bork.sass +6 -0
  218. data/test/sass/templates/options.sass +2 -0
  219. data/test/sass/templates/parent_ref.sass +25 -0
  220. data/test/sass/templates/same_name_different_ext.sass +2 -0
  221. data/test/sass/templates/same_name_different_ext.scss +1 -0
  222. data/test/sass/templates/same_name_different_partiality.scss +1 -0
  223. data/test/sass/templates/script.sass +101 -0
  224. data/test/sass/templates/scss_import.scss +11 -0
  225. data/test/sass/templates/scss_importee.scss +1 -0
  226. data/test/sass/templates/single_import_loop.sass +1 -0
  227. data/test/sass/templates/subdir/nested_subdir/_nested_partial.sass +2 -0
  228. data/test/sass/templates/subdir/nested_subdir/nested_subdir.sass +3 -0
  229. data/test/sass/templates/subdir/subdir.sass +6 -0
  230. data/test/sass/templates/units.sass +11 -0
  231. data/test/sass/templates/warn.sass +3 -0
  232. data/test/sass/templates/warn_imported.sass +4 -0
  233. data/test/sass/test_helper.rb +8 -0
  234. data/test/sass/util/multibyte_string_scanner_test.rb +147 -0
  235. data/test/sass/util/subset_map_test.rb +91 -0
  236. data/test/sass/util_test.rb +313 -0
  237. data/test/test_helper.rb +80 -0
  238. metadata +348 -0
@@ -0,0 +1,110 @@
1
+ require 'set'
2
+ require 'sass/script/string'
3
+ require 'sass/script/number'
4
+ require 'sass/script/color'
5
+ require 'sass/script/functions'
6
+ require 'sass/script/unary_operation'
7
+ require 'sass/script/interpolation'
8
+ require 'sass/script/string_interpolation'
9
+
10
+ module Sass::Script
11
+ # A SassScript parse node representing a binary operation,
12
+ # such as `$a + $b` or `"foo" + 1`.
13
+ class Operation < Node
14
+ attr_reader :operand1
15
+ attr_reader :operand2
16
+ attr_reader :operator
17
+
18
+ # @param operand1 [Script::Node] The parse-tree node
19
+ # for the right-hand side of the operator
20
+ # @param operand2 [Script::Node] The parse-tree node
21
+ # for the left-hand side of the operator
22
+ # @param operator [Symbol] The operator to perform.
23
+ # This should be one of the binary operator names in {Lexer::OPERATORS}
24
+ def initialize(operand1, operand2, operator)
25
+ @operand1 = operand1
26
+ @operand2 = operand2
27
+ @operator = operator
28
+ super()
29
+ end
30
+
31
+ # @return [String] A human-readable s-expression representation of the operation
32
+ def inspect
33
+ "(#{@operator.inspect} #{@operand1.inspect} #{@operand2.inspect})"
34
+ end
35
+
36
+ # @see Node#to_sass
37
+ def to_sass(opts = {})
38
+ o1 = operand_to_sass @operand1, :left, opts
39
+ o2 = operand_to_sass @operand2, :right, opts
40
+ sep =
41
+ case @operator
42
+ when :comma; ", "
43
+ when :space; " "
44
+ else; " #{Lexer::OPERATORS_REVERSE[@operator]} "
45
+ end
46
+ "#{o1}#{sep}#{o2}"
47
+ end
48
+
49
+ # Returns the operands for this operation.
50
+ #
51
+ # @return [Array<Node>]
52
+ # @see Node#children
53
+ def children
54
+ [@operand1, @operand2]
55
+ end
56
+
57
+ # @see Node#deep_copy
58
+ def deep_copy
59
+ node = dup
60
+ node.instance_variable_set('@operand1', @operand1.deep_copy)
61
+ node.instance_variable_set('@operand2', @operand2.deep_copy)
62
+ node
63
+ end
64
+
65
+ protected
66
+
67
+ # Evaluates the operation.
68
+ #
69
+ # @param environment [Sass::Environment] The environment in which to evaluate the SassScript
70
+ # @return [Literal] The SassScript object that is the value of the operation
71
+ # @raise [Sass::SyntaxError] if the operation is undefined for the operands
72
+ def _perform(environment)
73
+ literal1 = @operand1.perform(environment)
74
+
75
+ # Special-case :and and :or to support short-circuiting.
76
+ if @operator == :and
77
+ return literal1.to_bool ? @operand2.perform(environment) : literal1
78
+ elsif @operator == :or
79
+ return literal1.to_bool ? literal1 : @operand2.perform(environment)
80
+ end
81
+
82
+ literal2 = @operand2.perform(environment)
83
+
84
+ if (literal1.is_a?(Null) || literal2.is_a?(Null)) && @operator != :eq && @operator != :neq
85
+ raise Sass::SyntaxError.new("Invalid null operation: \"#{literal1.inspect} #{@operator} #{literal2.inspect}\".")
86
+ end
87
+
88
+ begin
89
+ opts(literal1.send(@operator, literal2))
90
+ rescue NoMethodError => e
91
+ raise e unless e.name.to_s == @operator.to_s
92
+ raise Sass::SyntaxError.new("Undefined operation: \"#{literal1} #{@operator} #{literal2}\".")
93
+ end
94
+ end
95
+
96
+ private
97
+
98
+ def operand_to_sass(op, side, opts)
99
+ return "(#{op.to_sass(opts)})" if op.is_a?(List)
100
+ return op.to_sass(opts) unless op.is_a?(Operation)
101
+
102
+ pred = Sass::Script::Parser.precedence_of(@operator)
103
+ sub_pred = Sass::Script::Parser.precedence_of(op.operator)
104
+ assoc = Sass::Script::Parser.associative?(@operator)
105
+ return "(#{op.to_sass(opts)})" if sub_pred < pred ||
106
+ (side == :right && sub_pred == pred && !assoc)
107
+ op.to_sass(opts)
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,495 @@
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
+ return node(Color.new(color))
325
+ end
326
+ node(Script::String.new(name.value, :identifier))
327
+ end
328
+
329
+ def funcall
330
+ return raw unless tok = try_tok(:funcall)
331
+ args, keywords, splat = fn_arglist || [[], {}]
332
+ assert_tok(:rparen)
333
+ node(Script::Funcall.new(tok.value, args, keywords, splat))
334
+ end
335
+
336
+ def defn_arglist!(must_have_parens)
337
+ if must_have_parens
338
+ assert_tok(:lparen)
339
+ else
340
+ return [], nil unless try_tok(:lparen)
341
+ end
342
+ return [], nil if try_tok(:rparen)
343
+
344
+ res = []
345
+ splat = nil
346
+ must_have_default = false
347
+ loop do
348
+ c = assert_tok(:const)
349
+ var = Script::Variable.new(c.value)
350
+ if try_tok(:colon)
351
+ val = assert_expr(:space)
352
+ must_have_default = true
353
+ elsif must_have_default
354
+ raise SyntaxError.new("Required argument #{var.inspect} must come before any optional arguments.")
355
+ elsif try_tok(:splat)
356
+ splat = var
357
+ break
358
+ end
359
+ res << [var, val]
360
+ break unless try_tok(:comma)
361
+ end
362
+ assert_tok(:rparen)
363
+ return res, splat
364
+ end
365
+
366
+ def fn_arglist
367
+ arglist(:equals, "function argument")
368
+ end
369
+
370
+ def mixin_arglist
371
+ arglist(:interpolation, "mixin argument")
372
+ end
373
+
374
+ def arglist(subexpr, description)
375
+ return unless e = send(subexpr)
376
+
377
+ args = []
378
+ keywords = {}
379
+ loop do
380
+ if @lexer.peek && @lexer.peek.type == :colon
381
+ name = e
382
+ @lexer.expected!("comma") unless name.is_a?(Variable)
383
+ assert_tok(:colon)
384
+ value = assert_expr(subexpr, description)
385
+
386
+ if keywords[name.underscored_name]
387
+ raise SyntaxError.new("Keyword argument \"#{name.to_sass}\" passed more than once")
388
+ end
389
+
390
+ keywords[name.underscored_name] = value
391
+ else
392
+ if !keywords.empty?
393
+ raise SyntaxError.new("Positional arguments must come before keyword arguments.")
394
+ end
395
+
396
+ return args, keywords, e if try_tok(:splat)
397
+ args << e
398
+ end
399
+
400
+ return args, keywords unless try_tok(:comma)
401
+ e = assert_expr(subexpr, description)
402
+ end
403
+ end
404
+
405
+ def raw
406
+ return special_fun unless tok = try_tok(:raw)
407
+ node(Script::String.new(tok.value))
408
+ end
409
+
410
+ def special_fun
411
+ return paren unless tok = try_tok(:special_fun)
412
+ first = node(Script::String.new(tok.value.first))
413
+ Sass::Util.enum_slice(tok.value[1..-1], 2).inject(first) do |l, (i, r)|
414
+ Script::Interpolation.new(
415
+ l, i, r && node(Script::String.new(r)),
416
+ false, false)
417
+ end
418
+ end
419
+
420
+ def paren
421
+ return variable unless try_tok(:lparen)
422
+ was_in_parens = @in_parens
423
+ @in_parens = true
424
+ line = @lexer.line
425
+ e = expr
426
+ assert_tok(:rparen)
427
+ return e || node(List.new([], :space), line)
428
+ ensure
429
+ @in_parens = was_in_parens
430
+ end
431
+
432
+ def variable
433
+ return string unless c = try_tok(:const)
434
+ node(Variable.new(*c.value))
435
+ end
436
+
437
+ def string
438
+ return number unless first = try_tok(:string)
439
+ return first.value unless try_tok(:begin_interpolation)
440
+ line = @lexer.line
441
+ mid = parse_interpolated
442
+ last = assert_expr(:string)
443
+ interp = StringInterpolation.new(first.value, mid, last)
444
+ interp.line = line
445
+ interp
446
+ end
447
+
448
+ def number
449
+ return literal unless tok = try_tok(:number)
450
+ num = tok.value
451
+ num.original = num.to_s unless @in_parens
452
+ num
453
+ end
454
+
455
+ def literal
456
+ (t = try_tok(:color, :bool, :null)) && (return t.value)
457
+ end
458
+
459
+ # It would be possible to have unified #assert and #try methods,
460
+ # but detecting the method/token difference turns out to be quite expensive.
461
+
462
+ EXPR_NAMES = {
463
+ :string => "string",
464
+ :default => "expression (e.g. 1px, bold)",
465
+ :mixin_arglist => "mixin argument",
466
+ :fn_arglist => "function argument",
467
+ }
468
+
469
+ def assert_expr(name, expected = nil)
470
+ (e = send(name)) && (return e)
471
+ @lexer.expected!(expected || EXPR_NAMES[name] || EXPR_NAMES[:default])
472
+ end
473
+
474
+ def assert_tok(*names)
475
+ (t = try_tok(*names)) && (return t)
476
+ @lexer.expected!(names.map {|tok| Lexer::TOKEN_NAMES[tok] || tok}.join(" or "))
477
+ end
478
+
479
+ def try_tok(*names)
480
+ peeked = @lexer.peek
481
+ peeked && names.include?(peeked.type) && @lexer.next
482
+ end
483
+
484
+ def assert_done
485
+ return if @lexer.done?
486
+ @lexer.expected!(EXPR_NAMES[:default])
487
+ end
488
+
489
+ def node(node, line = @lexer.line)
490
+ node.line = line
491
+ node
492
+ end
493
+ end
494
+ end
495
+ end