diff --git a/cssselect/parser.py b/cssselect/parser.py index 7125030..5494bd4 100644 --- a/cssselect/parser.py +++ b/cssselect/parser.py @@ -250,6 +250,30 @@ def specificity(self): return a1 + a2, b1 + b2, c1 + c2 +class Matching(object): + """ + Represents selector:is(selector_list) + """ + def __init__(self, selector, selector_list): + self.selector = selector + self.selector_list = selector_list + + def __repr__(self): + return '%s[%r:is(%s)]' % ( + self.__class__.__name__, self.selector, ", ".join( + map(repr, self.selector_list))) + + def canonical(self): + selector_arguments = [] + for s in self.selector_list: + selarg = s.canonical() + selector_arguments.append(selarg.lstrip('*')) + return '%s:is(%s)' % (self.selector.canonical(), + ", ".join(map(str, selector_arguments))) + + def specificity(self): + return max([x.specificity() for x in self.selector_list]) + class Attrib(object): """ Represents selector[namespace|attrib operator value] @@ -432,6 +456,7 @@ def parse_selector_group(stream): else: break + def parse_selector(stream): result, pseudo_element = parse_simple_selector(stream) while 1: @@ -538,6 +563,9 @@ def parse_simple_selector(stream, inside_negation=False): if next != ('DELIM', ')'): raise SelectorSyntaxError("Expected ')', got %s" % (next,)) result = Negation(result, argument) + elif ident.lower() in ('matches', 'is'): + selectors = parse_simple_selector_arguments(stream) + result = Matching(result, selectors) else: result = Function(result, ident, parse_arguments(stream)) else: @@ -564,6 +592,29 @@ def parse_arguments(stream): "Expected an argument, got %s" % (next,)) +def parse_simple_selector_arguments(stream): + arguments = [] + while 1: + result, pseudo_element = parse_simple_selector(stream, True) + if pseudo_element: + raise SelectorSyntaxError( + 'Got pseudo-element ::%s inside function' + % (pseudo_element, )) + stream.skip_whitespace() + next = stream.next() + if next in (('EOF', None), ('DELIM', ',')): + stream.next() + stream.skip_whitespace() + arguments.append(result) + elif next == ('DELIM', ')'): + arguments.append(result) + break + else: + raise SelectorSyntaxError( + "Expected an argument, got %s" % (next,)) + return arguments + + def parse_attrib(selector, stream): stream.skip_whitespace() attrib = stream.next_ident_or_star() diff --git a/cssselect/xpath.py b/cssselect/xpath.py index a8722bb..db44d42 100644 --- a/cssselect/xpath.py +++ b/cssselect/xpath.py @@ -54,9 +54,9 @@ def __str__(self): def __repr__(self): return '%s[%s]' % (self.__class__.__name__, self) - def add_condition(self, condition): + def add_condition(self, condition, conjuction='and'): if self.condition: - self.condition = '(%s) and (%s)' % (self.condition, condition) + self.condition = '(%s) %s (%s)' % (self.condition, conjuction, condition) else: self.condition = condition return self @@ -272,6 +272,15 @@ def xpath_negation(self, negation): else: return xpath.add_condition('0') + def xpath_matching(self, matching): + xpath = self.xpath(matching.selector) + exprs = [self.xpath(selector) for selector in matching.selector_list] + for e in exprs: + e.add_name_test() + if e.condition: + xpath.add_condition(e.condition, 'or') + return xpath + def xpath_function(self, function): """Translate a functional pseudo-class.""" method = 'xpath_%s_function' % function.name.replace('-', '_') diff --git a/tests/test_cssselect.py b/tests/test_cssselect.py index d6969f2..bd37875 100644 --- a/tests/test_cssselect.py +++ b/tests/test_cssselect.py @@ -145,6 +145,10 @@ def parse_many(first, *others): 'Hash[Element[div]#foobar]'] assert parse_many('div:not(div.foo)') == [ 'Negation[Element[div]:not(Class[Element[div].foo])]'] + assert parse_many('div:is(.foo, #bar)') == [ + 'Matching[Element[div]:is(Class[Element[*].foo], Hash[Element[*]#bar])]'] + assert parse_many(':is(:hover, :visited)') == [ + 'Matching[Element[*]:is(Pseudo[Element[*]:hover], Pseudo[Element[*]:visited])]'] assert parse_many('td ~ th') == [ 'CombinedSelector[Element[td] ~ Element[th]]'] assert parse_many(':scope > foo') == [ @@ -266,6 +270,9 @@ def specificity(css): assert specificity(':not(:empty)') == (0, 1, 0) assert specificity(':not(#foo)') == (1, 0, 0) + assert specificity(':is(.foo, #bar)') == (1, 0, 0) + assert specificity(':is(:hover, :visited)') == (0, 1, 0) + assert specificity('foo:empty') == (0, 1, 1) assert specificity('foo:before') == (0, 0, 2) assert specificity('foo::before') == (0, 0, 2) @@ -300,6 +307,8 @@ def css2css(css, res=None): css2css(':not(*[foo])', ':not([foo])') css2css(':not(:empty)') css2css(':not(#foo)') + css2css(':is(#bar, .foo)') + css2css(':is(:focused, :visited)') css2css('foo:empty') css2css('foo::before') css2css('foo:empty::before') @@ -373,6 +382,10 @@ def get_error(css): "Got pseudo-element ::before inside :not() at 12") assert get_error(':not(:not(a))') == ( "Got nested :not()") + assert get_error(':is(:before)') == ( + "Got pseudo-element ::before inside function") + assert get_error(':is(a b)') == ( + "Expected an argument, got ") assert get_error(':scope > div :scope header') == ( 'Got immediate child pseudo-element ":scope" not at the start of a selector' ) @@ -863,6 +876,12 @@ def pcss(main, *selectors, **kwargs): assert pcss('ol :Not(li[class])') == [ 'first-li', 'second-li', 'li-div', 'fifth-li', 'sixth-li', 'seventh-li'] + assert pcss(':is(#first-li, #second-li)') == [ + 'first-li', 'second-li'] + assert pcss('a:is(#name-anchor, #tag-anchor)') == [ + 'name-anchor', 'tag-anchor'] + assert pcss(':is(.c)') == [ + 'first-ol', 'third-li', 'fourth-li'] assert pcss('ol.a.b.c > li.c:nth-child(3)') == ['third-li'] # Invalid characters in XPath element names, should not crash