Skip to content

Commit

Permalink
Merge branch 'placeholder'
Browse files Browse the repository at this point in the history
  • Loading branch information
nex3 committed Jan 4, 2012
2 parents 1a3f69b + 835ccf8 commit 70a3b0e
Show file tree
Hide file tree
Showing 11 changed files with 150 additions and 6 deletions.
1 change: 1 addition & 0 deletions lib/sass/scss/css_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def parse_selector_string

private

def placeholder_selector; nil; end
def parent_selector; nil; end
def interpolation; nil; end
def interp_string; tok(STRING); end
Expand Down
14 changes: 11 additions & 3 deletions lib/sass/scss/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -509,12 +509,14 @@ def combinator
def simple_selector_sequence
# This allows for stuff like http://www.w3.org/TR/css3-animations/#keyframes-
return expr unless e = element_name || id_selector || class_selector ||
attrib || negation || pseudo || parent_selector || interpolation_selector
placeholder_selector || attrib || negation || pseudo || parent_selector ||
interpolation_selector
res = [e]

# The tok(/\*/) allows the "E*" hack
while v = id_selector || class_selector || attrib || negation || pseudo ||
interpolation_selector || (tok(/\*/) && Selector::Universal.new(nil))
while v = id_selector || class_selector || placeholder_selector || attrib ||
negation || pseudo || interpolation_selector ||
(tok(/\*/) && Selector::Universal.new(nil))
res << v
end

Expand Down Expand Up @@ -554,6 +556,12 @@ def id_selector
Selector::Id.new(merge(expr!(:interp_name)))
end

def placeholder_selector
return unless tok(/%/)
@expected = "placeholder name"
Selector::Placeholder.new(merge(expr!(:interp_ident)))
end

def element_name
return unless name = interp_ident || tok(/\*/) || (tok?(/\|/) && "")
if tok(/\|/)
Expand Down
21 changes: 21 additions & 0 deletions lib/sass/selector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,27 @@ def unify(sels)
end
end

# A placeholder selector (e.g. `%foo`).
# This exists to be replaced via `@extend`.
# Rulesets using this selector will not be printed, but can be extended.
# Otherwise, this acts just like a class selector.
class Placeholder < Simple
# The placeholder name.
#
# @return [Array<String, Sass::Script::Node>]
attr_reader :name

# @param name [Array<String, Sass::Script::Node>] The placeholder name
def initialize(name)
@name = name
end

# @see Selector#to_a
def to_a
["%", *@name]
end
end

# A universal selector (`*` in CSS).
class Universal < Simple
# The selector namespace.
Expand Down
7 changes: 7 additions & 0 deletions lib/sass/selector/abstract_sequence.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ def eql?(other)
other.class == self.class && other.hash == self.hash && _eql?(other)
end
alias_method :==, :eql?

# Whether or not this selector sequence contains a placeholder selector.
# Checks recursively.
def has_placeholder?
@has_placeholder ||=
members.any? {|m| m.is_a?(AbstractSequence) ? m.has_placeholder? : m.is_a?(Placeholder)}
end
end
end
end
5 changes: 5 additions & 0 deletions lib/sass/tree/rule_node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ def debug_info
:line => self.line}
end

# A rule node is invisible if it has only placeholder selectors.
def invisible?
resolved_rules.members.all? {|seq| seq.has_placeholder?}
end

private

def try_to_parse_non_interpolated_rules
Expand Down
2 changes: 2 additions & 0 deletions lib/sass/tree/visitors/cssize.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ def visit_extend(node)
sseq = seq.members.first
if !sseq.is_a?(Sass::Selector::SimpleSequence)
raise Sass::SyntaxError.new("Can't extend #{seq.to_a.join}: invalid selector")
elsif sseq.members.any? {|ss| ss.is_a?(Sass::Selector::Parent)}
raise Sass::SyntaxError.new("Can't extend #{seq.to_a.join}: can't extend parent selectors")
end

sel = sseq.members
Expand Down
2 changes: 1 addition & 1 deletion lib/sass/tree/visitors/perform.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def visit_each(node)
# Runs SassScript interpolation in the selector,
# and then parses the result into a {Sass::Selector::CommaSequence}.
def visit_extend(node)
parser = Sass::SCSS::CssParser.new(run_interp(node.selector), node.filename, node.line)
parser = Sass::SCSS::StaticParser.new(run_interp(node.selector), node.filename, node.line)
node.resolved_selector = parser.parse_selector
node
end
Expand Down
3 changes: 2 additions & 1 deletion lib/sass/tree/visitors/to_css.rb
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,11 @@ def visit_rule(node)
per_rule_indent, total_indent = [:nested, :expanded].include?(node.style) ? [rule_indent, ''] : ['', rule_indent]

joined_rules = node.resolved_rules.members.map do |seq|
next if seq.has_placeholder?
rule_part = seq.to_a.join
rule_part.gsub!(/\s*([^,])\s*\n\s*/m, '\1 ') if node.style == :compressed
rule_part
end.join(rule_separator)
end.compact.join(rule_separator)

joined_rules.sub!(/\A\s*/, per_rule_indent)
joined_rules.gsub!(/\s*\n\s*/, "#{line_separator}#{per_rule_indent}")
Expand Down
30 changes: 30 additions & 0 deletions test/sass/conversion_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1177,6 +1177,36 @@ def test_empty_content
SCSS
end

def test_placeholder_conversion
assert_renders(<<SASS, <<SCSS)
#content a%foo.bar
color: blue
SASS
#content a%foo.bar {
color: blue; }
SCSS
end

def test_placeholder_interoplation_conversion
assert_renders(<<SASS, <<SCSS)
$foo: foo
%\#{$foo}
color: blue
.bar
@extend %foo
SASS
$foo: foo;
%\#{$foo} {
color: blue; }
.bar {
@extend %foo; }
SCSS
end

private

def assert_sass_to_sass(sass, options = {})
Expand Down
6 changes: 5 additions & 1 deletion test/sass/engine_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ class SassEngineTest < Test::Unit::TestCase
"a\n b: c;" => 'Invalid CSS after "c": expected expression (e.g. 1px, bold), was ";"',
".foo ^bar\n a: b" => ['Invalid CSS after ".foo ": expected selector, was "^bar"', 1],
"a\n @extend .foo ^bar" => 'Invalid CSS after ".foo ": expected selector, was "^bar"',
"a\n @extend .foo .bar" => "Can't extend .foo .bar: can't extend nested selectors",
"a\n @extend >" => "Can't extend >: invalid selector",
"a\n @extend &.foo" => "Can't extend &.foo: can't extend parent selectors",
"a: b" => 'Properties are only allowed within rules, directives, mixin includes, or other properties.',
":a b" => 'Properties are only allowed within rules, directives, mixin includes, or other properties.',
"$" => 'Invalid variable: "$".',
Expand All @@ -64,7 +67,7 @@ class SassEngineTest < Test::Unit::TestCase
"@mixin foo\n @import foo" => "Import directives may not be used within control directives or mixins.",
"@import foo;" => "Invalid @import: expected end of line, was \";\".",
'$foo: "bar" "baz" !' => %Q{Invalid CSS after ""bar" "baz" ": expected expression (e.g. 1px, bold), was "!"},
'$foo: "bar" "baz" $' => %Q{Invalid CSS after ""bar" "baz" ": expected expression (e.g. 1px, bold), was "$"},
'$foo: "bar" "baz" $' => %Q{Invalid CSS after ""bar" "baz" ": expected expression (e.g. 1px, bold), was "$"}, #'
"=foo\n :color red\n.bar\n +bang" => "Undefined mixin 'bang'.",
"=foo\n :color red\n.bar\n +bang_bop" => "Undefined mixin 'bang_bop'.",
"=foo\n :color red\n.bar\n +bang-bop" => "Undefined mixin 'bang-bop'.",
Expand Down Expand Up @@ -141,6 +144,7 @@ class SassEngineTest < Test::Unit::TestCase
"@mixin foo\n @extend .bar\n@include foo" => ["Extend directives may only be used within rules.", 2],
"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],
"=foo\n @content error" => "Invalid content directive. Trailing characters found: \"error\".",
"=foo\n @content\n b: c" => "Illegal nesting: Nothing may be nested beneath @content directives.",
"@content" => '@content may only be used within a mixin.',
Expand Down
65 changes: 65 additions & 0 deletions test/sass/extend_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1339,6 +1339,71 @@ def test_control_flow_while
SCSS
end

def test_basic_placeholder_selector
assert_equal <<CSS, render(<<SCSS)
.bar {
color: blue; }
CSS
%foo {color: blue}
.bar {@extend %foo}
SCSS
end

def test_unused_placeholder_selector
assert_equal <<CSS, render(<<SCSS)
.baz {
color: blue; }
CSS
%foo {color: blue}
%bar {color: red}
.baz {@extend %foo}
SCSS
end

def test_placeholder_descendant_selector
assert_equal <<CSS, render(<<SCSS)
#context .bar a {
color: blue; }
CSS
#context %foo a {color: blue}
.bar {@extend %foo}
SCSS
end

def test_placeholder_selector_with_multiple_extenders
assert_equal <<CSS, render(<<SCSS)
.bar, .baz {
color: blue; }
CSS
%foo {color: blue}
.bar {@extend %foo}
.baz {@extend %foo}
SCSS
end

def test_placeholder_selector_as_modifier
assert_equal <<CSS, render(<<SCSS)
a.baz.bar {
color: blue; }
CSS
a%foo.baz {color: blue}
.bar {@extend %foo}
div {@extend %foo}
SCSS
end

def test_placeholder_interpolation
assert_equal <<CSS, render(<<SCSS)
.bar {
color: blue; }
CSS
$foo: foo;
%\#{$foo} {color: blue}
.bar {@extend %foo}
SCSS
end

private

def render(sass, options = {})
Expand Down

0 comments on commit 70a3b0e

Please sign in to comment.