Permalink
Browse files

Actually parse @keyframes directives.

This adds a KeyframeBlockNode that represents the blocks within a
@keyframe directive. Separating this out allows us to add detection
for invalid @keyframes contents and for invalid selectors that look
like expressions.

Closes #946
  • Loading branch information...
1 parent 19a5ebb commit f4b07be31558b755371fa426829c9f3188f6a19a @nex3 nex3 committed Jan 25, 2014
@@ -340,6 +340,10 @@ of all directives, but will preserve any CSS rules.
* Allow modulo arithmetic for numbers with compatible units. Thanks to
[Isaac Devine](http://www.devinesystems.co.nz).
+* There's new parser logic for `@keyframes` directives. This will
+ improve the formatting of these directives, as well as catch more
+ errors when using them.
+
### Backwards Incompatibilities -- Must Read!
* Sass will now throw an error when `@extend` is used to extend a selector
@@ -418,6 +422,9 @@ of all directives, but will preserve any CSS rules.
* `percentage()`, `round()`, `ceil()`, `floor()`, and `abs()` now
take arguments named '$number' instead of '$value'.
+* Some invalid selectors now generate errors where before they would
+ be passed on to the generated CSS.
+
## 3.2.14 (Unreleased)
* Don't crash when parsing a directive with no name in the indented syntax.
View
@@ -30,6 +30,7 @@
require 'sass/tree/import_node'
require 'sass/tree/charset_node'
require 'sass/tree/at_root_node'
+require 'sass/tree/keyframes_block_node'
require 'sass/tree/visitors/base'
require 'sass/tree/visitors/perform'
require 'sass/tree/visitors/cssize'
@@ -655,11 +656,11 @@ def parse_line(parent, line, root)
parse_mixin_include(line, root)
end
else
- parse_property_or_rule(line)
+ parse_property_or_rule(parent, line)
end
end
- def parse_property_or_rule(line)
+ def parse_property_or_rule(parent, line)
scanner = Sass::Util::MultibyteStringScanner.new(line.text)
hack_char = scanner.scan(/[:\*\.]|\#(?!\{)/)
offset = line.offset
@@ -670,7 +671,11 @@ def parse_property_or_rule(line)
unless (res = parser.parse_interp_ident)
parsed = parse_interp(line.text, line.offset)
- return Tree::RuleNode.new(parsed, full_line_range(line))
+ if parent.is_a?(Tree::DirectiveNode) && parent.name == '@keyframes'
+ return Tree::KeyframesBlockNode.new(parsed)
+ else
+ return Tree::RuleNode.new(parsed, full_line_range(line))
+ end
end
ident_range = Sass::Source::Range.new(
@@ -704,7 +709,11 @@ def parse_property_or_rule(line)
ident_range.start_pos,
Sass::Source::Position.new(@line, to_parser_offset(line.offset) + line.text.length),
@options[:filename], @options[:importer])
- rule = Tree::RuleNode.new(res + interp_parsed, selector_range)
+ if parent.is_a?(Tree::DirectiveNode) && parent.name == '@keyframes'
+ rule = Tree::KeyframesBlockNode.new(res + interp_parsed)
+ else
+ rule = Tree::RuleNode.new(res + interp_parsed, selector_range)
+ end
rule << Tree::CommentNode.new([trailing], :silent) if trailing
rule
end
@@ -16,14 +16,17 @@ def interpolation; nil; end
def use_css_import?; true; end
def block_child(context)
+ old_block_context, @block_context = @block_context, context
case context
- when :ruleset
- declaration
- when :stylesheet
- directive || ruleset
- when :directive
- directive || declaration_or_ruleset
+ when :stylesheet; directive || ruleset
+ when :keyframes; directive || keyframes_block
+ when :keyframes_block; directive || declaration
+ when :ruleset; declaration
+ when :directive; declaration_or_ruleset
+ else raise "[BUG] Unknown block_child context #{context.inspect}"
end
+ ensure
+ @block_context = old_block_context
end
def nested_properties!(node, space)
View
@@ -176,7 +176,7 @@ def process_comment(text, node)
DIRECTIVES = Set[:mixin, :include, :function, :return, :debug, :warn, :for,
:each, :while, :if, :else, :extend, :import, :media, :charset, :content,
- :_moz_document, :at_root]
+ :_moz_document, :at_root, :keyframes]
PREFIXED_DIRECTIVES = Set[:supports]
@@ -281,7 +281,8 @@ def for_directive(start_pos)
to = sass_script(:parse)
ss
- block(node(Sass::Tree::ForNode.new(var, from, to, exclusive), start_pos), :directive)
+ block(node(Sass::Tree::ForNode.new(var, from, to, exclusive), start_pos),
+ @block_context || :directive)
end
def each_directive(start_pos)
@@ -299,19 +300,19 @@ def each_directive(start_pos)
list = sass_script(:parse)
ss
- block(node(Sass::Tree::EachNode.new(vars, list), start_pos), :directive)
+ block(node(Sass::Tree::EachNode.new(vars, list), start_pos), @block_context || :directive)
end
def while_directive(start_pos)
expr = sass_script(:parse)
ss
- block(node(Sass::Tree::WhileNode.new(expr), start_pos), :directive)
+ block(node(Sass::Tree::WhileNode.new(expr), start_pos), @block_context || :directive)
end
def if_directive(start_pos)
expr = sass_script(:parse)
ss
- node = block(node(Sass::Tree::IfNode.new(expr), start_pos), :directive)
+ node = block(node(Sass::Tree::IfNode.new(expr), start_pos), @block_context || :directive)
pos = @scanner.pos
line = @line
ss
@@ -331,7 +332,7 @@ def else_block(node)
ss
else_node = block(
node(Sass::Tree::IfNode.new((sass_script(:parse) if tok(/if/))), start_pos),
- :directive)
+ @block_context || :directive)
node.add_else(else_node)
pos = @scanner.pos
line = @line
@@ -530,6 +531,25 @@ def at_root_directive_list
arr
end
+ def keyframes_directive(start_pos)
+ name = expr!(:interp_ident)
+ node = node(Sass::Tree::DirectiveNode.new(['@keyframes '] + name), start_pos)
+ ss
+ block(node, :keyframes)
+ end
+
+ def keyframes_block
+ start_pos = source_position
+ # Keyframes actually support a very restricted subset of all
+ # expressions, but we allow the general case for backwards and
+ # forwards compatibility, and so we get free support for
+ # interpolation.
+ return unless (value = expr)
+ ss
+ node = node(Sass::Tree::KeyframesBlockNode.new(value), start_pos)
+ block(node, :keyframes_block)
+ end
+
# http://www.w3.org/TR/css3-conditional/
def supports_directive(name, start_pos)
condition = expr!(:supports_condition)
@@ -651,9 +671,18 @@ def block_contents(node, context)
end
def block_child(context)
- return variable || directive if context == :function
- return variable || directive || ruleset if context == :stylesheet
- variable || directive || declaration_or_ruleset
+ old_block_context, @block_context = @block_context, context
+ case context
+ when :function; variable || directive
+ when :stylesheet; variable || directive || ruleset
+ when :keyframes; variable || directive || keyframes_block
+ when :keyframes_block; variable || directive || declaration
+ when :ruleset, :directive,
+ :property; variable || directive || declaration_or_ruleset
+ else raise "[BUG] Unknown block_child context #{context}"
+ end
+ ensure
+ @block_context = old_block_context
end
def has_children?(child_or_array)
@@ -775,14 +804,10 @@ def reference_combinator
end
def simple_selector_sequence
- # Returning expr by default allows for stuff like
- # http://www.w3.org/TR/css3-animations/#keyframes-
-
start_pos = source_position
- e = element_name || id_selector ||
+ return unless (e = element_name || id_selector ||
class_selector || placeholder_selector || attrib || pseudo ||
- parent_selector || interpolation_selector
- return expr(!:allow_var) unless e
+ parent_selector || interpolation_selector)
res = [e]
# The tok(/\*/) allows the "E*" hack
@@ -60,7 +60,7 @@ def interp_ident(ident = IDENT); (s = tok(ident)) && [s]; end
def use_css_import?; true; end
def special_directive(name, start_pos)
- return unless %w[media import charset -moz-document].include?(name)
+ return unless %w[media import charset -moz-document keyframes].include?(name)
super
end
@@ -0,0 +1,15 @@
+module Sass::Tree
+ # A static node reprenting a rule within a `@keyframes` directive.
+ #
+ # @see Sass::Tree
+ class KeyframesBlockNode < Node
+ attr_accessor :value
+
+ attr_accessor :resolved_value
+
+ def initialize(value)
+ @value = value
+ super()
+ end
+ end
+end
@@ -122,7 +122,7 @@ def invalid_prop_child?(parent, child)
VALID_PROP_PARENTS = [Sass::Tree::RuleNode, Sass::Tree::PropNode,
Sass::Tree::MixinDefNode, Sass::Tree::DirectiveNode,
- Sass::Tree::MixinNode]
+ Sass::Tree::MixinNode, Sass::Tree::KeyframesBlockNode]
def invalid_prop_parent?(parent, child)
unless is_any_of?(parent, VALID_PROP_PARENTS)
"Properties are only allowed within rules, directives, mixin includes, or other properties." +
@@ -134,6 +134,25 @@ def invalid_return_parent?(parent, child)
"@return may only be used within a function." unless parent.is_a?(Sass::Tree::FunctionNode)
end
+ VALID_KEYFRAMES_CHILDREN = CONTROL_NODES + [Sass::Tree::VariableNode,
+ Sass::Tree::CommentNode,
+ Sass::Tree::KeyframesBlockNode]
+ def invalid_directive_child?(parent, child)
+ return unless parent.name == '@keyframes'
+ unless is_any_of?(child, VALID_KEYFRAMES_CHILDREN)
+ 'Only keyframes blocks (e.g. "15% { ... }") are allowed within @keyframes.'
+ end
+ end
+
+ VALID_KEYFRAMES_BLOCK_CHILDREN = CONTROL_NODES + [Sass::Tree::VariableNode,
+ Sass::Tree::CommentNode,
+ Sass::Tree::PropNode]
+ def invalid_keyframesblock_child?(parent, child)
+ unless is_any_of?(child, VALID_KEYFRAMES_BLOCK_CHILDREN)
+ "Only properties are allowed within @keyframes blocks."
+ end
+ end
+
private
def is_any_of?(val, classes)
@@ -103,6 +103,10 @@ def visit_directive(node)
res + yield + "\n"
end
+ def visit_keyframesblock(node)
+ "#{tab_str}#{interp_to_src(node.value).rstrip}#{yield}"
+ end
+
def visit_each(node)
vars = node.vars.map {|var| "$#{dasherize(var)}"}.join(", ")
"#{tab_str}@each #{vars} in #{node.list.to_sass(@options)}#{yield}"
@@ -91,6 +91,11 @@ def visit_directive(node)
yield
end
+ def visit_keyframesblock(node)
+ node.value = node.value.map {|c| c.is_a?(Sass::Script::Tree::Node) ? c.deep_copy : c}
+ yield
+ end
+
def visit_media(node)
node.query = node.query.map {|c| c.is_a?(Sass::Script::Tree::Node) ? c.deep_copy : c}
yield
@@ -492,6 +492,14 @@ def visit_cssimport(node)
yield
end
+ def visit_keyframesblock(node)
+ node.resolved_value = run_interp(node.value)
+ with_environment Sass::Environment.new(@environment) do
+ node.children = node.children.map {|c| visit(c)}.flatten
+ node
+ end
+ end
+
private
def run_interp_no_strip(text)
@@ -118,6 +118,11 @@ def visit_cssimport(node)
yield
end
+ def visit_keyframesblock(node)
+ node.value.each {|c| c.options = @options if c.is_a?(Sass::Script::Tree::Node)}
+ yield
+ end
+
def visit_supports(node)
node.condition.options = @options
yield
@@ -189,25 +189,12 @@ def visit_directive(node)
node.children.each do |child|
next if child.invisible?
if node.style == :compact
+ output " " unless first
if child.is_a?(Sass::Tree::PropNode)
- with_tabs(first || was_prop ? 0 : @tabs + 1) do
- visit(child)
- output(' ')
- end
+ with_tabs(0) {visit(child)}
else
- if was_prop
- erase! 1
- output "\n"
- end
-
- if first
- lstrip {with_tabs(@tabs + 1) {visit(child)}}
- else
- with_tabs(@tabs + 1) {visit(child)}
- end
-
+ with_tabs(0) {visit(child)}
rstrip!
- output "\n"
end
was_prop = child.is_a?(Sass::Tree::PropNode)
first = false
@@ -246,6 +233,10 @@ def visit_cssimport(node)
visit_directive(node)
end
+ def visit_keyframesblock(node)
+ visit_directive(node)
+ end
+
def visit_prop(node)
return if node.resolved_value.empty?
tab_str = ' ' * (@tabs + node.tabs)
@@ -1795,6 +1795,30 @@ def test_function_var_kwargs_with_list
SCSS
end
+ def test_keyframes
+ assert_renders(<<SASS, <<SCSS)
+@keyframes bounce
+ from
+ top: 100px
+ 25%
+ top: 50px
+ to
+ top: 0px
+SASS
+@keyframes bounce {
+ from {
+ top: 100px;
+ }
+ 25% {
+ top: 50px;
+ }
+ to {
+ top: 0px;
+ }
+}
+SCSS
+ end
+
## Regression Tests
def test_list_in_args
Oops, something went wrong.

0 comments on commit f4b07be

Please sign in to comment.