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,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
@@ -0,0 +1,69 @@
1
+ module Sass::Script
2
+ # A SassScript parse node representing a unary operation,
3
+ # such as `-$b` or `not true`.
4
+ #
5
+ # Currently only `-`, `/`, and `not` are unary operators.
6
+ class UnaryOperation < Node
7
+ # @return [Symbol] The operation to perform
8
+ attr_reader :operator
9
+
10
+ # @return [Script::Node] The parse-tree node for the object of the operator
11
+ attr_reader :operand
12
+
13
+ # @param operand [Script::Node] See \{#operand}
14
+ # @param operator [Symbol] See \{#operator}
15
+ def initialize(operand, operator)
16
+ @operand = operand
17
+ @operator = operator
18
+ super()
19
+ end
20
+
21
+ # @return [String] A human-readable s-expression representation of the operation
22
+ def inspect
23
+ "(#{@operator.inspect} #{@operand.inspect})"
24
+ end
25
+
26
+ # @see Node#to_sass
27
+ def to_sass(opts = {})
28
+ operand = @operand.to_sass(opts)
29
+ if @operand.is_a?(Operation) ||
30
+ (@operator == :minus &&
31
+ (operand =~ Sass::SCSS::RX::IDENT) == 0)
32
+ operand = "(#{@operand.to_sass(opts)})"
33
+ end
34
+ op = Lexer::OPERATORS_REVERSE[@operator]
35
+ op + (op =~ /[a-z]/ ? " " : "") + operand
36
+ end
37
+
38
+ # Returns the operand of the operation.
39
+ #
40
+ # @return [Array<Node>]
41
+ # @see Node#children
42
+ def children
43
+ [@operand]
44
+ end
45
+
46
+ # @see Node#deep_copy
47
+ def deep_copy
48
+ node = dup
49
+ node.instance_variable_set('@operand', @operand.deep_copy)
50
+ node
51
+ end
52
+
53
+ protected
54
+
55
+ # Evaluates the operation.
56
+ #
57
+ # @param environment [Sass::Environment] The environment in which to evaluate the SassScript
58
+ # @return [Literal] The SassScript object that is the value of the operation
59
+ # @raise [Sass::SyntaxError] if the operation is undefined for the operand
60
+ def _perform(environment)
61
+ operator = "unary_#{@operator}"
62
+ literal = @operand.perform(environment)
63
+ literal.send(operator)
64
+ rescue NoMethodError => e
65
+ raise e unless e.name.to_s == operator.to_s
66
+ raise Sass::SyntaxError.new("Undefined unary operation: \"#{@operator} #{literal}\".")
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,58 @@
1
+ module Sass
2
+ module Script
3
+ # A SassScript parse node representing a variable.
4
+ class Variable < Node
5
+ # The name of the variable.
6
+ #
7
+ # @return [String]
8
+ attr_reader :name
9
+
10
+ # The underscored name of the variable.
11
+ #
12
+ # @return [String]
13
+ attr_reader :underscored_name
14
+
15
+ # @param name [String] See \{#name}
16
+ def initialize(name)
17
+ @name = name
18
+ @underscored_name = name.gsub(/-/,"_")
19
+ super()
20
+ end
21
+
22
+ # @return [String] A string representation of the variable
23
+ def inspect(opts = {})
24
+ "$#{dasherize(name, opts)}"
25
+ end
26
+ alias_method :to_sass, :inspect
27
+
28
+ # Returns an empty array.
29
+ #
30
+ # @return [Array<Node>] empty
31
+ # @see Node#children
32
+ def children
33
+ []
34
+ end
35
+
36
+ # @see Node#deep_copy
37
+ def deep_copy
38
+ dup
39
+ end
40
+
41
+ protected
42
+
43
+ # Evaluates the variable.
44
+ #
45
+ # @param environment [Sass::Environment] The environment in which to evaluate the SassScript
46
+ # @return [Literal] The SassScript object that is the value of the variable
47
+ # @raise [Sass::SyntaxError] if the variable is undefined
48
+ def _perform(environment)
49
+ raise SyntaxError.new("Undefined variable: \"$#{name}\".") unless val = environment.var(name)
50
+ if val.is_a?(Number)
51
+ val = val.dup
52
+ val.original = nil
53
+ end
54
+ return val
55
+ end
56
+ end
57
+ end
58
+ end
data/lib/sass/scss.rb ADDED
@@ -0,0 +1,16 @@
1
+ require 'sass/scss/rx'
2
+ require 'sass/scss/script_lexer'
3
+ require 'sass/scss/script_parser'
4
+ require 'sass/scss/parser'
5
+ require 'sass/scss/static_parser'
6
+ require 'sass/scss/css_parser'
7
+
8
+ module Sass
9
+ # SCSS is the CSS syntax for Sass.
10
+ # It parses into the same syntax tree as Sass,
11
+ # and generates the same sort of output CSS.
12
+ #
13
+ # This module contains code for the parsing of SCSS.
14
+ # The evaluation is handled by the broader {Sass} module.
15
+ module SCSS; end
16
+ end
@@ -0,0 +1,36 @@
1
+ require 'sass/script/css_parser'
2
+
3
+ module Sass
4
+ module SCSS
5
+ # This is a subclass of {Parser} which only parses plain CSS.
6
+ # It doesn't support any Sass extensions, such as interpolation,
7
+ # parent references, nested selectors, and so forth.
8
+ # It does support all the same CSS hacks as the SCSS parser, though.
9
+ class CssParser < StaticParser
10
+ private
11
+
12
+ def placeholder_selector; nil; end
13
+ def parent_selector; nil; end
14
+ def interpolation; nil; end
15
+ def use_css_import?; true; end
16
+
17
+ def block_child(context)
18
+ case context
19
+ when :ruleset
20
+ declaration
21
+ when :stylesheet
22
+ directive || ruleset
23
+ when :directive
24
+ directive || declaration_or_ruleset
25
+ end
26
+ end
27
+
28
+ def nested_properties!(node, space)
29
+ expected('expression (e.g. 1px, bold)');
30
+ end
31
+
32
+ @sass_script_parser = Class.new(Sass::Script::CssParser)
33
+ @sass_script_parser.send(:include, ScriptParser)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,1179 @@
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
+ media = media_query_list
328
+ ss
329
+ return node(Tree::CssImportNode.new(str, media.to_a))
330
+ end
331
+
332
+ path = @scanner[1] || @scanner[2]
333
+ ss
334
+
335
+ media = media_query_list
336
+ if path =~ /^(https?:)?\/\// || media || use_css_import?
337
+ node = Sass::Tree::CssImportNode.new(str, media.to_a)
338
+ else
339
+ node = Sass::Tree::ImportNode.new(path.strip)
340
+ end
341
+ node.line = line
342
+ node
343
+ end
344
+
345
+ def use_css_import?; false; end
346
+
347
+ def media_directive
348
+ block(node(Sass::Tree::MediaNode.new(expr!(:media_query_list).to_a)), :directive)
349
+ end
350
+
351
+ # http://www.w3.org/TR/css3-mediaqueries/#syntax
352
+ def media_query_list
353
+ return unless query = media_query
354
+ queries = [query]
355
+
356
+ ss
357
+ while tok(/,/)
358
+ ss; queries << expr!(:media_query)
359
+ end
360
+ ss
361
+
362
+ Sass::Media::QueryList.new(queries)
363
+ end
364
+
365
+ def media_query
366
+ if ident1 = interp_ident
367
+ ss
368
+ ident2 = interp_ident
369
+ ss
370
+ if ident2 && ident2.length == 1 && ident2[0].is_a?(String) && ident2[0].downcase == 'and'
371
+ query = Sass::Media::Query.new([], ident1, [])
372
+ else
373
+ if ident2
374
+ query = Sass::Media::Query.new(ident1, ident2, [])
375
+ else
376
+ query = Sass::Media::Query.new([], ident1, [])
377
+ end
378
+ return query unless tok(/and/i)
379
+ ss
380
+ end
381
+ end
382
+
383
+ if query
384
+ expr = expr!(:media_expr)
385
+ else
386
+ return unless expr = media_expr
387
+ end
388
+ query ||= Sass::Media::Query.new([], [], [])
389
+ query.expressions << expr
390
+
391
+ ss
392
+ while tok(/and/i)
393
+ ss; query.expressions << expr!(:media_expr)
394
+ end
395
+
396
+ query
397
+ end
398
+
399
+ def media_expr
400
+ interp = interpolation and return interp
401
+ return unless tok(/\(/)
402
+ res = ['(']
403
+ ss
404
+ res << sass_script(:parse)
405
+
406
+ if tok(/:/)
407
+ res << ': '
408
+ ss
409
+ res << sass_script(:parse)
410
+ end
411
+ res << tok!(/\)/)
412
+ ss
413
+ res
414
+ end
415
+
416
+ def charset_directive
417
+ tok! STRING
418
+ name = @scanner[1] || @scanner[2]
419
+ ss
420
+ node(Sass::Tree::CharsetNode.new(name))
421
+ end
422
+
423
+ # The document directive is specified in
424
+ # http://www.w3.org/TR/css3-conditional/, but Gecko allows the
425
+ # `url-prefix` and `domain` functions to omit quotation marks, contrary to
426
+ # the standard.
427
+ #
428
+ # We could parse all document directives according to Mozilla's syntax,
429
+ # but if someone's using e.g. @-webkit-document we don't want them to
430
+ # think WebKit works sans quotes.
431
+ def _moz_document_directive
432
+ res = ["@-moz-document "]
433
+ loop do
434
+ res << str{ss} << expr!(:moz_document_function)
435
+ break unless c = tok(/,/)
436
+ res << c
437
+ end
438
+ directive_body(res.flatten)
439
+ end
440
+
441
+ def moz_document_function
442
+ return unless val = interp_uri || _interp_string(:url_prefix) ||
443
+ _interp_string(:domain) || function(!:allow_var) || interpolation
444
+ ss
445
+ val
446
+ end
447
+
448
+ # http://www.w3.org/TR/css3-conditional/
449
+ def supports_directive(name)
450
+ condition = expr!(:supports_condition)
451
+ node = node(Sass::Tree::SupportsNode.new(name, condition))
452
+
453
+ tok!(/\{/)
454
+ node.has_children = true
455
+ block_contents(node, :directive)
456
+ tok!(/\}/)
457
+
458
+ node
459
+ end
460
+
461
+ def supports_condition
462
+ supports_negation || supports_operator || supports_interpolation
463
+ end
464
+
465
+ def supports_negation
466
+ return unless tok(/not/i)
467
+ ss
468
+ Sass::Supports::Negation.new(expr!(:supports_condition_in_parens))
469
+ end
470
+
471
+ def supports_operator
472
+ return unless cond = supports_condition_in_parens
473
+ return cond unless op = tok(/and|or/i)
474
+ begin
475
+ ss
476
+ cond = Sass::Supports::Operator.new(
477
+ cond, expr!(:supports_condition_in_parens), op)
478
+ end while op = tok(/and|or/i)
479
+ cond
480
+ end
481
+
482
+ def supports_condition_in_parens
483
+ interp = supports_interpolation and return interp
484
+ return unless tok(/\(/); ss
485
+ if cond = supports_condition
486
+ tok!(/\)/); ss
487
+ cond
488
+ else
489
+ name = sass_script(:parse)
490
+ tok!(/:/); ss
491
+ value = sass_script(:parse)
492
+ tok!(/\)/); ss
493
+ Sass::Supports::Declaration.new(name, value)
494
+ end
495
+ end
496
+
497
+ def supports_declaration_condition
498
+ return unless tok(/\(/); ss
499
+ supports_declaration_body
500
+ end
501
+
502
+ def supports_interpolation
503
+ return unless interp = interpolation
504
+ ss
505
+ Sass::Supports::Interpolation.new(interp)
506
+ end
507
+
508
+ def variable
509
+ return unless tok(/\$/)
510
+ name = tok!(IDENT)
511
+ ss; tok!(/:/); ss
512
+
513
+ expr = sass_script(:parse)
514
+ guarded = tok(DEFAULT)
515
+ node(Sass::Tree::VariableNode.new(name, expr, guarded))
516
+ end
517
+
518
+ def operator
519
+ # Many of these operators (all except / and ,)
520
+ # are disallowed by the CSS spec,
521
+ # but they're included here for compatibility
522
+ # with some proprietary MS properties
523
+ str {ss if tok(/[\/,:.=]/)}
524
+ end
525
+
526
+ def ruleset
527
+ return unless rules = selector_sequence
528
+ block(node(Sass::Tree::RuleNode.new(rules.flatten.compact)), :ruleset)
529
+ end
530
+
531
+ def block(node, context)
532
+ node.has_children = true
533
+ tok!(/\{/)
534
+ block_contents(node, context)
535
+ tok!(/\}/)
536
+ node
537
+ end
538
+
539
+ # A block may contain declarations and/or rulesets
540
+ def block_contents(node, context)
541
+ block_given? ? yield : ss_comments(node)
542
+ node << (child = block_child(context))
543
+ while tok(/;/) || has_children?(child)
544
+ block_given? ? yield : ss_comments(node)
545
+ node << (child = block_child(context))
546
+ end
547
+ node
548
+ end
549
+
550
+ def block_child(context)
551
+ return variable || directive if context == :function
552
+ return variable || directive || ruleset if context == :stylesheet
553
+ variable || directive || declaration_or_ruleset
554
+ end
555
+
556
+ def has_children?(child_or_array)
557
+ return false unless child_or_array
558
+ return child_or_array.last.has_children if child_or_array.is_a?(Array)
559
+ return child_or_array.has_children
560
+ end
561
+
562
+ # This is a nasty hack, and the only place in the parser
563
+ # that requires a large amount of backtracking.
564
+ # The reason is that we can't figure out if certain strings
565
+ # are declarations or rulesets with fixed finite lookahead.
566
+ # For example, "foo:bar baz baz baz..." could be either a property
567
+ # or a selector.
568
+ #
569
+ # To handle this, we simply check if it works as a property
570
+ # (which is the most common case)
571
+ # and, if it doesn't, try it as a ruleset.
572
+ #
573
+ # We could eke some more efficiency out of this
574
+ # by handling some easy cases (first token isn't an identifier,
575
+ # no colon after the identifier, whitespace after the colon),
576
+ # but I'm not sure the gains would be worth the added complexity.
577
+ def declaration_or_ruleset
578
+ old_use_property_exception, @use_property_exception =
579
+ @use_property_exception, false
580
+ decl_err = catch_error do
581
+ decl = declaration
582
+ unless decl && decl.has_children
583
+ # We want an exception if it's not there,
584
+ # but we don't want to consume if it is
585
+ tok!(/[;}]/) unless tok?(/[;}]/)
586
+ end
587
+ return decl
588
+ end
589
+
590
+ ruleset_err = catch_error {return ruleset}
591
+ rethrow(@use_property_exception ? decl_err : ruleset_err)
592
+ ensure
593
+ @use_property_exception = old_use_property_exception
594
+ end
595
+
596
+ def selector_sequence
597
+ if sel = tok(STATIC_SELECTOR, true)
598
+ return [sel]
599
+ end
600
+
601
+ rules = []
602
+ return unless v = selector
603
+ rules.concat v
604
+
605
+ ws = ''
606
+ while tok(/,/)
607
+ ws << str {ss}
608
+ if v = selector
609
+ rules << ',' << ws
610
+ rules.concat v
611
+ ws = ''
612
+ end
613
+ end
614
+ rules
615
+ end
616
+
617
+ def selector
618
+ return unless sel = _selector
619
+ sel.to_a
620
+ end
621
+
622
+ def selector_comma_sequence
623
+ return unless sel = _selector
624
+ selectors = [sel]
625
+ ws = ''
626
+ while tok(/,/)
627
+ ws << str{ss}
628
+ if sel = _selector
629
+ selectors << sel
630
+ selectors[-1] = Selector::Sequence.new(["\n"] + selectors.last.members) if ws.include?("\n")
631
+ ws = ''
632
+ end
633
+ end
634
+ Selector::CommaSequence.new(selectors)
635
+ end
636
+
637
+ def _selector
638
+ # The combinator here allows the "> E" hack
639
+ return unless val = combinator || simple_selector_sequence
640
+ nl = str{ss}.include?("\n")
641
+ res = []
642
+ res << val
643
+ res << "\n" if nl
644
+
645
+ while val = combinator || simple_selector_sequence
646
+ res << val
647
+ res << "\n" if str{ss}.include?("\n")
648
+ end
649
+ Selector::Sequence.new(res.compact)
650
+ end
651
+
652
+ def combinator
653
+ tok(PLUS) || tok(GREATER) || tok(TILDE) || reference_combinator
654
+ end
655
+
656
+ def reference_combinator
657
+ return unless tok(/\//)
658
+ res = ['/']
659
+ ns, name = expr!(:qualified_name)
660
+ res << ns << '|' if ns
661
+ res << name << tok!(/\//)
662
+ res = res.flatten
663
+ res = res.join '' if res.all? {|e| e.is_a?(String)}
664
+ res
665
+ end
666
+
667
+ def simple_selector_sequence
668
+ # Returning expr by default allows for stuff like
669
+ # http://www.w3.org/TR/css3-animations/#keyframes-
670
+ return expr(!:allow_var) unless e = element_name || id_selector ||
671
+ class_selector || placeholder_selector || attrib || pseudo ||
672
+ parent_selector || interpolation_selector
673
+ res = [e]
674
+
675
+ # The tok(/\*/) allows the "E*" hack
676
+ while v = id_selector || class_selector || placeholder_selector || attrib ||
677
+ pseudo || interpolation_selector ||
678
+ (tok(/\*/) && Selector::Universal.new(nil))
679
+ res << v
680
+ end
681
+
682
+ pos = @scanner.pos
683
+ line = @line
684
+ if sel = str? {simple_selector_sequence}
685
+ @scanner.pos = pos
686
+ @line = line
687
+ begin
688
+ # If we see "*E", don't force a throw because this could be the
689
+ # "*prop: val" hack.
690
+ expected('"{"') if res.length == 1 && res[0].is_a?(Selector::Universal)
691
+ throw_error {expected('"{"')}
692
+ rescue Sass::SyntaxError => e
693
+ e.message << "\n\n\"#{sel}\" may only be used at the beginning of a compound selector."
694
+ raise e
695
+ end
696
+ end
697
+
698
+ Selector::SimpleSequence.new(res, tok(/!/))
699
+ end
700
+
701
+ def parent_selector
702
+ return unless tok(/&/)
703
+ Selector::Parent.new
704
+ end
705
+
706
+ def class_selector
707
+ return unless tok(/\./)
708
+ @expected = "class name"
709
+ Selector::Class.new(merge(expr!(:interp_ident)))
710
+ end
711
+
712
+ def id_selector
713
+ return unless tok(/#(?!\{)/)
714
+ @expected = "id name"
715
+ Selector::Id.new(merge(expr!(:interp_name)))
716
+ end
717
+
718
+ def placeholder_selector
719
+ return unless tok(/%/)
720
+ @expected = "placeholder name"
721
+ Selector::Placeholder.new(merge(expr!(:interp_ident)))
722
+ end
723
+
724
+ def element_name
725
+ ns, name = Sass::Util.destructure(qualified_name(:allow_star_name))
726
+ return unless ns || name
727
+
728
+ if name == '*'
729
+ Selector::Universal.new(merge(ns))
730
+ else
731
+ Selector::Element.new(merge(name), merge(ns))
732
+ end
733
+ end
734
+
735
+ def qualified_name(allow_star_name=false)
736
+ return unless name = interp_ident || tok(/\*/) || (tok?(/\|/) && "")
737
+ return nil, name unless tok(/\|/)
738
+
739
+ return name, expr!(:interp_ident) unless allow_star_name
740
+ @expected = "identifier or *"
741
+ return name, interp_ident || tok!(/\*/)
742
+ end
743
+
744
+ def interpolation_selector
745
+ return unless script = interpolation
746
+ Selector::Interpolation.new(script)
747
+ end
748
+
749
+ def attrib
750
+ return unless tok(/\[/)
751
+ ss
752
+ ns, name = attrib_name!
753
+ ss
754
+
755
+ if op = tok(/=/) ||
756
+ tok(INCLUDES) ||
757
+ tok(DASHMATCH) ||
758
+ tok(PREFIXMATCH) ||
759
+ tok(SUFFIXMATCH) ||
760
+ tok(SUBSTRINGMATCH)
761
+ @expected = "identifier or string"
762
+ ss
763
+ val = interp_ident || expr!(:interp_string)
764
+ ss
765
+ end
766
+ flags = interp_ident || interp_string
767
+ tok!(/\]/)
768
+
769
+ Selector::Attribute.new(merge(name), merge(ns), op, merge(val), merge(flags))
770
+ end
771
+
772
+ def attrib_name!
773
+ if name_or_ns = interp_ident
774
+ # E, E|E
775
+ if tok(/\|(?!=)/)
776
+ ns = name_or_ns
777
+ name = interp_ident
778
+ else
779
+ name = name_or_ns
780
+ end
781
+ else
782
+ # *|E or |E
783
+ ns = [tok(/\*/) || ""]
784
+ tok!(/\|/)
785
+ name = expr!(:interp_ident)
786
+ end
787
+ return ns, name
788
+ end
789
+
790
+ def pseudo
791
+ return unless s = tok(/::?/)
792
+ @expected = "pseudoclass or pseudoelement"
793
+ name = expr!(:interp_ident)
794
+ if tok(/\(/)
795
+ ss
796
+ arg = expr!(:pseudo_arg)
797
+ while tok(/,/)
798
+ arg << ',' << str{ss}
799
+ arg.concat expr!(:pseudo_arg)
800
+ end
801
+ tok!(/\)/)
802
+ end
803
+ Selector::Pseudo.new(s == ':' ? :class : :element, merge(name), merge(arg))
804
+ end
805
+
806
+ def pseudo_arg
807
+ # In the CSS spec, every pseudo-class/element either takes a pseudo
808
+ # expression or a selector comma sequence as an argument. However, we
809
+ # don't want to have to know which takes which, so we handle both at
810
+ # once.
811
+ #
812
+ # However, there are some ambiguities between the two. For instance, "n"
813
+ # could start a pseudo expression like "n+1", or it could start a
814
+ # selector like "n|m". In order to handle this, we must regrettably
815
+ # backtrack.
816
+ expr, sel = nil, nil
817
+ pseudo_err = catch_error do
818
+ expr = pseudo_expr
819
+ next if tok?(/[,)]/)
820
+ expr = nil
821
+ expected '")"'
822
+ end
823
+
824
+ return expr if expr
825
+ sel_err = catch_error {sel = selector}
826
+ return sel if sel
827
+ rethrow pseudo_err if pseudo_err
828
+ rethrow sel_err if sel_err
829
+ return
830
+ end
831
+
832
+ def pseudo_expr
833
+ return unless e = tok(PLUS) || tok(/[-*]/) || tok(NUMBER) ||
834
+ interp_string || tok(IDENT) || interpolation
835
+ res = [e, str{ss}]
836
+ while e = tok(PLUS) || tok(/[-*]/) || tok(NUMBER) ||
837
+ interp_string || tok(IDENT) || interpolation
838
+ res << e << str{ss}
839
+ end
840
+ res
841
+ end
842
+
843
+ def declaration
844
+ # This allows the "*prop: val", ":prop: val", and ".prop: val" hacks
845
+ if s = tok(/[:\*\.]|\#(?!\{)/)
846
+ @use_property_exception = s !~ /[\.\#]/
847
+ name = [s, str{ss}, *expr!(:interp_ident)]
848
+ else
849
+ return unless name = interp_ident
850
+ name = [name] if name.is_a?(String)
851
+ end
852
+ if comment = tok(COMMENT)
853
+ name << comment
854
+ end
855
+ ss
856
+
857
+ tok!(/:/)
858
+ space, value = value!
859
+ ss
860
+ require_block = tok?(/\{/)
861
+
862
+ node = node(Sass::Tree::PropNode.new(name.flatten.compact, value, :new))
863
+
864
+ return node unless require_block
865
+ nested_properties! node, space
866
+ end
867
+
868
+ def value!
869
+ space = !str {ss}.empty?
870
+ @use_property_exception ||= space || !tok?(IDENT)
871
+
872
+ return true, Sass::Script::String.new("") if tok?(/\{/)
873
+ # This is a bit of a dirty trick:
874
+ # if the value is completely static,
875
+ # we don't parse it at all, and instead return a plain old string
876
+ # containing the value.
877
+ # This results in a dramatic speed increase.
878
+ if val = tok(STATIC_VALUE, true)
879
+ return space, Sass::Script::String.new(val.strip)
880
+ end
881
+ return space, sass_script(:parse)
882
+ end
883
+
884
+ def nested_properties!(node, space)
885
+ err(<<MESSAGE) unless space
886
+ Invalid CSS: a space is required between a property and its definition
887
+ when it has other properties nested beneath it.
888
+ MESSAGE
889
+
890
+ @use_property_exception = true
891
+ @expected = 'expression (e.g. 1px, bold) or "{"'
892
+ block(node, :property)
893
+ end
894
+
895
+ def expr(allow_var = true)
896
+ return unless t = term(allow_var)
897
+ res = [t, str{ss}]
898
+
899
+ while (o = operator) && (t = term(allow_var))
900
+ res << o << t << str{ss}
901
+ end
902
+
903
+ res.flatten
904
+ end
905
+
906
+ def term(allow_var)
907
+ if e = tok(NUMBER) ||
908
+ interp_uri ||
909
+ function(allow_var) ||
910
+ interp_string ||
911
+ tok(UNICODERANGE) ||
912
+ interp_ident ||
913
+ tok(HEXCOLOR) ||
914
+ (allow_var && var_expr)
915
+ return e
916
+ end
917
+
918
+ return unless op = tok(/[+-]/)
919
+ @expected = "number or function"
920
+ return [op, tok(NUMBER) || function(allow_var) ||
921
+ (allow_var && var_expr) || expr!(:interpolation)]
922
+ end
923
+
924
+ def function(allow_var)
925
+ return unless name = tok(FUNCTION)
926
+ if name == "expression(" || name == "calc("
927
+ str, _ = Sass::Shared.balance(@scanner, ?(, ?), 1)
928
+ [name, str]
929
+ else
930
+ [name, str{ss}, expr(allow_var), tok!(/\)/)]
931
+ end
932
+ end
933
+
934
+ def var_expr
935
+ return unless tok(/\$/)
936
+ line = @line
937
+ var = Sass::Script::Variable.new(tok!(IDENT))
938
+ var.line = line
939
+ var
940
+ end
941
+
942
+ def interpolation
943
+ return unless tok(INTERP_START)
944
+ sass_script(:parse_interpolated)
945
+ end
946
+
947
+ def interp_string
948
+ _interp_string(:double) || _interp_string(:single)
949
+ end
950
+
951
+ def interp_uri
952
+ _interp_string(:uri)
953
+ end
954
+
955
+ def _interp_string(type)
956
+ return unless start = tok(Sass::Script::Lexer::STRING_REGULAR_EXPRESSIONS[[type, false]])
957
+ res = [start]
958
+
959
+ mid_re = Sass::Script::Lexer::STRING_REGULAR_EXPRESSIONS[[type, true]]
960
+ # @scanner[2].empty? means we've started an interpolated section
961
+ while @scanner[2] == '#{'
962
+ @scanner.pos -= 2 # Don't consume the #{
963
+ res.last.slice!(-2..-1)
964
+ res << expr!(:interpolation) << tok(mid_re)
965
+ end
966
+ res
967
+ end
968
+
969
+ def interp_ident(start = IDENT)
970
+ return unless val = tok(start) || interpolation || tok(IDENT_HYPHEN_INTERP, true)
971
+ res = [val]
972
+ while val = tok(NAME) || interpolation
973
+ res << val
974
+ end
975
+ res
976
+ end
977
+
978
+ def interp_ident_or_var
979
+ (id = interp_ident) and return id
980
+ (var = var_expr) and return [var]
981
+ end
982
+
983
+ def interp_name
984
+ interp_ident NAME
985
+ end
986
+
987
+ def str
988
+ @strs.push ""
989
+ yield
990
+ @strs.last
991
+ ensure
992
+ @strs.pop
993
+ end
994
+
995
+ def str?
996
+ pos = @scanner.pos
997
+ line = @line
998
+ @strs.push ""
999
+ throw_error {yield} && @strs.last
1000
+ rescue Sass::SyntaxError
1001
+ @scanner.pos = pos
1002
+ @line = line
1003
+ nil
1004
+ ensure
1005
+ @strs.pop
1006
+ end
1007
+
1008
+ def node(node)
1009
+ node.line = @line
1010
+ node
1011
+ end
1012
+
1013
+ @sass_script_parser = Class.new(Sass::Script::Parser)
1014
+ @sass_script_parser.send(:include, ScriptParser)
1015
+ # @private
1016
+ def self.sass_script_parser; @sass_script_parser; end
1017
+
1018
+ def sass_script(*args)
1019
+ parser = self.class.sass_script_parser.new(@scanner, @line,
1020
+ @scanner.pos - (@scanner.string[0...@scanner.pos].rindex("\n") || 0))
1021
+ result = parser.send(*args)
1022
+ unless @strs.empty?
1023
+ # Convert to CSS manually so that comments are ignored.
1024
+ src = result.to_sass
1025
+ @strs.each {|s| s << src}
1026
+ end
1027
+ @line = parser.line
1028
+ result
1029
+ rescue Sass::SyntaxError => e
1030
+ throw(:_sass_parser_error, true) if @throw_error
1031
+ raise e
1032
+ end
1033
+
1034
+ def merge(arr)
1035
+ arr && Sass::Util.merge_adjacent_strings([arr].flatten)
1036
+ end
1037
+
1038
+ EXPR_NAMES = {
1039
+ :media_query => "media query (e.g. print, screen, print and screen)",
1040
+ :media_query_list => "media query (e.g. print, screen, print and screen)",
1041
+ :media_expr => "media expression (e.g. (min-device-width: 800px))",
1042
+ :pseudo_arg => "expression (e.g. fr, 2n+1)",
1043
+ :interp_ident => "identifier",
1044
+ :interp_name => "identifier",
1045
+ :qualified_name => "identifier",
1046
+ :expr => "expression (e.g. 1px, bold)",
1047
+ :_selector => "selector",
1048
+ :selector_comma_sequence => "selector",
1049
+ :simple_selector_sequence => "selector",
1050
+ :import_arg => "file to import (string or url())",
1051
+ :moz_document_function => "matching function (e.g. url-prefix(), domain())",
1052
+ :supports_condition => "@supports condition (e.g. (display: flexbox))",
1053
+ :supports_condition_in_parens => "@supports condition (e.g. (display: flexbox))",
1054
+ }
1055
+
1056
+ TOK_NAMES = Sass::Util.to_hash(
1057
+ Sass::SCSS::RX.constants.map {|c| [Sass::SCSS::RX.const_get(c), c.downcase]}).
1058
+ merge(IDENT => "identifier", /[;}]/ => '";"')
1059
+
1060
+ def tok?(rx)
1061
+ @scanner.match?(rx)
1062
+ end
1063
+
1064
+ def expr!(name)
1065
+ (e = send(name)) && (return e)
1066
+ expected(EXPR_NAMES[name] || name.to_s)
1067
+ end
1068
+
1069
+ def tok!(rx)
1070
+ (t = tok(rx)) && (return t)
1071
+ name = TOK_NAMES[rx]
1072
+
1073
+ unless name
1074
+ # Display basic regexps as plain old strings
1075
+ string = rx.source.gsub(/\\(.)/, '\1')
1076
+ name = rx.source == Regexp.escape(string) ? string.inspect : rx.inspect
1077
+ end
1078
+
1079
+ expected(name)
1080
+ end
1081
+
1082
+ def expected(name)
1083
+ throw(:_sass_parser_error, true) if @throw_error
1084
+ self.class.expected(@scanner, @expected || name, @line)
1085
+ end
1086
+
1087
+ def err(msg)
1088
+ throw(:_sass_parser_error, true) if @throw_error
1089
+ raise Sass::SyntaxError.new(msg, :line => @line)
1090
+ end
1091
+
1092
+ def throw_error
1093
+ old_throw_error, @throw_error = @throw_error, false
1094
+ yield
1095
+ ensure
1096
+ @throw_error = old_throw_error
1097
+ end
1098
+
1099
+ def catch_error(&block)
1100
+ old_throw_error, @throw_error = @throw_error, true
1101
+ pos = @scanner.pos
1102
+ line = @line
1103
+ expected = @expected
1104
+ if catch(:_sass_parser_error) {yield; false}
1105
+ @scanner.pos = pos
1106
+ @line = line
1107
+ @expected = expected
1108
+ {:pos => pos, :line => line, :expected => @expected, :block => block}
1109
+ end
1110
+ ensure
1111
+ @throw_error = old_throw_error
1112
+ end
1113
+
1114
+ def rethrow(err)
1115
+ if @throw_error
1116
+ throw :_sass_parser_error, err
1117
+ else
1118
+ @scanner = Sass::Util::MultibyteStringScanner.new(@scanner.string)
1119
+ @scanner.pos = err[:pos]
1120
+ @line = err[:line]
1121
+ @expected = err[:expected]
1122
+ err[:block].call
1123
+ end
1124
+ end
1125
+
1126
+ # @private
1127
+ def self.expected(scanner, expected, line)
1128
+ pos = scanner.pos
1129
+
1130
+ after = scanner.string[0...pos]
1131
+ # Get rid of whitespace between pos and the last token,
1132
+ # but only if there's a newline in there
1133
+ after.gsub!(/\s*\n\s*$/, '')
1134
+ # Also get rid of stuff before the last newline
1135
+ after.gsub!(/.*\n/, '')
1136
+ after = "..." + after[-15..-1] if after.size > 18
1137
+
1138
+ was = scanner.rest.dup
1139
+ # Get rid of whitespace between pos and the next token,
1140
+ # but only if there's a newline in there
1141
+ was.gsub!(/^\s*\n\s*/, '')
1142
+ # Also get rid of stuff after the next newline
1143
+ was.gsub!(/\n.*/, '')
1144
+ was = was[0...15] + "..." if was.size > 18
1145
+
1146
+ raise Sass::SyntaxError.new(
1147
+ "Invalid CSS after \"#{after}\": expected #{expected}, was \"#{was}\"",
1148
+ :line => line)
1149
+ end
1150
+
1151
+ # Avoid allocating lots of new strings for `#tok`.
1152
+ # This is important because `#tok` is called all the time.
1153
+ NEWLINE = "\n"
1154
+
1155
+ def tok(rx, last_group_lookahead = false)
1156
+ res = @scanner.scan(rx)
1157
+ if res
1158
+ # This fixes https://github.com/nex3/sass/issues/104, which affects
1159
+ # Ruby 1.8.7 and REE. This fix is to replace the ?= zero-width
1160
+ # positive lookahead operator in the Regexp (which matches without
1161
+ # consuming the matched group), with a match that does consume the
1162
+ # group, but then rewinds the scanner and removes the group from the
1163
+ # end of the matched string. This fix makes the assumption that the
1164
+ # matched group will always occur at the end of the match.
1165
+ if last_group_lookahead && @scanner[-1]
1166
+ @scanner.pos -= @scanner[-1].length
1167
+ res.slice!(-@scanner[-1].length..-1)
1168
+ end
1169
+ @line += res.count(NEWLINE)
1170
+ @expected = nil
1171
+ if !@strs.empty? && rx != COMMENT && rx != SINGLE_LINE_COMMENT
1172
+ @strs.each {|s| s << res}
1173
+ end
1174
+ res
1175
+ end
1176
+ end
1177
+ end
1178
+ end
1179
+ end