Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge branch 'stable'

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...
commit e215507545439a84c2b2bc01d454ebab722fe209 2 parents ba206d6 + 51f5ffd
@nex3 nex3 authored
View
2  doc-src/SASS_CHANGELOG.md
@@ -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
View
38 doc-src/SASS_REFERENCE.md
@@ -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.
View
1  lib/sass/engine.rb
@@ -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'
View
6 lib/sass/selector/comma_sequence.rb
@@ -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.
#
View
8 lib/sass/selector/sequence.rb
@@ -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
View
26 lib/sass/selector/simple_sequence.rb
@@ -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
@@ -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
View
22 lib/sass/tree/node.rb
@@ -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.
#
View
2  lib/sass/tree/root_node.rb
@@ -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
View
9 lib/sass/tree/rule_node.rb
@@ -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).
View
8 lib/sass/tree/visitors/check_nesting.rb
@@ -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)
View
9 lib/sass/tree/visitors/cssize.rb
@@ -12,6 +12,7 @@ def self.visit(root); super; end
attr_reader :parent
def initialize
+ @parent_directives = []
@extends = Sass::Util::SubsetMap.new
end
@@ -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
@@ -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)
@@ -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
View
42 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
View
7 lib/sass/tree/visitors/to_css.rb
@@ -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
View
17 lib/sass/util.rb
@@ -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
View
18 test/sass/engine_test.rb
@@ -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],
@@ -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?
View
213 test/sass/extend_test.rb
@@ -795,6 +795,219 @@ def test_media_in_placeholder_selector
SCSS
end
+ def test_extend_out_of_media
+ assert_warning(<<WARN) {assert_equal(<<CSS, render(<<SCSS))}
+DEPRECATION WARNING on line 3 of test_extend_out_of_media_inline.sass:
+ @extending an outer selector from within @media 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.
+WARN
+.foo {
+ a: b; }
+CSS
+.foo {a: b}
+@media screen {
+ .bar {@extend .foo}
+}
+SCSS
+ end
+
+ def test_extend_out_of_unknown_directive
+ assert_warning(<<WARN) {assert_equal(<<CSS, render(<<SCSS))}
+DEPRECATION WARNING on line 3 of test_extend_out_of_unknown_directive_inline.sass:
+ @extending an outer selector from within @flooblehoof 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.
+WARN
+.foo {
+ a: b; }
+
+@flooblehoof {}
+CSS
+.foo {a: b}
+@flooblehoof {
+ .bar {@extend .foo}
+}
+SCSS
+ end
+
+ def test_extend_out_of_nested_directives
+ assert_warning(<<WARN) {assert_equal(<<CSS, render(<<SCSS))}
+DEPRECATION WARNING on line 4 of test_extend_out_of_nested_directives_inline.sass:
+ @extending an outer selector from within @flooblehoof 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.
+WARN
+@media screen {
+ .foo {
+ a: b; }
+
+ @flooblehoof {} }
+CSS
+@media screen {
+ .foo {a: b}
+ @flooblehoof {
+ .bar {@extend .foo}
+ }
+}
+SCSS
+ end
+
+ def test_extend_within_media
+ assert_equal(<<CSS, render(<<SCSS))
+@media screen {
+ .foo, .bar {
+ a: b; } }
+CSS
+@media screen {
+ .foo {a: b}
+ .bar {@extend .foo}
+}
+SCSS
+ end
+
+ def test_extend_within_unknown_directive
+ assert_equal(<<CSS, render(<<SCSS))
+@flooblehoof {
+ .foo, .bar {
+ a: b; } }
+CSS
+@flooblehoof {
+ .foo {a: b}
+ .bar {@extend .foo}
+}
+SCSS
+ end
+
+ def test_extend_within_nested_directives
+ assert_equal(<<CSS, render(<<SCSS))
+@media screen {
+ @flooblehoof {
+ .foo, .bar {
+ a: b; } } }
+CSS
+@media screen {
+ @flooblehoof {
+ .foo {a: b}
+ .bar {@extend .foo}
+ }
+}
+SCSS
+ end
+
+ def test_extend_within_disparate_media
+ assert_equal(<<CSS, render(<<SCSS))
+@media screen {
+ .foo, .bar {
+ a: b; } }
+CSS
+@media screen {.foo {a: b}}
+@media screen {.bar {@extend .foo}}
+SCSS
+ end
+
+ def test_extend_within_disparate_unknown_directive
+ assert_equal(<<CSS, render(<<SCSS))
+@flooblehoof {
+ .foo, .bar {
+ a: b; } }
+
+@flooblehoof {}
+CSS
+@flooblehoof {.foo {a: b}}
+@flooblehoof {.bar {@extend .foo}}
+SCSS
+ end
+
+ def test_extend_within_disparate_nested_directives
+ assert_equal(<<CSS, render(<<SCSS))
+@media screen {
+ @flooblehoof {
+ .foo, .bar {
+ a: b; } } }
+@media screen {
+ @flooblehoof {} }
+CSS
+@media screen {@flooblehoof {.foo {a: b}}}
+@media screen {@flooblehoof {.bar {@extend .foo}}}
+SCSS
+ end
+
+ def test_extend_within_and_without_media
+ assert_warning(<<WARN) {assert_equal(<<CSS, render(<<SCSS))}
+DEPRECATION WARNING on line 4 of test_extend_within_and_without_media_inline.sass:
+ @extending an outer selector from within @media 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.
+WARN
+.foo {
+ a: b; }
+
+@media screen {
+ .foo, .bar {
+ c: d; } }
+CSS
+.foo {a: b}
+@media screen {
+ .foo {c: d}
+ .bar {@extend .foo}
+}
+SCSS
+ end
+
+ def test_extend_within_and_without_unknown_directive
+ assert_warning(<<WARN) {assert_equal(<<CSS, render(<<SCSS))}
+DEPRECATION WARNING on line 4 of test_extend_within_and_without_unknown_directive_inline.sass:
+ @extending an outer selector from within @flooblehoof 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.
+WARN
+.foo {
+ a: b; }
+
+@flooblehoof {
+ .foo, .bar {
+ c: d; } }
+CSS
+.foo {a: b}
+@flooblehoof {
+ .foo {c: d}
+ .bar {@extend .foo}
+}
+SCSS
+ end
+
+ def test_extend_within_and_without_nested_directives
+ assert_warning(<<WARN) {assert_equal(<<CSS, render(<<SCSS))}
+DEPRECATION WARNING on line 5 of test_extend_within_and_without_nested_directives_inline.sass:
+ @extending an outer selector from within @flooblehoof 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.
+WARN
+@media screen {
+ .foo {
+ a: b; }
+
+ @flooblehoof {
+ .foo, .bar {
+ c: d; } } }
+CSS
+@media screen {
+ .foo {a: b}
+ @flooblehoof {
+ .foo {c: d}
+ .bar {@extend .foo}
+ }
+}
+SCSS
+ end
+
# Regression Tests
def test_newline_near_combinator
View
10 test/sass/util_test.rb
@@ -127,6 +127,16 @@ def test_group_by_to_a
group_by_to_a(1..12) {|i| i % 3})
end
+ def test_subsequence
+ assert(subsequence?([1, 2, 3], [1, 2, 3]))
+ assert(subsequence?([1, 2, 3], [1, :a, 2, :b, 3]))
+ assert(subsequence?([1, 2, 3], [:a, 1, :b, :c, 2, :d, 3, :e, :f]))
+
+ assert(!subsequence?([1, 2, 3], [1, 2]))
+ assert(!subsequence?([1, 2, 3], [1, 3, 2]))
+ assert(!subsequence?([1, 2, 3], [3, 2, 1]))
+ end
+
def test_silence_warnings
old_stderr, $stderr = $stderr, StringIO.new
warn "Out"
Please sign in to comment.
Something went wrong with that request. Please try again.