blob: 093c637b6fbed1249363606434d7a1c4bed28979 [file] [log] [blame]
module Sass
module Selector
# A comma-separated sequence of selectors.
class CommaSequence < AbstractSequence
@@compound_extend_deprecation = Sass::Deprecation.new
# The comma-separated selector sequences
# represented by this class.
#
# @return [Array<Sequence>]
attr_reader :members
# @param seqs [Array<Sequence>] See \{#members}
def initialize(seqs)
@members = seqs
end
# Resolves the {Parent} selectors within this selector
# by replacing them with the given parent selector,
# handling commas appropriately.
#
# @param super_cseq [CommaSequence] The parent selector
# @param implicit_parent [Boolean] Whether the the parent
# selector should automatically be prepended to the resolved
# selector if it contains no parent refs.
# @return [CommaSequence] This selector, with parent references resolved
# @raise [Sass::SyntaxError] If a parent selector is invalid
def resolve_parent_refs(super_cseq, implicit_parent = true)
if super_cseq.nil?
if contains_parent_ref?
raise Sass::SyntaxError.new(
"Base-level rules cannot contain the parent-selector-referencing character '&'.")
end
return self
end
CommaSequence.new(Sass::Util.flatten_vertically(@members.map do |seq|
seq.resolve_parent_refs(super_cseq, implicit_parent).members
end))
end
# Returns whether there's a {Parent} selector anywhere in this sequence.
#
# @return [Boolean]
def contains_parent_ref?
@members.any? {|sel| sel.contains_parent_ref?}
end
# Non-destrucively extends this selector with the extensions specified in a hash
# (which should come from {Sass::Tree::Visitors::Cssize}).
#
# @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 selector
# @param parent_directives [Array<Sass::Tree::DirectiveNode>]
# The directives containing this selector.
# @param replace [Boolean]
# Whether to replace the original selector entirely or include
# it in the result.
# @param seen [Set<Array<Selector::Simple>>]
# The set of simple sequences that are currently being replaced.
# @param original [Boolean]
# Whether this is the original selector being extended, as opposed to
# the result of a previous extension that's being re-extended.
# @return [CommaSequence] A copy of this selector,
# with extensions made according to `extends`
def do_extend(extends, parent_directives = [], replace = false, seen = Set.new,
original = true)
CommaSequence.new(members.map do |seq|
seq.do_extend(extends, parent_directives, replace, seen, original)
end.flatten)
end
# Returns whether or not this selector matches all elements
# that the given selector matches (as well as possibly more).
#
# @example
# (.foo).superselector?(.foo.bar) #=> true
# (.foo).superselector?(.bar) #=> false
# @param cseq [CommaSequence]
# @return [Boolean]
def superselector?(cseq)
cseq.members.all? {|seq1| members.any? {|seq2| seq2.superselector?(seq1)}}
end
# Populates a subset map that can then be used to extend
# selectors. This registers an extension with this selector as
# the extender and `extendee` as the extendee.
#
# @param extends [Sass::Util::SubsetMap{Selector::Simple =>
# Sass::Tree::Visitors::Cssize::Extend}]
# The subset map representing the extensions to perform.
# @param extendee [CommaSequence] The selector being extended.
# @param extend_node [Sass::Tree::ExtendNode]
# The node that caused this extension.
# @param parent_directives [Array<Sass::Tree::DirectiveNode>]
# The parent directives containing `extend_node`.
# @param allow_compound_target [Boolean]
# Whether `extendee` is allowed to contain compound selectors.
# @raise [Sass::SyntaxError] if this extension is invalid.
def populate_extends(extends, extendee, extend_node = nil, parent_directives = [],
allow_compound_target = false)
extendee.members.each do |seq|
if seq.members.size > 1
raise Sass::SyntaxError.new("Can't extend #{seq}: can't extend nested selectors")
end
sseq = seq.members.first
if !sseq.is_a?(Sass::Selector::SimpleSequence)
raise Sass::SyntaxError.new("Can't extend #{seq}: invalid selector")
elsif sseq.members.any? {|ss| ss.is_a?(Sass::Selector::Parent)}
raise Sass::SyntaxError.new("Can't extend #{seq}: can't extend parent selectors")
end
sel = sseq.members
if !allow_compound_target && sel.length > 1
@@compound_extend_deprecation.warn(sseq.filename, sseq.line, <<WARNING)
Extending a compound selector, #{sseq}, is deprecated and will not be supported in a future release.
See https://github.com/sass/sass/issues/1599 for details.
WARNING
end
members.each do |member|
unless member.members.last.is_a?(Sass::Selector::SimpleSequence)
raise Sass::SyntaxError.new("#{member} can't extend: invalid selector")
end
extends[sel] = Sass::Tree::Visitors::Cssize::Extend.new(
member, sel, extend_node, parent_directives, false)
end
end
end
# Unifies this with another comma selector to produce a selector
# that matches (a subset of) the intersection of the two inputs.
#
# @param other [CommaSequence]
# @return [CommaSequence, nil] The unified selector, or nil if unification failed.
# @raise [Sass::SyntaxError] If this selector cannot be unified.
# This will only ever occur when a dynamic selector,
# such as {Parent} or {Interpolation}, is used in unification.
# Since these selectors should be resolved
# by the time extension and unification happen,
# this exception will only ever be raised as a result of programmer error
def unify(other)
results = members.map {|seq1| other.members.map {|seq2| seq1.unify(seq2)}}.flatten.compact
results.empty? ? nil : CommaSequence.new(results.map {|cseq| cseq.members}.flatten)
end
# Returns a SassScript representation of this selector.
#
# @return [Sass::Script::Value::List]
def to_sass_script
Sass::Script::Value::List.new(members.map do |seq|
Sass::Script::Value::List.new(seq.members.map do |component|
next if component == "\n"
Sass::Script::Value::String.new(component.to_s)
end.compact, separator: :space)
end, separator: :comma)
end
# Returns a string representation of the sequence.
# This is basically the selector string.
#
# @return [String]
def inspect
members.map {|m| m.inspect}.join(", ")
end
# @see AbstractSequence#to_s
def to_s(opts = {})
@members.map do |m|
next if opts[:placeholder] == false && m.invisible?
m.to_s(opts)
end.compact.
join(opts[:style] == :compressed ? "," : ", ").
gsub(", \n", ",\n")
end
private
def _hash
members.hash
end
def _eql?(other)
other.class == self.class && other.members.eql?(members)
end
end
end
end