23
23
from lxml import etree , html
24
24
from cssselect import (parse , GenericTranslator , HTMLTranslator ,
25
25
SelectorSyntaxError , ExpressionError )
26
- from cssselect .parser import tokenize , parse_series , _unicode
26
+ from cssselect .parser import (tokenize , parse_series , _unicode ,
27
+ FunctionalPseudoElement )
28
+ from cssselect .xpath import _unicode_safe_getattr , XPathExpr
27
29
28
30
29
31
if sys .version_info [0 ] < 3 :
@@ -150,6 +152,7 @@ def parse_pseudo(css):
150
152
result = []
151
153
for selector in parse (css ):
152
154
pseudo = selector .pseudo_element
155
+ pseudo = _unicode (pseudo ) if pseudo else pseudo
153
156
# No Symbol here
154
157
assert pseudo is None or type (pseudo ) is _unicode
155
158
selector = repr (selector .parsed_tree ).replace ("(u'" , "('" )
@@ -176,6 +179,10 @@ def parse_one(css):
176
179
assert parse_one ('::firsT-linE' ) == ('Element[*]' , 'first-line' )
177
180
assert parse_one ('::firsT-letteR' ) == ('Element[*]' , 'first-letter' )
178
181
182
+ assert parse_one ('::text-content' ) == ('Element[*]' , 'text-content' )
183
+ assert parse_one ('::attr(name)' ) == (
184
+ "Element[*]" , "FunctionalPseudoElement[::attr(['name'])]" )
185
+
179
186
assert parse_one ('::Selection' ) == ('Element[*]' , 'selection' )
180
187
assert parse_one ('foo:after' ) == ('Element[foo]' , 'after' )
181
188
assert parse_one ('foo::selection' ) == ('Element[foo]' , 'selection' )
@@ -264,8 +271,6 @@ def get_error(css):
264
271
"Expected ident or '*', got <DELIM '#' at 1>" )
265
272
assert get_error ('[foo=#]' ) == (
266
273
"Expected string or ident, got <DELIM '#' at 5>" )
267
- assert get_error (':nth-child()' ) == (
268
- "Expected at least one argument, got <DELIM ')' at 11>" )
269
274
assert get_error ('[href]a' ) == (
270
275
"Expected selector, got <IDENT 'a' at 6>" )
271
276
assert get_error ('[rel=stylesheet]' ) == None
@@ -436,6 +441,71 @@ def test_unicode_escapes(self):
436
441
assert css_to_xpath ('*[aval="\' \\ 20\r \n \' "]' ) == (
437
442
'''descendant-or-self::*[@aval = "' '"]''' )
438
443
444
+ def test_xpath_pseudo_elements (self ):
445
+ class CustomTranslator (GenericTranslator ):
446
+ def xpath_pseudo_element (self , xpath , pseudo_element ):
447
+ if isinstance (pseudo_element , FunctionalPseudoElement ):
448
+ method = 'xpath_%s_functional_pseudo_element' % (
449
+ pseudo_element .name .replace ('-' , '_' ))
450
+ method = _unicode_safe_getattr (self , method , None )
451
+ if not method :
452
+ raise ExpressionError (
453
+ "The functional pseudo-element ::%s() is unknown"
454
+ % functional .name )
455
+ xpath = method (xpath , pseudo_element .arguments )
456
+ else :
457
+ method = 'xpath_%s_simple_pseudo_element' % (
458
+ pseudo_element .replace ('-' , '_' ))
459
+ method = _unicode_safe_getattr (self , method , None )
460
+ if not method :
461
+ raise ExpressionError (
462
+ "The pseudo-element ::%s is unknown"
463
+ % pseudo_element )
464
+ xpath = method (xpath )
465
+ return xpath
466
+
467
+ # functional pseudo-class:
468
+ # elements that have a certain number of attributes
469
+ def xpath_nb_attr_function (self , xpath , function ):
470
+ nb_attributes = int (function .arguments [0 ].value )
471
+ return xpath .add_condition (
472
+ "count(@*)=%d" % nb_attributes )
473
+
474
+ # pseudo-class:
475
+ # elements that have 5 attributes
476
+ def xpath_five_attributes_pseudo (self , xpath ):
477
+ return xpath .add_condition ("count(@*)=5" )
478
+
479
+ # functional pseudo-element:
480
+ # element's attribute by name
481
+ def xpath_attr_functional_pseudo_element (self , xpath , arguments ):
482
+ attribute_name = arguments [0 ].value
483
+ other = XPathExpr ('@%s' % attribute_name , '' , )
484
+ return xpath .join ('/' , other )
485
+
486
+ # pseudo-element:
487
+ # element's text() nodes
488
+ def xpath_text_node_simple_pseudo_element (self , xpath ):
489
+ other = XPathExpr ('text()' , '' , )
490
+ return xpath .join ('/' , other )
491
+
492
+ # pseudo-element:
493
+ # element's href attribute
494
+ def xpath_attr_href_simple_pseudo_element (self , xpath ):
495
+ other = XPathExpr ('@href' , '' , )
496
+ return xpath .join ('/' , other )
497
+
498
+ def xpath (css ):
499
+ return _unicode (CustomTranslator ().css_to_xpath (css ))
500
+
501
+ assert xpath (':five-attributes' ) == "descendant-or-self::*[count(@*)=5]"
502
+ assert xpath (':nb-attr(3)' ) == "descendant-or-self::*[count(@*)=3]"
503
+ assert xpath ('::attr(href)' ) == "descendant-or-self::*/@href"
504
+ assert xpath ('::text-node' ) == "descendant-or-self::*/text()"
505
+ assert xpath ('::attr-href' ) == "descendant-or-self::*/@href"
506
+ assert xpath ('p img::attr(src)' ) == (
507
+ "descendant-or-self::p/descendant-or-self::*/img/@src" )
508
+
439
509
def test_series (self ):
440
510
def series (css ):
441
511
selector , = parse (':nth-child(%s)' % css )
0 commit comments