From f7bff6e69491040d39ed570e9f7ec82ca4d30ee5 Mon Sep 17 00:00:00 2001 From: Kaj Date: Wed, 18 Mar 2020 11:20:23 +0300 Subject: [PATCH 1/6] Add :matches() pseudo-class --- cssselect/parser.py | 49 +++++++++++++++++++++++++++++++++++++++++++++ cssselect/xpath.py | 13 ++++++++++-- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/cssselect/parser.py b/cssselect/parser.py index 7125030..33a0cb9 100644 --- a/cssselect/parser.py +++ b/cssselect/parser.py @@ -250,6 +250,28 @@ 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): + subsel = self.subselector.canonical() + if len(subsel) > 1: + subsel = subsel.lstrip('*') + return '%s:not(%s)' % (self.selector.canonical(), subsel) + + def specificity(self): + return max([x.specificity() for x in self.selector_list]) + class Attrib(object): """ Represents selector[namespace|attrib operator value] @@ -432,6 +454,7 @@ def parse_selector_group(stream): else: break + def parse_selector(stream): result, pseudo_element = parse_simple_selector(stream) while 1: @@ -538,6 +561,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 +590,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 db50c77..5cf24cc 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('-', '_') From e688e85caa1b8ca0b08cee5c7ec72278b5221dfc Mon Sep 17 00:00:00 2001 From: Kaj Date: Sat, 21 Mar 2020 15:11:36 +0300 Subject: [PATCH 2/6] Fix typo in membership expression --- cssselect/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cssselect/parser.py b/cssselect/parser.py index 33a0cb9..a3e01d7 100644 --- a/cssselect/parser.py +++ b/cssselect/parser.py @@ -561,7 +561,7 @@ 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'): + elif ident.lower() in ('matches', 'is'): selectors = parse_simple_selector_arguments(stream) result = Matching(result, selectors) else: From 2e1e047c6b727fcd646eecdbdb5cf4d872746ca0 Mon Sep 17 00:00:00 2001 From: Kaj Date: Tue, 24 Mar 2020 07:24:01 +0300 Subject: [PATCH 3/6] Fix canonical function for Matching class --- cssselect/parser.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cssselect/parser.py b/cssselect/parser.py index a3e01d7..5494bd4 100644 --- a/cssselect/parser.py +++ b/cssselect/parser.py @@ -264,10 +264,12 @@ def __repr__(self): map(repr, self.selector_list))) def canonical(self): - subsel = self.subselector.canonical() - if len(subsel) > 1: - subsel = subsel.lstrip('*') - return '%s:not(%s)' % (self.selector.canonical(), subsel) + 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]) From 865d56dbbb52d1cbdae0d52e066fa5c271b26906 Mon Sep 17 00:00:00 2001 From: Kaj Date: Tue, 24 Mar 2020 07:25:08 +0300 Subject: [PATCH 4/6] Add tests for :is pseudo-class --- tests/test_cssselect.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_cssselect.py b/tests/test_cssselect.py index 320736c..ababfe6 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)', ':is(#bar, .foo)') + css2css(':is(:focused, :visited)') css2css('foo:empty') css2css('foo::before') css2css('foo:empty::before') @@ -839,6 +848,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 From ba24c559dbcf3cc295fbaf70b8a2a0a6bcc798b2 Mon Sep 17 00:00:00 2001 From: Kaj Date: Tue, 24 Mar 2020 08:02:05 +0300 Subject: [PATCH 5/6] Add parse_error tests for :is --- tests/test_cssselect.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_cssselect.py b/tests/test_cssselect.py index ababfe6..44b4768 100644 --- a/tests/test_cssselect.py +++ b/tests/test_cssselect.py @@ -382,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' ) From 433773a3b46b6480449b43c115d57f08e4c80962 Mon Sep 17 00:00:00 2001 From: Kaj Date: Tue, 24 Mar 2020 11:17:40 +0300 Subject: [PATCH 6/6] Remove unnecessary argument in css2css test of :is --- tests/test_cssselect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cssselect.py b/tests/test_cssselect.py index 44b4768..c08836f 100644 --- a/tests/test_cssselect.py +++ b/tests/test_cssselect.py @@ -307,7 +307,7 @@ def css2css(css, res=None): css2css(':not(*[foo])', ':not([foo])') css2css(':not(:empty)') css2css(':not(#foo)') - css2css(':is(#bar, .foo)', ':is(#bar, .foo)') + css2css(':is(#bar, .foo)') css2css(':is(:focused, :visited)') css2css('foo:empty') css2css('foo::before')