haml 2.2.24 → 3.0.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of haml might be problematic. Click here for more details.

Files changed (168) hide show
  1. data/.yardopts +0 -1
  2. data/README.md +91 -151
  3. data/REMEMBER +11 -1
  4. data/Rakefile +73 -55
  5. data/VERSION +1 -1
  6. data/VERSION_NAME +1 -1
  7. data/bin/css2sass +7 -1
  8. data/bin/sass-convert +7 -0
  9. data/extra/haml-mode.el +2 -1
  10. data/lib/haml/buffer.rb +22 -4
  11. data/lib/haml/engine.rb +5 -1
  12. data/lib/haml/exec.rb +231 -46
  13. data/lib/haml/filters.rb +19 -8
  14. data/lib/haml/helpers.rb +47 -20
  15. data/lib/haml/helpers/action_view_extensions.rb +2 -4
  16. data/lib/haml/helpers/action_view_mods.rb +11 -8
  17. data/lib/haml/helpers/xss_mods.rb +13 -2
  18. data/lib/haml/html.rb +179 -48
  19. data/lib/haml/html/erb.rb +141 -0
  20. data/lib/haml/precompiler.rb +40 -15
  21. data/lib/haml/railtie.rb +1 -5
  22. data/lib/haml/root.rb +3 -0
  23. data/lib/haml/template.rb +4 -14
  24. data/lib/haml/util.rb +120 -30
  25. data/lib/haml/version.rb +25 -2
  26. data/lib/sass.rb +5 -1
  27. data/lib/sass/callbacks.rb +50 -0
  28. data/lib/sass/css.rb +40 -191
  29. data/lib/sass/engine.rb +170 -74
  30. data/lib/sass/environment.rb +8 -2
  31. data/lib/sass/error.rb +163 -25
  32. data/lib/sass/files.rb +31 -28
  33. data/lib/sass/plugin.rb +268 -87
  34. data/lib/sass/plugin/rails.rb +9 -4
  35. data/lib/sass/repl.rb +1 -1
  36. data/lib/sass/script.rb +31 -29
  37. data/lib/sass/script/bool.rb +1 -0
  38. data/lib/sass/script/color.rb +290 -23
  39. data/lib/sass/script/css_lexer.rb +22 -0
  40. data/lib/sass/script/css_parser.rb +28 -0
  41. data/lib/sass/script/funcall.rb +22 -3
  42. data/lib/sass/script/functions.rb +523 -33
  43. data/lib/sass/script/interpolation.rb +42 -0
  44. data/lib/sass/script/lexer.rb +169 -52
  45. data/lib/sass/script/literal.rb +58 -9
  46. data/lib/sass/script/node.rb +79 -1
  47. data/lib/sass/script/number.rb +20 -5
  48. data/lib/sass/script/operation.rb +49 -3
  49. data/lib/sass/script/parser.rb +162 -28
  50. data/lib/sass/script/string.rb +50 -2
  51. data/lib/sass/script/unary_operation.rb +25 -2
  52. data/lib/sass/script/variable.rb +21 -4
  53. data/lib/sass/scss.rb +14 -0
  54. data/lib/sass/scss/css_parser.rb +39 -0
  55. data/lib/sass/scss/parser.rb +683 -0
  56. data/lib/sass/scss/rx.rb +112 -0
  57. data/lib/sass/scss/script_lexer.rb +13 -0
  58. data/lib/sass/scss/script_parser.rb +25 -0
  59. data/lib/sass/tree/comment_node.rb +69 -27
  60. data/lib/sass/tree/debug_node.rb +7 -2
  61. data/lib/sass/tree/directive_node.rb +41 -35
  62. data/lib/sass/tree/for_node.rb +6 -0
  63. data/lib/sass/tree/if_node.rb +13 -1
  64. data/lib/sass/tree/import_node.rb +52 -27
  65. data/lib/sass/tree/mixin_def_node.rb +18 -0
  66. data/lib/sass/tree/mixin_node.rb +41 -6
  67. data/lib/sass/tree/node.rb +197 -70
  68. data/lib/sass/tree/prop_node.rb +152 -57
  69. data/lib/sass/tree/root_node.rb +118 -0
  70. data/lib/sass/tree/rule_node.rb +193 -96
  71. data/lib/sass/tree/variable_node.rb +9 -5
  72. data/lib/sass/tree/while_node.rb +4 -0
  73. data/test/benchmark.rb +5 -5
  74. data/test/haml/engine_test.rb +147 -10
  75. data/test/haml/{rhtml/_av_partial_1.rhtml → erb/_av_partial_1.erb} +1 -1
  76. data/test/haml/{rhtml/_av_partial_2.rhtml → erb/_av_partial_2.erb} +1 -1
  77. data/test/haml/{rhtml/action_view.rhtml → erb/action_view.erb} +1 -1
  78. data/test/haml/{rhtml/standard.rhtml → erb/standard.erb} +0 -0
  79. data/test/haml/helper_test.rb +91 -24
  80. data/test/haml/html2haml/erb_tests.rb +410 -0
  81. data/test/haml/html2haml_test.rb +210 -66
  82. data/test/haml/results/filters.xhtml +1 -1
  83. data/test/haml/results/just_stuff.xhtml +2 -0
  84. data/test/haml/spec_test.rb +44 -0
  85. data/test/haml/template_test.rb +22 -2
  86. data/test/haml/templates/helpers.haml +0 -13
  87. data/test/haml/templates/just_stuff.haml +2 -0
  88. data/test/haml/util_test.rb +48 -0
  89. data/test/sass/callbacks_test.rb +61 -0
  90. data/test/sass/conversion_test.rb +884 -0
  91. data/test/sass/css2sass_test.rb +99 -18
  92. data/test/sass/data/hsl-rgb.txt +319 -0
  93. data/test/sass/engine_test.rb +1049 -131
  94. data/test/sass/functions_test.rb +398 -47
  95. data/test/sass/more_results/more_import.css +1 -1
  96. data/test/sass/more_templates/more_import.sass +3 -3
  97. data/test/sass/plugin_test.rb +184 -10
  98. data/test/sass/results/compact.css +1 -1
  99. data/test/sass/results/complex.css +5 -5
  100. data/test/sass/results/compressed.css +1 -1
  101. data/test/sass/results/expanded.css +1 -1
  102. data/test/sass/results/import.css +3 -1
  103. data/test/sass/results/mixins.css +12 -12
  104. data/test/sass/results/nested.css +1 -1
  105. data/test/sass/results/options.css +1 -0
  106. data/test/sass/results/parent_ref.css +4 -4
  107. data/test/sass/results/script.css +3 -3
  108. data/test/sass/results/scss_import.css +15 -0
  109. data/test/sass/results/scss_importee.css +2 -0
  110. data/test/sass/script_conversion_test.rb +153 -0
  111. data/test/sass/script_test.rb +137 -70
  112. data/test/sass/scss/css_test.rb +811 -0
  113. data/test/sass/scss/rx_test.rb +156 -0
  114. data/test/sass/scss/scss_test.rb +871 -0
  115. data/test/sass/scss/test_helper.rb +37 -0
  116. data/test/sass/templates/alt.sass +2 -2
  117. data/test/sass/templates/bork1.sass +2 -0
  118. data/test/sass/templates/bork3.sass +2 -0
  119. data/test/sass/templates/bork4.sass +2 -0
  120. data/test/sass/templates/import.sass +4 -4
  121. data/test/sass/templates/importee.sass +3 -3
  122. data/test/sass/templates/line_numbers.sass +1 -1
  123. data/test/sass/templates/mixin_bork.sass +5 -0
  124. data/test/sass/templates/mixins.sass +2 -2
  125. data/test/sass/templates/nested_bork1.sass +2 -0
  126. data/test/sass/templates/nested_bork2.sass +2 -0
  127. data/test/sass/templates/nested_bork3.sass +2 -0
  128. data/test/sass/templates/nested_bork4.sass +2 -0
  129. data/test/sass/templates/nested_mixin_bork.sass +6 -0
  130. data/test/sass/templates/options.sass +2 -0
  131. data/test/sass/templates/parent_ref.sass +2 -2
  132. data/test/sass/templates/script.sass +69 -69
  133. data/test/sass/templates/scss_import.scss +10 -0
  134. data/test/sass/templates/scss_importee.scss +1 -0
  135. data/test/sass/templates/units.sass +10 -10
  136. data/test/test_helper.rb +20 -8
  137. data/vendor/fssm/LICENSE +20 -0
  138. data/vendor/fssm/README.markdown +55 -0
  139. data/vendor/fssm/Rakefile +59 -0
  140. data/vendor/fssm/VERSION.yml +5 -0
  141. data/vendor/fssm/example.rb +9 -0
  142. data/vendor/fssm/fssm.gemspec +77 -0
  143. data/vendor/fssm/lib/fssm.rb +33 -0
  144. data/vendor/fssm/lib/fssm/backends/fsevents.rb +36 -0
  145. data/vendor/fssm/lib/fssm/backends/inotify.rb +26 -0
  146. data/vendor/fssm/lib/fssm/backends/polling.rb +25 -0
  147. data/vendor/fssm/lib/fssm/backends/rubycocoa/fsevents.rb +131 -0
  148. data/vendor/fssm/lib/fssm/monitor.rb +26 -0
  149. data/vendor/fssm/lib/fssm/path.rb +91 -0
  150. data/vendor/fssm/lib/fssm/pathname.rb +502 -0
  151. data/vendor/fssm/lib/fssm/state/directory.rb +57 -0
  152. data/vendor/fssm/lib/fssm/state/file.rb +24 -0
  153. data/vendor/fssm/lib/fssm/support.rb +63 -0
  154. data/vendor/fssm/lib/fssm/tree.rb +176 -0
  155. data/vendor/fssm/profile/prof-cache.rb +40 -0
  156. data/vendor/fssm/profile/prof-fssm-pathname.html +1231 -0
  157. data/vendor/fssm/profile/prof-pathname.rb +68 -0
  158. data/vendor/fssm/profile/prof-plain-pathname.html +988 -0
  159. data/vendor/fssm/profile/prof.html +2379 -0
  160. data/vendor/fssm/spec/path_spec.rb +75 -0
  161. data/vendor/fssm/spec/root/duck/quack.txt +0 -0
  162. data/vendor/fssm/spec/root/file.css +0 -0
  163. data/vendor/fssm/spec/root/file.rb +0 -0
  164. data/vendor/fssm/spec/root/file.yml +0 -0
  165. data/vendor/fssm/spec/root/moo/cow.txt +0 -0
  166. data/vendor/fssm/spec/spec_helper.rb +14 -0
  167. metadata +94 -14
  168. data/test/sass/templates/bork.sass +0 -2
@@ -1,12 +1,60 @@
1
1
  require 'sass/script/literal'
2
2
 
3
3
  module Sass::Script
4
- # A SassScript object representing a string of text.
4
+ # A SassScript object representing a CSS string *or* a CSS identifier.
5
5
  class String < Literal
6
6
  # The Ruby value of the string.
7
7
  #
8
8
  # @return [String]
9
9
  attr_reader :value
10
- alias_method :to_s, :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
+ def context=(context)
19
+ super
20
+ @type = :identifier if context == :equals
21
+ end
22
+
23
+ # Creates a new string.
24
+ #
25
+ # @param value [String] See \{#value}
26
+ # @param type [Symbol] See \{#type}
27
+ def initialize(value, type = :identifier)
28
+ super(value)
29
+ @type = type
30
+ end
31
+
32
+ def plus(other)
33
+ other_str = other.is_a?(Sass::Script::String) ? other.value : other.to_s
34
+ Sass::Script::String.new(self.value + other_str, self.type)
35
+ end
36
+
37
+ # @see Node#to_s
38
+ def to_s
39
+ to_sass
40
+ end
41
+
42
+ # @param type [Symbol] The type of string to render this as.
43
+ # `:string`s have double quotes, `:identifier`s do not.
44
+ # @see Node#to_sass
45
+ def to_sass(type = self.type)
46
+ if type == :identifier
47
+ if context == :equals && Sass::SCSS::RX.escape_ident(self.value).include?(?\\)
48
+ return "unquote(#{Sass::Script::String.new(self.value, :string).to_sass})"
49
+ end
50
+ return self.value
51
+ end
52
+
53
+ # Replace single backslashes with double. Really.
54
+ value = self.value.gsub("\\", "\\\\\\\\")
55
+ return "\"#{value}\"" unless value.include?('"')
56
+ return "'#{value}'" unless value.include?("'")
57
+ "\"#{value.gsub('"', "\\\"")}\"" #'
58
+ end
11
59
  end
12
60
  end
@@ -1,6 +1,6 @@
1
1
  module Sass::Script
2
2
  # A SassScript parse node representing a unary operation,
3
- # such as `-!b` or `not true`.
3
+ # such as `-$b` or `not true`.
4
4
  #
5
5
  # Currently only `-`, `/`, and `not` are unary operators.
6
6
  class UnaryOperation < Node
@@ -10,6 +10,7 @@ module Sass::Script
10
10
  def initialize(operand, operator)
11
11
  @operand = operand
12
12
  @operator = operator
13
+ super()
13
14
  end
14
15
 
15
16
  # @return [String] A human-readable s-expression representation of the operation
@@ -17,12 +18,34 @@ module Sass::Script
17
18
  "(#{@operator.inspect} #{@operand.inspect})"
18
19
  end
19
20
 
21
+ # @see Node#to_sass
22
+ def to_sass
23
+ operand = @operand.to_sass
24
+ if @operand.is_a?(Operation) ||
25
+ (@operator == :minus &&
26
+ (operand =~ Sass::SCSS::RX::IDENT) == 0)
27
+ operand = "(#{@operand.to_sass})"
28
+ end
29
+ op = Lexer::OPERATORS_REVERSE[@operator]
30
+ op + (op =~ /[a-z]/ ? " " : "") + operand
31
+ end
32
+
33
+ # Returns the operand of the operation.
34
+ #
35
+ # @return [Array<Node>]
36
+ # @see Node#children
37
+ def children
38
+ [@operand]
39
+ end
40
+
41
+ protected
42
+
20
43
  # Evaluates the operation.
21
44
  #
22
45
  # @param environment [Sass::Environment] The environment in which to evaluate the SassScript
23
46
  # @return [Literal] The SassScript object that is the value of the operation
24
47
  # @raise [Sass::SyntaxError] if the operation is undefined for the operand
25
- def perform(environment)
48
+ def _perform(environment)
26
49
  operator = "unary_#{@operator}"
27
50
  literal = @operand.perform(environment)
28
51
  literal.send(operator)
@@ -10,21 +10,38 @@ module Sass
10
10
  # @param name [String] See \{#name}
11
11
  def initialize(name)
12
12
  @name = name
13
+ super()
13
14
  end
14
15
 
15
16
  # @return [String] A string representation of the variable
16
17
  def inspect
17
- "!#{name}"
18
+ return "!important" if name == "important"
19
+ "$#{name}"
18
20
  end
21
+ alias_method :to_sass, :inspect
22
+
23
+ # Returns an empty array.
24
+ #
25
+ # @return [Array<Node>] empty
26
+ # @see Node#children
27
+ def children
28
+ []
29
+ end
30
+
31
+ protected
19
32
 
20
33
  # Evaluates the variable.
21
34
  #
22
35
  # @param environment [Sass::Environment] The environment in which to evaluate the SassScript
23
36
  # @return [Literal] The SassScript object that is the value of the variable
24
37
  # @raise [Sass::SyntaxError] if the variable is undefined
25
- def perform(environment)
26
- (val = environment.var(name)) && (return val)
27
- raise SyntaxError.new("Undefined variable: \"!#{name}\".")
38
+ def _perform(environment)
39
+ raise SyntaxError.new("Undefined variable: \"$#{name}\".") unless val = environment.var(name)
40
+ if val.is_a?(Number)
41
+ val = val.dup
42
+ val.original = nil
43
+ end
44
+ return val
28
45
  end
29
46
  end
30
47
  end
@@ -0,0 +1,14 @@
1
+ require 'sass/scss/rx'
2
+ require 'sass/scss/script_lexer'
3
+ require 'sass/scss/script_parser'
4
+ require 'sass/scss/parser'
5
+
6
+ module Sass
7
+ # SCSS is the CSS syntax for Sass.
8
+ # It parses into the same syntax tree as Sass,
9
+ # and generates the same sort of output CSS.
10
+ #
11
+ # This module contains code for the parsing of SCSS.
12
+ # The evaluation is handled by the broader {Sass} module.
13
+ module SCSS; end
14
+ end
@@ -0,0 +1,39 @@
1
+ require 'sass/script/css_parser'
2
+
3
+ module Sass
4
+ module SCSS
5
+ class CssParser < Parser
6
+ private
7
+
8
+ def variable; nil; end
9
+ def parent_selector; nil; end
10
+ def interpolation; nil; end
11
+ def interp_string; tok(STRING); end
12
+ def expected_property_separator; '":"'; end
13
+ def use_css_import?; true; end
14
+
15
+ def special_directive(name)
16
+ return unless name == 'media' || name == 'import'
17
+ super
18
+ end
19
+
20
+ def block_child(context)
21
+ case context
22
+ when :ruleset
23
+ declaration
24
+ when :stylesheet
25
+ directive || ruleset
26
+ when :directive
27
+ directive || declaration_or_ruleset
28
+ end
29
+ end
30
+
31
+ def nested_properties!(node, space)
32
+ expected('expression (e.g. 1px, bold)');
33
+ end
34
+
35
+ @sass_script_parser = Class.new(Sass::Script::CssParser)
36
+ @sass_script_parser.send(:include, ScriptParser)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,683 @@
1
+ require 'strscan'
2
+ require 'set'
3
+
4
+ module Sass
5
+ module SCSS
6
+ # The parser for SCSS.
7
+ # It parses a string of code into a tree of {Sass::Tree::Node}s.
8
+ class Parser
9
+ # @param str [String] The source document to parse
10
+ def initialize(str)
11
+ @template = str
12
+ @line = 1
13
+ @strs = []
14
+ end
15
+
16
+ # Parses an SCSS document.
17
+ #
18
+ # @return [Sass::Tree::RootNode] The root node of the document tree
19
+ # @raise [Sass::SyntaxError] if there's a syntax error in the document
20
+ def parse
21
+ @scanner = StringScanner.new(
22
+ Haml::Util.check_encoding(@template) do |msg, line|
23
+ raise Sass::SyntaxError.new(msg, :line => line)
24
+ end.gsub("\r", ""))
25
+ root = stylesheet
26
+ expected("selector or at-rule") unless @scanner.eos?
27
+ root
28
+ end
29
+
30
+ private
31
+
32
+ include Sass::SCSS::RX
33
+
34
+ def stylesheet
35
+ node = node(Sass::Tree::RootNode.new(@scanner.string))
36
+ block_contents(node, :stylesheet) {s(node)}
37
+ end
38
+
39
+ def s(node)
40
+ while tok(S) || tok(CDC) || tok(CDO) || (c = tok(SINGLE_LINE_COMMENT)) || (c = tok(COMMENT))
41
+ next unless c
42
+ process_comment c, node
43
+ c = nil
44
+ end
45
+ true
46
+ end
47
+
48
+ def ss
49
+ nil while tok(S) || tok(SINGLE_LINE_COMMENT) || tok(COMMENT)
50
+ true
51
+ end
52
+
53
+ def ss_comments(node)
54
+ while tok(S) || (c = tok(SINGLE_LINE_COMMENT)) || (c = tok(COMMENT))
55
+ next unless c
56
+ process_comment c, node
57
+ c = nil
58
+ end
59
+
60
+ true
61
+ end
62
+
63
+ def whitespace
64
+ return unless tok(S) || tok(SINGLE_LINE_COMMENT) || tok(COMMENT)
65
+ ss
66
+ end
67
+
68
+ def process_comment(text, node)
69
+ single_line = text =~ /^\/\//
70
+ pre_str = single_line ? "" : @scanner.
71
+ string[0...@scanner.pos].
72
+ reverse[/.*?\*\/(.*?)($|\Z)/, 1].
73
+ reverse.gsub(/[^\s]/, ' ')
74
+ text = text.sub(/^\s*\/\//, '/*').gsub(/^\s*\/\//, ' *') + ' */' if single_line
75
+ comment = Sass::Tree::CommentNode.new(pre_str + text, single_line)
76
+ comment.line = @line - text.count("\n")
77
+ node << comment
78
+ end
79
+
80
+ DIRECTIVES = Set[:mixin, :include, :debug, :for, :while, :if, :import, :media]
81
+
82
+ def directive
83
+ return unless tok(/@/)
84
+ name = tok!(IDENT)
85
+ ss
86
+
87
+ if dir = special_directive(name)
88
+ return dir
89
+ end
90
+
91
+ val = str do
92
+ # Most at-rules take expressions (e.g. @import),
93
+ # but some (e.g. @page) take selector-like arguments
94
+ expr || selector
95
+ end
96
+ node = node(Sass::Tree::DirectiveNode.new("@#{name} #{val}".strip))
97
+
98
+ if tok(/\{/)
99
+ node.has_children = true
100
+ block_contents(node, :directive)
101
+ tok!(/\}/)
102
+ end
103
+
104
+ node
105
+ end
106
+
107
+ def special_directive(name)
108
+ sym = name.gsub('-', '_').to_sym
109
+ DIRECTIVES.include?(sym) && send(sym)
110
+ end
111
+
112
+ def mixin
113
+ name = tok! IDENT
114
+ args = sass_script(:parse_mixin_definition_arglist)
115
+ ss
116
+ block(node(Sass::Tree::MixinDefNode.new(name, args)), :directive)
117
+ end
118
+
119
+ def include
120
+ name = tok! IDENT
121
+ args = sass_script(:parse_mixin_include_arglist)
122
+ ss
123
+ node(Sass::Tree::MixinNode.new(name, args))
124
+ end
125
+
126
+ def debug
127
+ node(Sass::Tree::DebugNode.new(sass_script(:parse)))
128
+ end
129
+
130
+ def for
131
+ tok!(/\$/)
132
+ var = tok! IDENT
133
+ ss
134
+
135
+ tok!(/from/)
136
+ from = sass_script(:parse_until, Set["to", "through"])
137
+ ss
138
+
139
+ @expected = '"to" or "through"'
140
+ exclusive = (tok(/to/) || tok!(/through/)) == 'to'
141
+ to = sass_script(:parse)
142
+ ss
143
+
144
+ block(node(Sass::Tree::ForNode.new(var, from, to, exclusive)), :directive)
145
+ end
146
+
147
+ def while
148
+ expr = sass_script(:parse)
149
+ ss
150
+ block(node(Sass::Tree::WhileNode.new(expr)), :directive)
151
+ end
152
+
153
+ def if
154
+ expr = sass_script(:parse)
155
+ ss
156
+ node = block(node(Sass::Tree::IfNode.new(expr)), :directive)
157
+ ss
158
+ else_block(node)
159
+ end
160
+
161
+ def else_block(node)
162
+ return node unless tok(/@else/)
163
+ ss
164
+ else_node = block(
165
+ Sass::Tree::IfNode.new((sass_script(:parse) if tok(/if/))),
166
+ :directive)
167
+ node.add_else(else_node)
168
+ ss
169
+ else_block(node)
170
+ end
171
+
172
+ def import
173
+ @expected = "string or url()"
174
+ arg = tok(STRING) || tok!(URI)
175
+ path = @scanner[1] || @scanner[2] || @scanner[3]
176
+ ss
177
+
178
+ media = str {media_query_list}.strip
179
+
180
+ if !media.strip.empty? || use_css_import?
181
+ return node(Sass::Tree::DirectiveNode.new("@import #{arg} #{media}".strip))
182
+ end
183
+
184
+ node(Sass::Tree::ImportNode.new(path.strip))
185
+ end
186
+
187
+ def use_css_import?; false; end
188
+
189
+ def media
190
+ val = str {media_query_list}.strip
191
+ block(node(Sass::Tree::DirectiveNode.new("@media #{val}")), :directive)
192
+ end
193
+
194
+ # http://www.w3.org/TR/css3-mediaqueries/#syntax
195
+ def media_query_list
196
+ return unless media_query
197
+
198
+ ss
199
+ while tok(/,/)
200
+ ss; expr!(:media_query); ss
201
+ end
202
+
203
+ true
204
+ end
205
+
206
+ def media_query
207
+ if tok(/only|not/i)
208
+ ss
209
+ @expected = "media type (e.g. print, screen)"
210
+ tok!(IDENT)
211
+ ss
212
+ elsif !tok(IDENT) && !media_expr
213
+ return
214
+ end
215
+
216
+ ss
217
+ while tok(/and/i)
218
+ ss; expr!(:media_expr); ss
219
+ end
220
+
221
+ true
222
+ end
223
+
224
+ def media_expr
225
+ return unless tok(/\(/)
226
+ ss
227
+ @expected = "media feature (e.g. min-device-width, color)"
228
+ tok!(IDENT)
229
+ ss
230
+
231
+ if tok(/:/)
232
+ ss; expr!(:expr)
233
+ end
234
+ tok!(/\)/)
235
+ ss
236
+
237
+ true
238
+ end
239
+
240
+ def variable
241
+ return unless tok(/\$/)
242
+ name = tok!(IDENT)
243
+ ss; tok!(/:/); ss
244
+
245
+ expr = sass_script(:parse)
246
+ guarded = tok(DEFAULT)
247
+ node(Sass::Tree::VariableNode.new(name, expr, guarded))
248
+ end
249
+
250
+ def operator
251
+ # Many of these operators (all except / and ,)
252
+ # are disallowed by the CSS spec,
253
+ # but they're included here for compatibility
254
+ # with some proprietary MS properties
255
+ str {ss if tok(/[\/,:.=]/)}
256
+ end
257
+
258
+ def unary_operator
259
+ tok(/[+-]/)
260
+ end
261
+
262
+ def property
263
+ return unless e = (tok(IDENT) || interpolation)
264
+ res = [e, str{ss}]
265
+
266
+ while e = (interpolation || tok(IDENT))
267
+ res << e
268
+ end
269
+
270
+ ss
271
+ res
272
+ end
273
+
274
+ def ruleset
275
+ rules = []
276
+ return unless v = selector
277
+ rules.concat v
278
+
279
+ while tok(/,/)
280
+ rules << ',' << str {ss}
281
+ rules.concat expr!(:selector)
282
+ end
283
+
284
+ block(node(Sass::Tree::RuleNode.new(rules.flatten.compact)), :ruleset)
285
+ end
286
+
287
+ def block(node, context)
288
+ node.has_children = true
289
+ tok!(/\{/)
290
+ block_contents(node, context)
291
+ tok!(/\}/)
292
+ node
293
+ end
294
+
295
+ # A block may contain declarations and/or rulesets
296
+ def block_contents(node, context)
297
+ block_given? ? yield : ss_comments(node)
298
+ node << (child = block_child(context))
299
+ while tok(/;/) || (child && child.has_children)
300
+ block_given? ? yield : ss_comments(node)
301
+ node << (child = block_child(context))
302
+ end
303
+ node
304
+ end
305
+
306
+ def block_child(context)
307
+ variable || directive || declaration_or_ruleset
308
+ end
309
+
310
+ # This is a nasty hack, and the only place in the parser
311
+ # that requires backtracking.
312
+ # The reason is that we can't figure out if certain strings
313
+ # are declarations or rulesets with fixed finite lookahead.
314
+ # For example, "foo:bar baz baz baz..." could be either a property
315
+ # or a selector.
316
+ #
317
+ # To handle this, we simply check if it works as a property
318
+ # (which is the most common case)
319
+ # and, if it doesn't, try it as a ruleset.
320
+ #
321
+ # We could eke some more efficiency out of this
322
+ # by handling some easy cases (first token isn't an identifier,
323
+ # no colon after the identifier, whitespace after the colon),
324
+ # but I'm not sure the gains would be worth the added complexity.
325
+ def declaration_or_ruleset
326
+ pos = @scanner.pos
327
+ line = @line
328
+ old_use_property_exception, @use_property_exception =
329
+ @use_property_exception, false
330
+ begin
331
+ decl = declaration
332
+ # We want an exception if it's not there,
333
+ # but we don't want to consume if it is
334
+ tok!(/[;}]/) unless tok?(/[;}]/)
335
+ return decl
336
+ rescue Sass::SyntaxError => decl_err
337
+ end
338
+
339
+ @line = line
340
+ @scanner.pos = pos
341
+
342
+ begin
343
+ return ruleset
344
+ rescue Sass::SyntaxError => ruleset_err
345
+ raise @use_property_exception ? decl_err : ruleset_err
346
+ end
347
+ ensure
348
+ @use_property_exception = old_use_property_exception
349
+ end
350
+
351
+ def selector
352
+ # The combinator here allows the "> E" hack
353
+ return unless (comb = combinator) || (seq = simple_selector_sequence)
354
+ res = [comb] + (seq || [])
355
+
356
+ while v = combinator
357
+ res << v
358
+ res.concat(simple_selector_sequence || [])
359
+ end
360
+ res
361
+ end
362
+
363
+ def combinator
364
+ tok(PLUS) || tok(GREATER) || tok(TILDE) || str?{whitespace}
365
+ end
366
+
367
+ def simple_selector_sequence
368
+ # This allows for stuff like http://www.w3.org/TR/css3-animations/#keyframes-
369
+ return expr unless e = element_name || tok(HASH) || class_expr ||
370
+ attrib || negation || pseudo || parent_selector || interpolation
371
+ res = [e]
372
+
373
+ # The tok(/\*/) allows the "E*" hack
374
+ while v = element_name || tok(HASH) || class_expr ||
375
+ attrib || negation || pseudo || tok(/\*/) || interpolation
376
+ res << v
377
+ end
378
+ res
379
+ end
380
+
381
+ def parent_selector
382
+ tok(/&/)
383
+ end
384
+
385
+ def class_expr
386
+ return unless tok(/\./)
387
+ '.' + tok!(IDENT)
388
+ end
389
+
390
+ def element_name
391
+ return unless name = tok(IDENT) || tok(/\*/) || tok?(/\|/)
392
+ if tok(/\|/)
393
+ @expected = "element name or *"
394
+ name << "|" << (tok(IDENT) || tok!(/\*/))
395
+ end
396
+ name
397
+ end
398
+
399
+ def attrib
400
+ return unless tok(/\[/)
401
+ res = ['[', str{ss}, str{attrib_name!}, str{ss}]
402
+
403
+ if m = tok(/=/) ||
404
+ tok(INCLUDES) ||
405
+ tok(DASHMATCH) ||
406
+ tok(PREFIXMATCH) ||
407
+ tok(SUFFIXMATCH) ||
408
+ tok(SUBSTRINGMATCH)
409
+ @expected = "identifier or string"
410
+ res << m << str{ss} << (tok(IDENT) || expr!(:interp_string)) << str{ss}
411
+ end
412
+ res << tok!(/\]/)
413
+ end
414
+
415
+ def attrib_name!
416
+ if tok(IDENT)
417
+ # E, E|E, or E|
418
+ # The last is allowed so that E|="foo" will work
419
+ tok(IDENT) if tok(/\|/)
420
+ elsif tok(/\*/)
421
+ # *|E
422
+ tok!(/\|/)
423
+ tok! IDENT
424
+ else
425
+ # |E or E
426
+ tok(/\|/)
427
+ tok! IDENT
428
+ end
429
+ end
430
+
431
+ def pseudo
432
+ return unless s = tok(/::?/)
433
+
434
+ @expected = "pseudoclass or pseudoelement"
435
+ [s, functional_pseudo || tok!(IDENT)]
436
+ end
437
+
438
+ def functional_pseudo
439
+ return unless fn = tok(FUNCTION)
440
+ [fn, str{ss}, expr!(:pseudo_expr), tok!(/\)/)]
441
+ end
442
+
443
+ def pseudo_expr
444
+ return unless e = tok(PLUS) || tok(/-/) || tok(NUMBER) ||
445
+ interp_string || tok(IDENT) || interpolation
446
+ res = [e, str{ss}]
447
+ while e = tok(PLUS) || tok(/-/) || tok(NUMBER) ||
448
+ interp_string || tok(IDENT) || interpolation
449
+ res << e << str{ss}
450
+ end
451
+ res
452
+ end
453
+
454
+ def negation
455
+ return unless tok(NOT)
456
+ res = [":not(", str{ss}]
457
+ @expected = "selector"
458
+ res << (element_name || tok(HASH) || class_expr || attrib || expr!(:pseudo))
459
+ res << tok!(/\)/)
460
+ end
461
+
462
+ def declaration
463
+ # This allows the "*prop: val", ":prop: val", and ".prop: val" hacks
464
+ if s = tok(/[:\*\.]/)
465
+ @use_property_exception = s != '.'
466
+ name = [s, str{ss}] + expr!(:property)
467
+ else
468
+ return unless name = property
469
+ end
470
+
471
+ @expected = expected_property_separator
472
+ space, value = expr!(:value)
473
+ ss
474
+ require_block = tok?(/\{/)
475
+
476
+ node = node(Sass::Tree::PropNode.new(name.flatten.compact, value, :new))
477
+
478
+ return node unless require_block
479
+ nested_properties! node, space
480
+ end
481
+
482
+ def expected_property_separator
483
+ '":" or "="'
484
+ end
485
+
486
+ def value
487
+ return unless tok(/:/)
488
+ space = !str {ss}.empty?
489
+ @use_property_exception ||= space || !tok?(IDENT)
490
+
491
+ return true, Sass::Script::String.new("") if tok?(/\{/)
492
+ return space, sass_script(:parse)
493
+ end
494
+
495
+ def plain_value
496
+ return unless tok(/:/)
497
+ space = !str {ss}.empty?
498
+ @use_property_exception ||= space || !tok?(IDENT)
499
+
500
+ expression = expr
501
+ expression << tok(IMPORTANT) if expression
502
+ # expression, space, value
503
+ return expression, space, expression || [""]
504
+ end
505
+
506
+ def nested_properties!(node, space)
507
+ raise Sass::SyntaxError.new(<<MESSAGE, :line => @line) unless space
508
+ Invalid CSS: a space is required between a property and its definition
509
+ when it has other properties nested beneath it.
510
+ MESSAGE
511
+
512
+ @use_property_exception = true
513
+ @expected = 'expression (e.g. 1px, bold) or "{"'
514
+ block(node, :property)
515
+ end
516
+
517
+ def expr
518
+ return unless t = term
519
+ res = [t, str{ss}]
520
+
521
+ while (o = operator) && (t = term)
522
+ res << o << t << str{ss}
523
+ end
524
+
525
+ res
526
+ end
527
+
528
+ def term
529
+ unless e = tok(NUMBER) ||
530
+ tok(URI) ||
531
+ function ||
532
+ interp_string ||
533
+ tok(UNICODERANGE) ||
534
+ tok(IDENT) ||
535
+ tok(HEXCOLOR) ||
536
+ interpolation
537
+
538
+ return unless op = unary_operator
539
+ @expected = "number or function"
540
+ return [op, tok(NUMBER) || expr!(:function)]
541
+ end
542
+ e
543
+ end
544
+
545
+ def function
546
+ return unless name = tok(FUNCTION)
547
+ if name == "expression(" || name == "calc("
548
+ str, _ = Haml::Shared.balance(@scanner, ?(, ?), 1)
549
+ [name, str]
550
+ else
551
+ [name, str{ss}, expr, tok!(/\)/)]
552
+ end
553
+ end
554
+
555
+ def interpolation
556
+ return unless tok(/#\{/)
557
+ sass_script(:parse_interpolated)
558
+ end
559
+
560
+ def interp_string
561
+ _interp_string(:double) || _interp_string(:single)
562
+ end
563
+
564
+ def _interp_string(type)
565
+ return unless start = tok(Sass::Script::Lexer::STRING_REGULAR_EXPRESSIONS[[type, false]])
566
+ res = [start]
567
+
568
+ mid_re = Sass::Script::Lexer::STRING_REGULAR_EXPRESSIONS[[type, true]]
569
+ # @scanner[2].empty? means we've started an interpolated section
570
+ res << expr!(:interpolation) << tok(mid_re) while @scanner[2].empty?
571
+ res
572
+ end
573
+
574
+ def str
575
+ @strs.push ""
576
+ yield
577
+ @strs.last
578
+ ensure
579
+ @strs.pop
580
+ end
581
+
582
+ def str?
583
+ @strs.push ""
584
+ yield && @strs.last
585
+ ensure
586
+ @strs.pop
587
+ end
588
+
589
+ def node(node)
590
+ node.line = @line
591
+ node
592
+ end
593
+
594
+ @sass_script_parser = Class.new(Sass::Script::Parser)
595
+ @sass_script_parser.send(:include, ScriptParser)
596
+ # @private
597
+ def self.sass_script_parser; @sass_script_parser; end
598
+
599
+ def sass_script(*args)
600
+ parser = self.class.sass_script_parser.new(@scanner, @line,
601
+ @scanner.pos - (@scanner.string[0...@scanner.pos].rindex("\n") || 0))
602
+ result = parser.send(*args)
603
+ @line = parser.line
604
+ result
605
+ end
606
+
607
+ EXPR_NAMES = {
608
+ :media_query => "media query (e.g. print, screen, print and screen)",
609
+ :media_expr => "media expression (e.g. (min-device-width: 800px)))",
610
+ :pseudo_expr => "expression (e.g. fr, 2n+1)",
611
+ :expr => "expression (e.g. 1px, bold)",
612
+ }
613
+
614
+ TOK_NAMES = Haml::Util.to_hash(
615
+ Sass::SCSS::RX.constants.map {|c| [Sass::SCSS::RX.const_get(c), c.downcase]}).
616
+ merge(IDENT => "identifier", /[;}]/ => '";"', /[=:]/ => '":"')
617
+
618
+ def tok?(rx)
619
+ @scanner.match?(rx)
620
+ end
621
+
622
+ def expr!(name)
623
+ (e = send(name)) && (return e)
624
+ expected(EXPR_NAMES[name] || name.to_s)
625
+ end
626
+
627
+ def tok!(rx)
628
+ (t = tok(rx)) && (return t)
629
+ name = TOK_NAMES[rx]
630
+
631
+ unless name
632
+ # Display basic regexps as plain old strings
633
+ string = rx.source.gsub(/\\(.)/, '\1')
634
+ name = rx.source == Regexp.escape(string) ? string.inspect : rx.inspect
635
+ end
636
+
637
+ expected(name)
638
+ end
639
+
640
+ def expected(name)
641
+ self.class.expected(@scanner, @expected || name, @line)
642
+ end
643
+
644
+ # @private
645
+ def self.expected(scanner, expected, line)
646
+ pos = scanner.pos
647
+
648
+ after = scanner.string[0...pos]
649
+ # Get rid of whitespace between pos and the last token,
650
+ # but only if there's a newline in there
651
+ after.gsub!(/\s*\n\s*$/, '')
652
+ # Also get rid of stuff before the last newline
653
+ after.gsub!(/.*\n/, '')
654
+ after = "..." + after[-15..-1] if after.size > 18
655
+
656
+ was = scanner.rest.dup
657
+ # Get rid of whitespace between pos and the next token,
658
+ # but only if there's a newline in there
659
+ was.gsub!(/^\s*\n\s*/, '')
660
+ # Also get rid of stuff after the next newline
661
+ was.gsub!(/\n.*/, '')
662
+ was = was[0...15] + "..." if was.size > 18
663
+
664
+ raise Sass::SyntaxError.new(
665
+ "Invalid CSS after \"#{after}\": expected #{expected}, was \"#{was}\"",
666
+ :line => line)
667
+ end
668
+
669
+ def tok(rx)
670
+ res = @scanner.scan(rx)
671
+ if res
672
+ @line += res.count("\n")
673
+ @expected = nil
674
+ if !@strs.empty? && rx != COMMENT && rx != SINGLE_LINE_COMMENT
675
+ @strs.each {|s| s << res}
676
+ end
677
+ end
678
+
679
+ res
680
+ end
681
+ end
682
+ end
683
+ end