Skip to content

Commit

Permalink
Get CSS4 selectors working to a degree.
Browse files Browse the repository at this point in the history
  • Loading branch information
nex3 committed Jun 23, 2012
1 parent fb917b1 commit afdf0e8
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 44 deletions.
93 changes: 65 additions & 28 deletions lib/sass/scss/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,7 @@ def has_children?(child_or_array)
end

# This is a nasty hack, and the only place in the parser
# that requires backtracking.
# that requires a large amount of backtracking.
# The reason is that we can't figure out if certain strings
# are declarations or rulesets with fixed finite lookahead.
# For example, "foo:bar baz baz baz..." could be either a property
Expand Down Expand Up @@ -578,17 +578,27 @@ def _selector
end

def combinator
tok(PLUS) || tok(GREATER) || tok(TILDE)
tok(PLUS) || tok(GREATER) || tok(TILDE) || reference_combinator
end

def reference_combinator
return unless tok(/\//)
res = ['/']
ns, name = expr!(:qualified_name)
res << ns << '|' if ns
res << name << tok!(/\//)
res.flatten
end

def simple_selector_sequence
# This allows for stuff like http://www.w3.org/TR/css3-animations/#keyframes-
# Returning expr by default 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
attrib || pseudo || parent_selector || interpolation_selector
res = [e]

# The tok(/\*/) allows the "E*" hack
while v = id_selector || class_selector || attrib || negation || pseudo ||
while v = id_selector || class_selector || attrib || pseudo ||
interpolation_selector || (tok(/\*/) && Selector::Universal.new(nil))
res << v
end
Expand All @@ -615,7 +625,7 @@ def simple_selector_sequence
end
end

Selector::SimpleSequence.new(res)
Selector::SimpleSequence.new(res, tok(/!/))
end

def parent_selector
Expand All @@ -636,12 +646,8 @@ def id_selector
end

def element_name
return unless name = interp_ident || tok(/\*/) || (tok?(/\|/) && "")
if tok(/\|/)
@expected = "element name or *"
ns = name
name = interp_ident || tok!(/\*/)
end
ns, name = qualified_name(:allow_star_name)
return unless ns || name

if name == '*'
Selector::Universal.new(merge(ns))
Expand All @@ -650,6 +656,15 @@ def element_name
end
end

def qualified_name(allow_star_name=false)
return unless name = interp_ident || tok(/\*/) || (tok?(/\|/) && "")
return nil, name unless tok(/\|/)

return name, expr!(:interp_ident) unless allow_star_name
@expected = "identifier or *"
return name, interp_ident || tok!(/\*/)
end

def interpolation_selector
return unless script = interpolation
Selector::Interpolation.new(script)
Expand All @@ -672,9 +687,10 @@ def attrib
val = interp_ident || expr!(:interp_string)
ss
end
flags = interp_ident || interp_string
tok!(/\]/)

Selector::Attribute.new(merge(name), merge(ns), op, merge(val))
Selector::Attribute.new(merge(name), merge(ns), op, merge(val), merge(flags))
end

def attrib_name!
Expand All @@ -701,32 +717,53 @@ def pseudo
name = expr!(:interp_ident)
if tok(/\(/)
ss
arg = expr!(:pseudo_expr)
arg = expr!(:pseudo_arg)
while tok(/,/)
arg << ',' << str{ss}
arg.concat expr!(:pseudo_arg)
end
tok!(/\)/)
end
Selector::Pseudo.new(s == ':' ? :class : :element, merge(name), merge(arg))
end

def pseudo_arg
# In the CSS spec, every pseudo-class/element either takes a pseudo
# expression or a selector comma sequence as an argument. However, we
# don't want to have to know which takes which, so we handle both at
# once.
#
# However, there are some ambiguities between the two. For instance, "n"
# could start a pseudo expression like "n+1", or it could start a
# selector like "n|m". In order to handle this, we must regrettably
# backtrack.
expr, sel = nil
pseudo_err = catch_error do
expr = pseudo_expr
next if tok?(/[,)]/)
expr = nil
expected '")"'
end

return expr if expr
sel_err = catch_error {sel = selector}
return sel if sel
rethrow pseudo_err if pseudo_err
rethrow sel_err if sel_err
return
end

def pseudo_expr
return unless e = tok(PLUS) || tok(/-/) || tok(NUMBER) ||
return unless e = tok(PLUS) || tok(/[-*]/) || tok(NUMBER) ||
interp_string || tok(IDENT) || interpolation
res = [e, str{ss}]
while e = tok(PLUS) || tok(/-/) || tok(NUMBER) ||
while e = tok(PLUS) || tok(/[-*]/) || tok(NUMBER) ||
interp_string || tok(IDENT) || interpolation
res << e << str{ss}
end
res
end

def negation
return unless name = tok(NOT) || tok(MOZ_ANY)
ss
@expected = "selector"
sel = selector_comma_sequence
tok!(/\)/)
Selector::SelectorPseudoClass.new(name[1...-1], sel)
end

def declaration
# This allows the "*prop: val", ":prop: val", and ".prop: val" hacks
if s = tok(/[:\*\.]|\#(?!\{)/)
Expand Down Expand Up @@ -912,12 +949,12 @@ def merge(arr)
EXPR_NAMES = {
:media_query => "media query (e.g. print, screen, print and screen)",
:media_expr => "media expression (e.g. (min-device-width: 800px)))",
:pseudo_expr => "expression (e.g. fr, 2n+1)",
:pseudo_arg => "expression (e.g. fr, 2n+1)",
:interp_ident => "identifier",
:interp_name => "identifier",
:qualified_name => "identifier",
:expr => "expression (e.g. 1px, bold)",
:_selector => "selector",
:selector_comma_sequence => "selector",
:simple_selector_sequence => "selector",
:import_arg => "file to import (string or url())",
:moz_document_function => "matching function (e.g. url-prefix(), domain())",
Expand Down Expand Up @@ -973,7 +1010,7 @@ def catch_error(&block)
pos = @scanner.pos
line = @line
expected = @expected
if catch(:_sass_parser_error, &block)
if catch(:_sass_parser_error) {yield; false}
@scanner.pos = pos
@line = line
@expected = expected
Expand Down
10 changes: 9 additions & 1 deletion lib/sass/selector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -256,15 +256,22 @@ class Attribute < Simple
# @return [Array<String, Sass::Script::Node>]
attr_reader :value

# Flags for the attribute selector (e.g. `i`).
#
# @return [Array<String, Sass::Script::Node>]
attr_reader :flags

# @param name [Array<String, Sass::Script::Node>] The attribute name
# @param namespace [Array<String, Sass::Script::Node>, nil] See \{#namespace}
# @param operator [String] The matching operator, e.g. `"="` or `"^="`
# @param value [Array<String, Sass::Script::Node>] See \{#value}
def initialize(name, namespace, operator, value)
# @param value [Array<String, Sass::Script::Node>] See \{#flags}
def initialize(name, namespace, operator, value, flags)
@name = name
@namespace = namespace
@operator = operator
@value = value
@flags = flags
end

# @see Selector#to_a
Expand All @@ -273,6 +280,7 @@ def to_a
res.concat(@namespace) << "|" if @namespace
res.concat @name
(res << @operator).concat @value if @value
(res << " ").concat @flags if @flags
res << "]"
end
end
Expand Down
17 changes: 8 additions & 9 deletions lib/sass/selector/sequence.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,16 @@ def filename=(filename)
filename
end

# The array of {SimpleSequence simple selector sequences}, operators, and newlines.
# The operators are strings such as `"+"` and `">"`
# representing the corresponding CSS operators.
# Newlines are also newline strings;
# these aren't semantically relevant,
# but they do affect formatting.
# The array of {SimpleSequence simple selector sequences}, operators, and
# newlines. The operators are strings such as `"+"` and `">"` representing
# the corresponding CSS operators, or interpolated SassScript. Newlines
# are also newline strings; these aren't semantically relevant, but they
# do affect formatting.
#
# @return [Array<SimpleSequence, String>]
# @return [Array<SimpleSequence, String|Array<Sass::Tree::Node, String>>]
attr_reader :members

# @param seqs_and_ops [Array<SimpleSequence, String>] See \{#members}
# @param seqs_and_ops [Array<SimpleSequence, String|Array<Sass::Tree::Node, String>>] See \{#members}
def initialize(seqs_and_ops)
@members = seqs_and_ops
end
Expand All @@ -54,7 +53,7 @@ def resolve_parent_refs(super_seq)
end
members = []
members << nl if nl
members << SimpleSequence.new([Parent.new])
members << SimpleSequence.new([Parent.new], false)
members += @members
end

Expand Down
26 changes: 20 additions & 6 deletions lib/sass/selector/simple_sequence.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,20 @@ def rest
@rest ||= Set.new(base ? members[1..-1] : members)
end

# Whether or not this compound selector is the subject of the parent
# selector; that is, whether it is prepended with `$` and represents the
# actual element that will be selected.
#
# @return [Boolean]
def subject?
@subject
end

# @param selectors [Array<Simple>] See \{#members}
def initialize(selectors)
# @param subject [Boolean] See \{#subject?}
def initialize(selectors, subject)
@members = selectors
@subject = subject
end

# Resolves the {Parent} selectors within this selector
Expand All @@ -48,7 +59,7 @@ def resolve_parent_refs(super_seq)
end

super_seq.members[0...-1] +
[SimpleSequence.new(super_seq.members.last.members + @members[1..-1])]
[SimpleSequence.new(super_seq.members.last.members + @members[1..-1], subject?)]
end

# Non-destrucively extends this selector with the extensions specified in a hash
Expand All @@ -69,7 +80,7 @@ def do_extend(extends, parent_directives, seen = Set.new)
# ex.extender is A, sels is B, and self is C

self_without_sel = self.members - sels
next unless unified = ex.extender.members.last.unify(self_without_sel)
next unless unified = ex.extender.members.last.unify(self_without_sel, subject?)
next unless check_directives_match!(ex, parent_directives)
[sels, ex.extender.members[0...-1] + [unified]]
end.compact.map do |sels, seq|
Expand All @@ -84,6 +95,7 @@ def do_extend(extends, parent_directives, seen = Set.new)
# that matches both this selector and the input selector.
#
# @param sels [Array<Simple>] A {SimpleSequence}'s {SimpleSequence#members members array}
# @param subject [Boolean] Whether the {SimpleSequence} being merged is a subject.
# @return [SimpleSequence, nil] A {SimpleSequence} matching both `sels` and this selector,
# or `nil` if this is impossible (e.g. unifying `#foo` and `#bar`)
# @raise [Sass::SyntaxError] If this selector cannot be unified.
Expand All @@ -92,12 +104,12 @@ def do_extend(extends, parent_directives, seen = Set.new)
# 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(sels)
def unify(sels, other_subject)
return unless sseq = members.inject(sels) do |sseq, sel|
return unless sseq
sel.unify(sseq)
end
SimpleSequence.new(sseq)
SimpleSequence.new(sseq, other_subject || subject?)
end

# Returns whether or not this selector matches all elements
Expand All @@ -114,7 +126,9 @@ def superselector?(sseq)

# @see Simple#to_a
def to_a
@members.map {|sel| sel.to_a}.flatten
res = @members.map {|sel| sel.to_a}.flatten
res << '!' if subject?
res
end

# Returns a string representation of the sequence.
Expand Down

0 comments on commit afdf0e8

Please sign in to comment.