Skip to content

Commit

Permalink
Merge branch 'stable'
Browse files Browse the repository at this point in the history
Conflicts:
	lib/sass/selector/comma_sequence.rb
	lib/sass/selector/sequence.rb
	lib/sass/selector/simple_sequence.rb
	lib/sass/tree/visitors/check_nesting.rb
	lib/sass/tree/visitors/to_css.rb
	test/sass/extend_test.rb
	test/sass/util_test.rb
  • Loading branch information
nex3 committed May 26, 2012
2 parents ba206d6 + 51f5ffd commit e215507
Show file tree
Hide file tree
Showing 17 changed files with 378 additions and 60 deletions.
2 changes: 2 additions & 0 deletions doc-src/SASS_CHANGELOG.md
Expand Up @@ -148,6 +148,8 @@ that make use of `@media` and other directives dynamically.

* Fix an `uninitialized constant Sass::Exec::Sass::Util` error when using the
command-line tool.
* Allow `@extend` within directives such as `@media` as long as it only extends
selectors that are within the same directive.

## 3.1.18

Expand Down
38 changes: 34 additions & 4 deletions doc-src/SASS_REFERENCE.md
Expand Up @@ -1596,10 +1596,40 @@ Is compiled to:

#### `@extend` in Directives

Unfortunately, `@extend` cannot be used within directives such as `@media`. Sass
is unable to make CSS rules outside of the `@media` block apply to selectors
inside it without creating a huge amount of stylesheet bloat by copying styles
all over the place.
There are some restrictions on the use of `@extend` within directives such as
`@media`. Sass is unable to make CSS rules outside of the `@media` block apply
to selectors inside it without creating a huge amount of stylesheet bloat by
copying styles all over the place. This means that if you use `@extend` within
`@media` (or other CSS directives), you may only extend selectors that appear
within the same directive block.

For example, the following works fine:

@media print {
.error {
border: 1px #f00;
background-color: #fdd;
}
.seriousError {
@extend .error;
border-width: 3px;
}
}

But this is an error:

.error {
border: 1px #f00;
background-color: #fdd;
}

@media print {
.seriousError {
// INVALID EXTEND: .error is used outside of the "@media print" directive
@extend .error;
border-width: 3px;
}
}

Someday we hope to have `@extend` supported natively in the browser, which will
allow it to be used within `@media` and other directives.
Expand Down
1 change: 1 addition & 0 deletions lib/sass/engine.rb
Expand Up @@ -29,6 +29,7 @@
require 'sass/tree/visitors/base'
require 'sass/tree/visitors/perform'
require 'sass/tree/visitors/cssize'
require 'sass/tree/visitors/extend'
require 'sass/tree/visitors/convert'
require 'sass/tree/visitors/to_css'
require 'sass/tree/visitors/deep_copy'
Expand Down
6 changes: 4 additions & 2 deletions lib/sass/selector/comma_sequence.rb
Expand Up @@ -47,11 +47,13 @@ def resolve_parent_refs(super_cseq)
# @param extends [Sass::Util::SubsetMap{Selector::Simple =>
# Sass::Tree::Visitors::Cssize::Extend}]
# The extensions to perform on this selector
# @param parent_directives [Array<Sass::Tree::DirectiveNode>]
# The directives containing this selector.
# @return [CommaSequence] A copy of this selector,
# with extensions made according to `extends`
def do_extend(extends)
def do_extend(extends, parent_directives)
CommaSequence.new(members.map do |seq|
extended = seq.do_extend(extends)
extended = seq.do_extend(extends, parent_directives)
# First Law of Extend: the result of extending a selector should
# always contain the base selector.
#
Expand Down
8 changes: 5 additions & 3 deletions lib/sass/selector/sequence.rb
Expand Up @@ -68,18 +68,20 @@ def resolve_parent_refs(super_seq)
# Non-destructively extends this selector with the extensions specified in a hash
# (which should come from {Sass::Tree::Visitors::Cssize}).
#
# @overload def do_extend(extends)
# @overload def do_extend(extends, parent_directives)
# @param extends [Sass::Util::SubsetMap{Selector::Simple =>
# Sass::Tree::Visitors::Cssize::Extend}]
# The extensions to perform on this selector
# @param parent_directives [Array<Sass::Tree::DirectiveNode>]
# The directives containing this selector.
# @return [Array<Sequence>] A list of selectors generated
# by extending this selector with `extends`.
# These correspond to a {CommaSequence}'s {CommaSequence#members members array}.
# @see CommaSequence#do_extend
def do_extend(extends, seen = Set.new)
def do_extend(extends, parent_directives, seen = Set.new)
extended_not_expanded = members.map do |sseq_or_op|
next [[sseq_or_op]] unless sseq_or_op.is_a?(SimpleSequence)
extended = sseq_or_op.do_extend(extends, seen)
extended = sseq_or_op.do_extend(extends, parent_directives, seen)
choices = extended.map {|seq| seq.members}
choices.unshift([sseq_or_op]) unless extended.any? {|seq| seq.superselector?(sseq_or_op)}
choices
Expand Down
26 changes: 22 additions & 4 deletions lib/sass/selector/simple_sequence.rb
Expand Up @@ -72,26 +72,29 @@ def resolve_parent_refs(super_seq)
# Non-destrucively extends this selector with the extensions specified in a hash
# (which should come from {Sass::Tree::Visitors::Cssize}).
#
# @overload def do_extend(extends, sources)
# @overload def do_extend(extends, parent_directives)
# @param extends [{Selector::Simple =>
# Sass::Tree::Visitors::Cssize::Extend}]
# The extensions to perform on this selector
# @param parent_directives [Array<Sass::Tree::DirectiveNode>]
# The directives containing this selector.
# @return [Array<Sequence>] A list of selectors generated
# by extending this selector with `extends`.
# @see CommaSequence#do_extend
def do_extend(extends, seen = Set.new)
def do_extend(extends, parent_directives, seen = Set.new)
Sass::Util.group_by_to_a(extends.get(members.to_set)) {|ex, _| ex.extender}.map do |seq, group|
sels = group.map {|_, s| s}.flatten
# If A {@extend B} and C {...},
# ex.extender is A, sels is B, and self is C
# seq is A, sels is B, and self is C

self_without_sel = self.members - sels
next unless unified = seq.members.last.unify(self_without_sel)
next if group.map {|e, _| check_directives_match!(e, parent_directives)}.none?
new_seq = Sequence.new(seq.members[0...-1] + [unified])
new_seq.add_sources!(sources + [seq])
[sels, new_seq]
end.compact.map do |sels, seq|
seen.include?(sels) ? [] : seq.do_extend(extends, seen + [sels])
seen.include?(sels) ? [] : seq.do_extend(extends, parent_directives, seen + [sels])
end.flatten.uniq
end

Expand Down Expand Up @@ -155,6 +158,21 @@ def with_more_sources(sources)

private

def check_directives_match!(extend, parent_directives)
dirs1 = extend.directives.map {|d| d.resolved_value}
dirs2 = parent_directives.map {|d| d.resolved_value}
return true if Sass::Util.subsequence?(dirs1, dirs2)

Sass::Util.sass_warn <<WARNING
DEPRECATION WARNING on line #{extend.node.line}#{" of #{extend.node.filename}" if extend.node.filename}:
@extending an outer selector from within #{extend.directives.last.name} is deprecated.
You may only @extend selectors within the same directive.
This will be an error in Sass 3.3.
It can only work once @extend is supported natively in the browser.
WARNING
return false
end

def _hash
[base, Sass::Util.set_hash(rest)].hash
end
Expand Down
22 changes: 0 additions & 22 deletions lib/sass/tree/node.rb
Expand Up @@ -141,28 +141,6 @@ def inspect
"(#{self.class} #{children.map {|c| c.inspect}.join(' ')})"
end

# Converts a static CSS tree (e.g. the output of \{Tree::Visitors::Cssize})
# into another static CSS tree,
# with the given extensions applied to all relevant {RuleNode}s.
#
# @todo Link this to the reference documentation on `@extend`
# when such a thing exists.
#
# @param extends [Sass::Util::SubsetMap{Selector::Simple =>
# Sass::Tree::Visitors::Cssize::Extend}]
# The extensions to perform on this tree
# @return [Tree::Node] The resulting tree of static CSS nodes.
# @raise [Sass::SyntaxError] Only if there's a programmer error
# and this is not a static CSS tree
def do_extend(extends)
node = dup
node.children = children.map {|c| c.do_extend(extends)}
node
rescue Sass::SyntaxError => e
e.modify_backtrace(:filename => filename, :line => line)
raise e
end

# Iterates through each node in the tree rooted at this node
# in a pre-order walk.
#
Expand Down
2 changes: 1 addition & 1 deletion lib/sass/tree/root_node.rb
Expand Up @@ -20,7 +20,7 @@ def render
result = Visitors::Perform.visit(self)
Visitors::CheckNesting.visit(result) # Check again to validate mixins
result, extends = Visitors::Cssize.visit(result)
result = result.do_extend(extends) unless extends.empty?
Visitors::Extend.visit(result, extends)
result.to_s
end
end
Expand Down
9 changes: 0 additions & 9 deletions lib/sass/tree/rule_node.rb
Expand Up @@ -103,15 +103,6 @@ def continued?
last.is_a?(String) && last[-1] == ?,
end

# Extends this Rule's selector with the given `extends`.
#
# @see Node#do_extend
def do_extend(extends)
node = dup
node.resolved_rules = resolved_rules.do_extend(extends)
node
end

# A hash that will be associated with this rule in the CSS document
# if the {file:SASS_REFERENCE.md#debug_info-option `:debug_info` option} is enabled.
# This data is used by e.g. [the FireSass Firebug extension](https://addons.mozilla.org/en-US/firefox/addon/103988).
Expand Down
8 changes: 0 additions & 8 deletions lib/sass/tree/visitors/check_nesting.rb
Expand Up @@ -71,14 +71,6 @@ def invalid_extend_parent?(parent, child)
unless is_any_of?(parent, VALID_EXTEND_PARENTS)
return "Extend directives may only be used within rules."
end

if directive = @parents.find {|p| p.is_a?(Sass::Tree::DirectiveNode)}
return <<ERR.rstrip
@extend may not be used within directives (e.g. #{directive.name}).
This will only work once @extend is supported natively in the browser.
ERR
end
end

def invalid_function_parent?(parent, child)
Expand Down
9 changes: 7 additions & 2 deletions lib/sass/tree/visitors/cssize.rb
Expand Up @@ -12,6 +12,7 @@ def self.visit(root); super; end
attr_reader :parent

def initialize
@parent_directives = []
@extends = Sass::Util::SubsetMap.new
end

Expand All @@ -38,9 +39,11 @@ def visit_children(parent)
# @yield A block in which the parent is set to `parent`.
# @return [Object] The return value of the block.
def with_parent(parent)
@parent_directives.push parent if parent.is_a?(Sass::Tree::DirectiveNode)
old_parent, @parent = @parent, parent
yield
ensure
@parent_directives.pop if parent.is_a?(Sass::Tree::DirectiveNode)
@parent = old_parent
end

Expand Down Expand Up @@ -89,7 +92,9 @@ def visit_root(node)
# The selector of the CSS rule containing the `@extend`.
# @attr target [Array<Sass::Selector::Simple>] The selector being `@extend`ed.
# @attr node [Sass::Tree::ExtendNode] The node that produced this extend.
Extend = Struct.new(:extender, :target, :node)
# @attr directives [Array<Sass::Tree::DirectiveNode>]
# The directives containing the `@extend`.
Extend = Struct.new(:extender, :target, :node, :directives)

# Registers an extension in the `@extends` subset map.
def visit_extend(node)
Expand All @@ -111,7 +116,7 @@ def visit_extend(node)
raise Sass::SyntaxError.new("#{seq} can't extend: invalid selector")
end

@extends[sel] = Extend.new(seq, sel, node)
@extends[sel] = Extend.new(seq, sel, node, @parent_directives.dup)
end
end

Expand Down
42 changes: 42 additions & 0 deletions lib/sass/tree/visitors/extend.rb
@@ -0,0 +1,42 @@
# A visitor for performing selector inheritance on a static CSS tree.
#
# Destructively modifies the tree.
class Sass::Tree::Visitors::Extend < Sass::Tree::Visitors::Base
# @param root [Tree::Node] The root node of the tree to visit.
# @param extends [Sass::Util::SubsetMap{Selector::Simple =>
# Sass::Tree::Visitors::Cssize::Extend}]
# The extensions to perform on this tree.
# @return [Object] The return value of \{#visit} for the root node.
def self.visit(root, extends)
return if extends.empty?
new(extends).send(:visit, root)
end

protected

def initialize(extends)
@parent_directives = []
@extends = extends
end

# If an exception is raised, this adds proper metadata to the backtrace.
def visit(node)
super(node)
rescue Sass::SyntaxError => e
e.modify_backtrace(:filename => node.filename, :line => node.line)
raise e
end

# Keeps track of the current parent directives.
def visit_children(parent)
@parent_directives.push parent if parent.is_a?(Sass::Tree::DirectiveNode)
super
ensure
@parent_directives.pop if parent.is_a?(Sass::Tree::DirectiveNode)
end

# Applies the extend to a single rule's selector.
def visit_rule(node)
node.resolved_rules = node.resolved_rules.do_extend(@extends, @parent_directives)
end
end
7 changes: 4 additions & 3 deletions lib/sass/tree/visitors/to_css.rb
Expand Up @@ -68,13 +68,14 @@ def visit_comment(node)

def visit_directive(node)
was_in_directive = @in_directive
return node.resolved_value + ";" unless node.has_children
return node.resolved_value + " {}" if node.children.empty?
tab_str = ' ' * @tabs
return tab_str + node.resolved_value + ";" unless node.has_children
return tab_str + node.resolved_value + " {}" if node.children.empty?
@in_directive = @in_directive || !node.is_a?(Sass::Tree::MediaNode)
result = if node.style == :compressed
"#{node.resolved_value}{"
else
"#{' ' * @tabs}#{node.resolved_value} {" + (node.style == :compact ? ' ' : "\n")
"#{tab_str}#{node.resolved_value} {" + (node.style == :compact ? ' ' : "\n")
end
was_prop = false
first = true
Expand Down
17 changes: 17 additions & 0 deletions lib/sass/util.rb
Expand Up @@ -276,6 +276,23 @@ def check_range(name, range, value, unit='')
"#{name} #{str} must be between #{range.first}#{unit} and #{range.last}#{unit}")
end

# Returns whether or not `seq1` is a subsequence of `seq2`. That is, whether
# or not `seq2` contains every element in `seq1` in the same order (and
# possibly more elements besides).
#
# @param seq1 [Array]
# @param seq2 [Array]
# @return [Boolean]
def subsequence?(seq1, seq2)
i = j = 0
loop do
return true if i == seq1.size
return false if j == seq2.size
i += 1 if seq1[i] == seq2[j]
j += 1
end
end

# Returns information about the caller of the previous method.
#
# @param entry [String] An entry in the `#caller` list, or a similarly formatted string
Expand Down
18 changes: 16 additions & 2 deletions test/sass/engine_test.rb
Expand Up @@ -146,8 +146,6 @@ class SassEngineTest < Test::Unit::TestCase
"$var: true\n@while $var\n @extend .bar\n $var: false" => ["Extend directives may only be used within rules.", 3],
"@for $i from 0 to 1\n @extend .bar" => ["Extend directives may only be used within rules.", 2],
"@mixin foo\n @extend .bar\n@include foo" => ["Extend directives may only be used within rules.", 2],
"@media screen\n .bar\n @extend .foo" => "@extend may not be used within directives (e.g. @media).\n\nThis will only work once @extend is supported natively in the browser.",
"@flooblehoof\n .bar\n @extend .foo" => "@extend may not be used within directives (e.g. @flooblehoof).\n\nThis will only work once @extend is supported natively in the browser.",
"foo\n &a\n b: c" => ["Invalid CSS after \"&\": expected \"{\", was \"a\"\n\n\"a\" may only be used at the beginning of a selector.", 2],
"foo\n &1\n b: c" => ["Invalid CSS after \"&\": expected \"{\", was \"1\"\n\n\"1\" may only be used at the beginning of a selector.", 2],
"foo %\n a: b" => ['Invalid CSS after "foo %": expected placeholder name, was ""', 1],
Expand Down Expand Up @@ -2724,6 +2722,22 @@ def test_comment_like_selector
SASS
end

def test_nested_empty_directive
assert_equal <<CSS, render(<<SASS)
@media screen {
.foo {
a: b; }
@unknown-directive; }
CSS
@media screen
.foo
a: b
@unknown-directive
SASS
end

# Encodings

unless Sass::Util.ruby1_8?
Expand Down

0 comments on commit e215507

Please sign in to comment.