summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEevee (Alex Munroe) <eevee.git@veekun.com>2013-09-11 16:22:06 -0700
committerEevee (Alex Munroe) <eevee.git@veekun.com>2013-09-11 16:22:06 -0700
commit51075332edae1092ebb8fc8b98e567f1dd17e93b (patch)
tree6e785d75220cc9ac5912bcfa326724f5e03296ce
parent0d97a99616850ee177bdb6aefb73d80c81e451c1 (diff)
downloadpyscss-51075332edae1092ebb8fc8b98e567f1dd17e93b.tar.gz
Cleanup and documenting of scss.selector.
-rw-r--r--scss/selector.py260
1 files changed, 150 insertions, 110 deletions
diff --git a/scss/selector.py b/scss/selector.py
index 202c1ab..49f735e 100644
--- a/scss/selector.py
+++ b/scss/selector.py
@@ -63,6 +63,17 @@ TOKEN_SORT_KEY = lambda token: TOKEN_TYPE_ORDER.get(token[0], 0)
class SimpleSelector(object):
+ """A simple selector, by CSS 2.1 terminology: a combination of element
+ name, class selectors, id selectors, and other criteria that all apply to a
+ single element.
+
+ Note that CSS 3 considers EACH of those parts to be a "simple selector",
+ and calls a group of them a "sequence of simple selectors". That's a
+ terrible class name, so we're going with 2.1 here.
+
+ For lack of a better name, each of the individual parts is merely called a
+ "token".
+ """
def __init__(self, combinator, tokens):
self.combinator = combinator
# TODO enforce that only one element name (including *) appears in a
@@ -80,7 +91,9 @@ class SimpleSelector(object):
if not isinstance(other, SimpleSelector):
return NotImplemented
- return self.combinator == other.combinator and self.tokens == other.tokens
+ return (
+ self.combinator == other.combinator and
+ self.tokens == other.tokens)
@property
def has_parent_reference(self):
@@ -141,20 +154,37 @@ class SimpleSelector(object):
class Selector(object):
"""A single CSS selector."""
- def __init__(self, selector, tree):
- """Private; please use parse()."""
- self.original_selector = selector
- # TODO rename this
+ def __init__(self, simples):
+ """Return a selector containing a sequence of `SimpleSelector`s.
+
+ You probably want to use `parse_many` or `parse_one` instead.
+ """
# TODO enforce uniqueness
- self._tree = tuple(tree)
+ self.simple_selectors = tuple(simples)
@classmethod
def parse_many(cls, selector):
selector = selector.strip()
ret = []
- pending_tree = []
- pending_combinator = ' '
- pending_tokens = []
+
+ pending = dict(
+ simples=[],
+ combinator=' ',
+ tokens=[],
+ )
+
+ def promote_simple():
+ if pending['tokens']:
+ pending['simples'].append(
+ SimpleSelector(pending['combinator'], pending['tokens']))
+ pending['combinator'] = ' '
+ pending['tokens'] = []
+ def promote_selector():
+ promote_simple()
+ if pending['simples']:
+ ret.append(cls(pending['simples']))
+ pending['simples'] = []
+
pos = 0
while pos < len(selector):
@@ -174,42 +204,21 @@ class Selector(object):
if token == ',':
# End current selector
# TODO what about "+ ,"? what do i even do with that
- if pending_tokens:
- pending_tree.append(
- SimpleSelector(pending_combinator, pending_tokens))
- if pending_tree:
- ret.append(cls(selector, pending_tree))
- pending_tree = []
- pending_combinator = ' '
- pending_tokens = []
+ promote_selector()
elif token in ' +>~':
# End current simple selector
- if pending_tokens:
- pending_tree.append(
- SimpleSelector(pending_combinator, pending_tokens))
- pending_combinator = token
- pending_tokens = []
+ promote_simple()
+ pending['combinator'] = token
else:
# Add to pending simple selector
- pending_tokens.append(token)
-
+ pending['tokens'].append(token)
# Deal with any remaining pending bits
- # TODO reduce copy-paste yikes
- if pending_tokens:
- pending_tree.append(
- SimpleSelector(pending_combinator, pending_tokens))
- if pending_tree:
- ret.append(cls(selector, pending_tree))
+ promote_selector()
return ret
@classmethod
- def parse(cls, selector_string):
- # TODO remove me
- return cls.parse_many(selector_string)
-
- @classmethod
def parse_one(cls, selector_string):
selectors = cls.parse_many(selector_string)
if len(selectors) != 1:
@@ -222,49 +231,48 @@ class Selector(object):
return "<%s: %r>" % (type(self).__name__, self.render())
def __hash__(self):
- return hash(self._tree)
+ return hash(self.simple_selectors)
def __eq__(self, other):
if not isinstance(other, Selector):
return NotImplemented
- return self._tree == other._tree
+ return self.simple_selectors == other.simple_selectors
@property
def has_parent_reference(self):
return any(
simple.has_parent_reference
- for simple in self._tree)
+ for simple in self.simple_selectors)
@property
def has_placeholder(self):
return any(
simple.has_placeholder
- for simple in self._tree)
+ for simple in self.simple_selectors)
def with_parent(self, parent):
saw_parent_ref = False
- new_tree = []
- for simple in self._tree:
+ new_simples = []
+ for simple in self.simple_selectors:
if simple.has_parent_reference:
- new_tree.extend(simple.replace_parent(parent._tree))
+ new_simples.extend(simple.replace_parent(parent.simple_selectors))
saw_parent_ref = True
else:
- new_tree.append(simple)
+ new_simples.append(simple)
if not saw_parent_ref:
- new_tree = parent._tree + tuple(new_tree)
+ new_simples = parent.simple_selectors + tuple(new_simples)
- return type(self)("", new_tree)
+ return type(self)(new_simples)
def lookup_key(self):
"""Build a key from the "important" parts of a selector: elements,
classes, ids.
"""
- # TODO how does this work with multiple selectors
parts = set()
- for node in self._tree:
+ for node in self.simple_selectors:
for token in node.tokens:
if token[0] not in ':[':
parts.add(token)
@@ -281,12 +289,12 @@ class Selector(object):
assert isinstance(other, Selector)
idx = 0
- for other_node in other._tree:
- if idx >= len(self._tree):
+ for other_node in other.simple_selectors:
+ if idx >= len(self.simple_selectors):
return False
- while idx < len(self._tree):
- node = self._tree[idx]
+ while idx < len(self.simple_selectors):
+ node = self.simple_selectors[idx]
idx += 1
if node.is_superset_of(other_node):
@@ -307,22 +315,26 @@ class Selector(object):
selectors "inside" the target selector.
"""
- # Find the hinge in the parent selector, and split it into before/after
- p_before, p_extras, p_after = self.break_around(target._tree)
+ # Find the target in the parent selector, and split it into
+ # before/after
+ p_before, p_extras, p_after = self.break_around(target.simple_selectors)
# The replacement has no hinge; it only has the most specific simple
# selector (which is the part that replaces "self" in the parent) and
# whatever preceding simple selectors there may be
- r_trail = replacement._tree[:-1]
- r_extras = replacement._tree[-1]
+ r_trail = replacement.simple_selectors[:-1]
+ r_extras = replacement.simple_selectors[-1]
# TODO what if the prefix doesn't match? who wins? should we even get
# this far?
focal_nodes = (p_extras.merge_with(r_extras),)
- befores = self._merge_trails(p_before, r_trail)
+ befores = _merge_selectors(p_before, r_trail)
- return [Selector(None, before + focal_nodes + p_after) for before in befores]
+ cls = type(self)
+ return [
+ cls(before + focal_nodes + p_after)
+ for before in befores]
def break_around(self, hinge):
"""Given a simple selector node contained within this one (a "hinge"),
@@ -333,17 +345,17 @@ class Selector(object):
and B.
"""
hinge_start = hinge[0]
- for i, node in enumerate(self._tree):
+ for i, node in enumerate(self.simple_selectors):
# TODO does first combinator have to match? maybe only if the
# hinge has a non-descendant combinator?
if hinge_start.is_superset_of(node):
start_idx = i
break
else:
- raise ValueError("Couldn't find hinge %r in compound selector %r", (hinge_start, self._tree))
+ raise ValueError("Couldn't find hinge %r in compound selector %r", (hinge_start, self.simple_selectors))
for i, hinge_node in enumerate(hinge):
- self_node = self._tree[start_idx + i]
+ self_node = self.simple_selectors[start_idx + i]
if hinge_node.is_superset_of(self_node):
continue
@@ -351,65 +363,93 @@ class Selector(object):
raise TypeError("no match")
end_idx = start_idx + len(hinge) - 1
- focal_node = self._tree[end_idx]
+
+ focal_node = self.simple_selectors[end_idx]
extras = focal_node.difference(hinge[-1])
- return self._tree[:start_idx], extras, self._tree[end_idx + 1:]
-
- @staticmethod
- def _merge_trails(left, right):
- # XXX docs docs docs
-
- if not left or not right:
- # At least one is empty, so there are no conflicts; just
- # return whichever isn't empty
- return [left or right]
-
- lcs = longest_common_subsequence(left, right, _merge_selector_nodes)
-
- ret = [()]
- left_last = 0
- right_last = 0
- for left_next, right_next, merged in lcs:
- left_prefix = left[left_last:left_next]
- right_prefix = right[right_last:right_next]
-
- new_ret = [
- node + left_prefix + right_prefix + (merged,)
- for node in ret]
- if left_prefix and right_prefix:
- new_ret.extend(
- node + right_prefix + left_prefix + (merged,)
- for node in ret)
- ret = new_ret
-
- left_last = left_next + 1
- right_last = right_next + 1
-
- left_prefix = left[left_last:]
- right_prefix = right[right_last:]
- # TODO factor this out
- new_ret = [
- node + left_prefix + right_prefix
- for node in ret]
- if left_prefix and right_prefix:
- new_ret.extend(
- node + right_prefix + left_prefix
- for node in ret)
- ret = new_ret
- return ret
+ return (
+ self.simple_selectors[:start_idx],
+ extras,
+ self.simple_selectors[end_idx + 1:])
def render(self):
- return ' '.join(simple.render() for simple in self._tree).lstrip()
+ return ' '.join(simple.render() for simple in self.simple_selectors)
-def _merge_selector_nodes(a, b):
- # TODO document, turn me into a method on something
+def _merge_selectors(left, right):
+ """Given two selector chains (lists of simple selectors), return a list of
+ selector chains representing elements matched by both of them.
+
+ This operation is not exact, and involves some degree of fudging -- the
+ wackier and more divergent the input, the more fudging. It's meant to be
+ what a human might expect rather than a precise covering of all possible
+ cases. Most notably, when the two input chains have absolutely nothing in
+ common, the output is merely ``left + right`` and ``right + left`` rather
+ than all possible interleavings.
+ """
+
+ if not left or not right:
+ # At least one is empty, so there are no conflicts; just return
+ # whichever isn't empty. Remember to return a LIST, though
+ return [left or right]
+
+ lcs = longest_common_subsequence(left, right, _merge_simple_selectors)
+
+ ret = [()] # start with a dummy empty chain or weaving won't work
+
+ left_last = 0
+ right_last = 0
+ for left_next, right_next, merged in lcs:
+ ret = _weave_conflicting_selectors(
+ ret,
+ left[left_last:left_next],
+ right[right_last:right_next],
+ (merged,))
+
+ left_last = left_next + 1
+ right_last = right_next + 1
+
+ ret = _weave_conflicting_selectors(
+ ret,
+ left[left_last:],
+ right[right_last:])
+
+ return ret
+
+
+def _weave_conflicting_selectors(prefixes, a, b, suffix=()):
+ """Part of the selector merge algorithm above. Not useful on its own. Pay
+ no attention to the man behind the curtain.
+ """
+ # OK, what this actually does: given a list of selector chains, two
+ # "conflicting" selector chains, and an optional suffix, return a new list
+ # of chains like this:
+ # prefix[0] + a + b + suffix,
+ # prefix[0] + b + a + suffix,
+ # prefix[1] + a + b + suffix,
+ # ...
+ # In other words, this just appends a new chain to each of a list of given
+ # chains, except that the new chain might be the superposition of two
+ # other incompatible chains.
+ both = a and b
+ for prefix in prefixes:
+ yield prefix + a + b + suffix
+ if both:
+ # Only use both orderings if there's an actual conflict!
+ yield prefix + b + a + suffix
+
+
+def _merge_simple_selectors(a, b):
+ """Merge two simple selectors, for the purposes of the LCS algorithm below.
+
+ In practice this returns the more specific selector if one is a subset of
+ the other, else it returns None.
+ """
# TODO what about combinators
if a.is_superset_of(b):
- return a.merge_with(b)
+ return b
elif b.is_superset_of(a):
- return b.merge_with(a)
+ return a
else:
return None