Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge remote-tracking branch 'origin/master'

  • Loading branch information...
commit f09ce25e96365046c98c31882116f8d29febb9f0 2 parents bc6eab4 + 274d90c
@nex3 nex3 authored
View
2  doc-src/SCSS_FOR_SASS_USERS.md
@@ -6,7 +6,7 @@ while still supporting the full power of Sass.
This means that every valid CSS3 stylesheet
is a valid SCSS file with the same meaning.
In addition, SCSS understands most CSS hacks
-and vendor-specific syntax, such as [IE's old `filter` syntax](http://msdn.microsoft.com/en-us/library/ms533754%28VS.85%29.aspx).
+and vendor-specific syntax, such as [IE's old `filter` syntax](http://msdn.microsoft.com/en-us/library/ms532847%28v=vs.85%29.aspx#Defining_Visual_Filt).
Since SCSS is a CSS extension,
everything that works in CSS works in SCSS.
View
1  lib/sass/engine.rb
@@ -791,6 +791,7 @@ def parse_comment(line)
# rubocop:disable MethodLength
def parse_directive(parent, line, root)
directive, whitespace, value = line.text[1..-1].split(/(\s+)/, 2)
+ raise SyntaxError.new("Invalid directive: '@'.") unless directive
offset = directive.size + whitespace.size + 1 if whitespace
directive_name = directive.gsub('-', '_').to_sym
View
2  lib/sass/tree/at_root_node.rb
@@ -70,7 +70,7 @@ def exclude?(directive)
# @return [Boolean]
def exclude_node?(node)
return exclude?(node.name.gsub(/^@/, '')) if node.is_a?(Sass::Tree::DirectiveNode)
- exclude?(node.class.to_s.gsub(/.*::(.*)Node$/, '\1').downcase)
+ exclude?('rule') && node.is_a?(Sass::Tree::RuleNode)
end
# @see Node#bubbles?
View
232 lib/sass/tree/visitors/cssize.rb
@@ -9,10 +9,12 @@ def self.visit(root); super; end
# Returns the immediate parent of the current node.
# @return [Tree::Node]
- attr_reader :parent
+ def parent
+ @parents.last
+ end
def initialize
- @parent_directives = []
+ @parents = []
@extends = Sass::Util::SubsetMap.new
end
@@ -41,8 +43,6 @@ def visit_children_without_parent(node)
node.children.map {|c| visit(c)}.flatten
end
- MERGEABLE_DIRECTIVES = [Sass::Tree::MediaNode]
-
# Runs a block of code with the current parent node
# replaced with the given node.
#
@@ -50,19 +50,10 @@ def visit_children_without_parent(node)
# @yield A block in which the parent is set to `parent`.
# @return [Object] The return value of the block.
def with_parent(parent)
- if parent.is_a?(Sass::Tree::DirectiveNode)
- if MERGEABLE_DIRECTIVES.any? {|klass| parent.is_a?(klass)}
- old_parent_directive = @parent_directives.pop
- end
- @parent_directives.push parent
- end
-
- old_parent, @parent = @parent, parent
+ @parents.push parent
yield
ensure
- @parent_directives.pop if parent.is_a?(Sass::Tree::DirectiveNode)
- @parent_directives.push old_parent_directive if old_parent_directive
- @parent = old_parent
+ @parents.pop
end
# In Ruby 1.8, ensures that there's only one `@charset` directive
@@ -138,7 +129,8 @@ def visit_extend(node)
raise Sass::SyntaxError.new("#{member} can't extend: invalid selector")
end
- @extends[sel] = Extend.new(member, sel, node, @parent_directives.dup, :not_found)
+ parent_directives = @parents.select {|p| p.is_a?(Sass::Tree::DirectiveNode)}
+ @extends[sel] = Extend.new(member, sel, node, parent_directives, :not_found)
end
end
@@ -154,31 +146,6 @@ def visit_import(node)
raise e
end
- # Bubbles the `@media` directive up through RuleNodes
- # and merges it with other `@media` directives.
- def visit_media(node)
- yield unless bubble(node)
-
- bubbled = node.children.select do |n|
- n.is_a?(Sass::Tree::AtRootNode) || n.is_a?(Sass::Tree::MediaNode)
- end
- node.children -= bubbled
-
- bubbled = bubbled.map do |n|
- next visit(n) if n.is_a?(Sass::Tree::AtRootNode)
- # Otherwise, n should be a MediaNode.
- next [] unless n.resolved_query = n.resolved_query.merge(node.resolved_query)
- n
- end.flatten
-
- (node.children.empty? ? [] : [node]) + bubbled
- end
-
- # Bubbles the `@supports` directive up through RuleNodes.
- def visit_supports(node)
- visit_directive(node) {yield}
- end
-
# Asserts that all the traced children are valid in their new location.
def visit_trace(node)
visit_children_without_parent(node)
@@ -188,16 +155,6 @@ def visit_trace(node)
raise e
end
- # Bubbles a directive up through RuleNodes.
- def visit_directive(node)
- return yield unless node.has_children
- yield unless (bubbled = bubble(node))
- at_roots = node.children.select {|n| n.is_a?(Sass::Tree::AtRootNode)}
- node.children -= at_roots
- at_roots.map! {|n| visit(n)}.flatten
- (bubbled && node.children.empty? ? [] : [node]) + at_roots
- end
-
# Converts nested properties into flat properties
# and updates the indentation of the prop node based on the nesting level.
def visit_prop(node)
@@ -217,15 +174,38 @@ def visit_prop(node)
result
end
+ def visit_atroot(node)
+ # If there aren't any more directives or rules that this @at-root needs to
+ # exclude, we can get rid of it and just evaluate the children.
+ if @parents.none? {|n| node.exclude_node?(n)}
+ results = visit_children_without_parent(node)
+ results.each {|c| c.tabs += node.tabs if bubblable?(c)}
+ if !results.empty? && bubblable?(results.last)
+ results.last.group_end = node.group_end
+ end
+ return results
+ end
+
+ # If this @at-root excludes the immediate parent, return it as-is so that it
+ # can be bubbled up by the parent node.
+ return Bubble.new(node) if node.exclude_node?(parent)
+
+ # Otherwise, duplicate the current parent and move it into the @at-root
+ # node. As above, returning an @at-root node signals to the parent directive
+ # that it should be bubbled upwards.
+ bubble(node)
+ end
+
+ # The following directives are visible and have children. This means they need
+ # to be able to handle bubbling up nodes such as @at-root and @media.
+
# Updates the indentation of the rule node based on the nesting
# level. The selectors were resolved in {Perform}.
def visit_rule(node)
yield
- rules = node.children.select {|c| c.is_a?(Sass::Tree::RuleNode) || c.bubbles?}
- props = node.children.reject {|c| c.is_a?(Sass::Tree::RuleNode) || c.bubbles? || c.invisible?}
-
- rules.map {|c| c.is_a?(Sass::Tree::AtRootNode) ? visit(c) : c}.flatten
+ rules = node.children.select {|c| bubblable?(c)}
+ props = node.children.reject {|c| bubblable?(c) || c.invisible?}
unless props.empty?
node.children = props
@@ -233,37 +213,141 @@ def visit_rule(node)
rules.unshift(node)
end
- rules.last.group_end = true unless parent.is_a?(Sass::Tree::RuleNode) || rules.empty?
-
+ rules = debubble(rules)
+ unless parent.is_a?(Sass::Tree::RuleNode) || rules.empty? || !bubblable?(rules.last)
+ rules.last.group_end = true
+ end
rules
end
- def visit_atroot(node)
- if @parent_directives.any? {|n| node.exclude_node?(n)}
- return node if node.exclude_node?(parent)
+ # Bubbles a directive up through RuleNodes.
+ def visit_directive(node)
+ return node unless node.has_children
+ return bubble(node) if parent.is_a?(Sass::Tree::RuleNode)
+
+ yield
- new_rule = parent.dup
- new_rule.children = node.children
- node.children = [new_rule]
- return node
+ # Since we don't know if the mere presence of an unknown directive may be
+ # important, we should keep an empty version around even if all the contents
+ # are removed via @at-root. However, if the contents are just bubbled out,
+ # we don't need to do so.
+ directive_exists = node.children.any? do |child|
+ next true unless child.is_a?(Bubble)
+ next false unless child.node.is_a?(Sass::Tree::DirectiveNode)
+ child.node.resolved_value == node.resolved_value
end
- results = visit_children_without_parent(node)
- results.each {|n| n.tabs += node.tabs}
- results.last.group_end = node.group_end unless results.empty?
- results
+ if directive_exists
+ []
+ else
+ empty_node = node.dup
+ empty_node.children = []
+ [empty_node]
+ end + debubble(node.children, node)
+ end
+
+ # Bubbles the `@media` directive up through RuleNodes
+ # and merges it with other `@media` directives.
+ def visit_media(node)
+ return bubble(node) if parent.is_a?(Sass::Tree::RuleNode)
+ return Bubble.new(node) if parent.is_a?(Sass::Tree::MediaNode)
+
+ yield
+
+ debubble(node.children, node).map do |child|
+ next child unless child.is_a?(Sass::Tree::MediaNode)
+ # The debubbled list can include copies of `node`, and we don't want to
+ # merge it with its own query.
+ next child if child.resolved_query == node.resolved_query
+ next child if child.resolved_query = child.resolved_query.merge(node.resolved_query)
+ end.compact
+ end
+
+ # Bubbles the `@supports` directive up through RuleNodes.
+ def visit_supports(node)
+ return node unless node.has_children
+ return bubble(node) if parent.is_a?(Sass::Tree::RuleNode)
+
+ yield
+
+ debubble(node.children, node)
end
private
+ # "Bubbles" `node` one level by copying the parent and wrapping `node`'s
+ # children with it.
+ #
+ # @param node [Sass::Tree::Node].
+ # @return [Bubble]
def bubble(node)
- return unless parent.is_a?(Sass::Tree::RuleNode)
new_rule = parent.dup
new_rule.children = node.children
- node.children = with_parent(node) {Array(visit(new_rule))}
- # If the last child is actually the end of the group,
- # the parent's cssize will set it properly
- node.children.last.group_end = false unless node.children.empty?
- true
+ node.children = [new_rule]
+ Bubble.new(node)
+ end
+
+ # Pops all bubbles in `children` and intersperses the results with the other
+ # values.
+ #
+ # If `parent` is passed, it's copied and used as the parent node for the
+ # nested portions of `children`.
+ #
+ # @param children [List<Sass::Tree::Node, Bubble>]
+ # @param parent [Sass::Tree::Node]
+ # @return [List<Sass::Tree::Node, Bubble>]
+ def debubble(children, parent = nil)
+ Sass::Util.slice_by(children) {|c| c.is_a?(Bubble)}.map do |(is_bubble, slice)|
+ next slice.map {|b| b.pop(self)} if is_bubble
+ next slice unless parent
+ parent.children = slice
+ parent
+ end.flatten
+ end
+
+ # Returns whether or not a node can be bubbled up through the syntax tree.
+ #
+ # @param node [Sass::Tree::Node]
+ # @return [Boolean]
+ def bubblable?(node)
+ node.is_a?(Sass::Tree::RuleNode) || node.bubbles?
+ end
+
+ # A wrapper class for a node that indicates to the parent that it should
+ # treat the wrapped node as a sibling rather than a child.
+ #
+ # Nodes should be wrapped before they're passed to \{Cssize.visit}. They will
+ # be automatically visited upon calling \{#pop}.
+ #
+ # This duck types as a [Sass::Tree::Node] for the purposes of
+ # tree-manipulation operations.
+ class Bubble
+ attr_accessor :node
+ attr_accessor :tabs
+ attr_accessor :group_end
+
+ def initialize(node)
+ @node = node
+ @tabs = 0
+ end
+
+ # "Pops" the bubble by using `visitor` to visit the wrapped node and
+ # returning the result.
+ #
+ # @param visitor [Sass::Tree::Visitors::Cssize]
+ # @return [Array<Sass::Tree::Node, Bubble>] The results of visiting `node`.
+ def pop(visitor)
+ node.tabs += tabs
+ node.group_end = group_end
+ [visitor.send(:visit, node)].flatten
+ end
+
+ def bubbles?
+ true
+ end
+
+ def inspect
+ "(Bubble #{children.map {|c| c.inspect}.join(' ')})"
+ end
end
end
View
13 lib/sass/util.rb
@@ -170,6 +170,19 @@ def intersperse(enum, val)
enum.inject([]) {|a, e| a << e << val}[0...-1]
end
+ def slice_by(enum)
+ results = []
+ enum.each do |value|
+ key = yield(value)
+ if !results.empty? && results.last.first == key
+ results.last.last << value
+ else
+ results << [key, [value]]
+ end
+ end
+ results
+ end
+
# Substitutes a sub-array of one array with another sub-array.
#
# @param ary [Array] The array in which to make the substitution
View
9 test/sass/engine_test.rb
@@ -166,11 +166,13 @@ class SassEngineTest < Test::Unit::TestCase
"@content" => '@content may only be used within a mixin.',
"=simple\n .simple\n color: red\n+simple\n color: blue" => ['Mixin "simple" does not accept a content block.', 4],
"@import \"foo\" // bar" => "Invalid CSS after \"\"foo\" \": expected media query list, was \"// bar\"",
+ "@at-root\n a: b" => "Properties are only allowed within rules, directives, mixin includes, or other properties.",
# Regression tests
"a\n b:\n c\n d" => ["Illegal nesting: Only properties may be nested beneath properties.", 3],
"& foo\n bar: baz\n blat: bang" => ["Base-level rules cannot contain the parent-selector-referencing character '&'.", 1],
"a\n b: c\n& foo\n bar: baz\n blat: bang" => ["Base-level rules cannot contain the parent-selector-referencing character '&'.", 3],
+ "@" => "Invalid directive: '@'.",
}
def teardown
@@ -2308,9 +2310,10 @@ def test_nested_media_around_properties
@media print {
.outside {
color: black; } }
- @media print and (a: b) {
- .outside .inside {
- border: 1px solid black; } }
+ @media print and (a: b) {
+ .outside .inside {
+ border: 1px solid black; } }
+
.outside .middle {
display: block; }
CSS
View
128 test/sass/scss/scss_test.rb
@@ -2305,6 +2305,123 @@ def test_at_root_in_unknown_directive
SCSS
end
+ def test_comments_in_at_root
+ assert_equal <<CSS, render(<<SCSS)
+/* foo */
+.bar {
+ a: b; }
+
+/* baz */
+CSS
+.foo {
+ @at-root {
+ /* foo */
+ .bar {a: b}
+ /* baz */
+ }
+}
+SCSS
+ end
+
+ def test_comments_in_at_root_in_media
+ assert_equal <<CSS, render(<<SCSS)
+@media screen {
+ /* foo */
+ .bar {
+ a: b; }
+
+ /* baz */ }
+CSS
+@media screen {
+ .foo {
+ @at-root {
+ /* foo */
+ .bar {a: b}
+ /* baz */
+ }
+ }
+}
+SCSS
+ end
+
+ def test_comments_in_at_root_in_unknown_directive
+ assert_equal <<CSS, render(<<SCSS)
+@fblthp {
+ /* foo */
+ .bar {
+ a: b; }
+
+ /* baz */ }
+CSS
+@fblthp {
+ .foo {
+ @at-root {
+ /* foo */
+ .bar {a: b}
+ /* baz */
+ }
+ }
+}
+SCSS
+ end
+
+ def test_media_directive_in_at_root
+ assert_equal <<CSS, render(<<SCSS)
+@media screen {
+ .bar {
+ a: b; } }
+CSS
+.foo {
+ @at-root {
+ @media screen {.bar {a: b}}
+ }
+}
+SCSS
+ end
+
+ def test_bubbled_media_directive_in_at_root
+ assert_equal <<CSS, render(<<SCSS)
+@media screen {
+ .bar .baz {
+ a: b; } }
+CSS
+.foo {
+ @at-root {
+ .bar {
+ @media screen {.baz {a: b}}
+ }
+ }
+}
+SCSS
+ end
+
+ def test_unknown_directive_in_at_root
+ assert_equal <<CSS, render(<<SCSS)
+@fblthp {
+ .bar {
+ a: b; } }
+CSS
+.foo {
+ @at-root {
+ @fblthp {.bar {a: b}}
+ }
+}
+SCSS
+ end
+
+ def test_at_root_in_at_root
+ assert_equal <<CSS, render(<<SCSS)
+.bar {
+ a: b; }
+CSS
+.foo {
+ @at-root {
+ @at-root .bar {a: b}
+ }
+}
+SCSS
+ end
+
def test_at_root_with_parent_ref
assert_equal <<CSS, render(<<SCSS)
.foo {
@@ -2406,6 +2523,7 @@ def test_at_root_without_rule
def test_at_root_without_unknown_directive
assert_equal <<CSS, render(<<SCSS)
+@fblthp {}
.foo .bar {
a: b; }
CSS
@@ -2443,6 +2561,8 @@ def test_at_root_without_multiple
def test_at_root_without_all
assert_equal <<CSS, render(<<SCSS)
+@supports (foo: bar) {
+ @fblthp {} }
.bar {
a: b; }
CSS
@@ -2463,6 +2583,7 @@ def test_at_root_without_all
def test_at_root_with_media
assert_equal <<CSS, render(<<SCSS)
@media screen {
+ @fblthp {}
.bar {
a: b; } }
CSS
@@ -2484,6 +2605,8 @@ def test_at_root_with_media
def test_at_root_with_rule
assert_equal <<CSS, render(<<SCSS)
+@media screen {
+ @fblthp {} }
.foo .bar {
a: b; }
CSS
@@ -2505,6 +2628,8 @@ def test_at_root_with_rule
def test_at_root_with_supports
assert_equal <<CSS, render(<<SCSS)
+@media screen {
+ @fblthp {} }
@supports (foo: bar) {
.bar {
a: b; } }
@@ -2527,6 +2652,8 @@ def test_at_root_with_supports
def test_at_root_with_unknown_directive
assert_equal <<CSS, render(<<SCSS)
+@media screen {
+ @fblthp {} }
@fblthp {
.bar {
a: b; } }
@@ -2550,6 +2677,7 @@ def test_at_root_with_unknown_directive
def test_at_root_with_multiple
assert_equal <<CSS, render(<<SCSS)
@media screen {
+ @fblthp {}
.foo .bar {
a: b; } }
CSS
Please sign in to comment.
Something went wrong with that request. Please try again.