Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/turbohtml/convert.py: 95%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

20 statements  

1""" 

2turbohtml.convert: translate between the query languages turbohtml speaks. 

3 

4:func:`css_to_xpath` turns a CSS selector list into an equivalent XPath 1.0 expression, replacing `cssselect 

5<https://github.com/scrapy/cssselect>`__ (the translator behind lxml's ``cssselect()``, parsel, and pyquery). turbohtml 

6runs both a CSS selector engine and an XPath engine in-process, so the translation is a C pass over the parsed 

7selector, and a differential suite checks the output selects the same nodes as the native CSS engine. The emitted 

8string is not cssselect's: every predicate is context-free, and the HTML semantics are the ones turbohtml's own engine 

9implements (the WHATWG case-insensitive attribute set, Selectors 4 ``:empty``, the exact ``fieldset``/``legend`` rule 

10for ``:disabled``). 

11 

12:class:`GenericTranslator` and :class:`HTMLTranslator` mirror cssselect's classes so ports are mechanical; both 

13delegate to the same engine, which always applies the HTML rules (element and attribute names lowercase). 

14""" 

15 

16from __future__ import annotations 

17 

18from ._html import _css_specificity, _css_to_xpath 

19from ._internal._selectors import SelectorSyntaxError 

20 

21__all__ = [ 

22 "ExpressionError", 

23 "GenericTranslator", 

24 "HTMLTranslator", 

25 "SelectorError", 

26 "SelectorSyntaxError", 

27 "css_specificity", 

28 "css_to_xpath", 

29] 

30 

31 

32class SelectorError(Exception): 

33 """Parent of :class:`ExpressionError`, mirroring cssselect's error base for a valid-but-untranslatable selector.""" 

34 

35 

36class ExpressionError(SelectorError, RuntimeError): 

37 """The selector is valid CSS but has no XPath 1.0 equivalent (for example ``:dir()`` or ``*:first-of-type``).""" 

38 

39 

40def css_to_xpath(selector: str, *, prefix: str = "descendant-or-self::") -> str: 

41 """ 

42 Translate a CSS selector list to an equivalent XPath 1.0 expression. 

43 

44 The XPath selects the same nodes as the selector does under :meth:`turbohtml.Node.select`; a comma-separated list 

45 becomes a ``|`` union with the prefix prepended to each arm. 

46 

47 :param selector: the CSS selector list to translate. 

48 :param prefix: prepended to each arm; the default scopes the expression to the context node's subtree, matching 

49 cssselect. 

50 :raises SelectorSyntaxError: the selector does not parse. 

51 :raises ExpressionError: the selector parses but cannot be expressed in XPath 1.0. 

52 :returns: the XPath 1.0 expression. 

53 """ 

54 try: 

55 return _css_to_xpath(selector, prefix) 

56 except NotImplementedError as error: 

57 raise ExpressionError(str(error)) from None 

58 

59 

60def css_specificity(selector: str) -> list[tuple[int, int, int]]: 

61 """ 

62 Return the specificity of each selector in a comma-separated list. 

63 

64 Each triple is ``(a, b, c)`` per `CSS Selectors Level 4 §17 

65 <https://www.w3.org/TR/selectors-4/#specificity-rules>`_: ``a`` counts id selectors, ``b`` counts class, attribute, 

66 and pseudo-class selectors, and ``c`` counts type and pseudo-element selectors. ``:is()``, ``:not()``, and 

67 ``:has()`` take the specificity of their most specific argument; ``:where()`` contributes zero. The list is one 

68 triple per comma-separated selector, the shape ``cssselect``'s ``Selector.specificity()`` returns per parsed arm. 

69 

70 :param selector: the CSS selector list to weigh. 

71 :raises SelectorSyntaxError: the selector does not parse. 

72 :returns: one ``(a, b, c)`` triple per selector in the list, in source order. 

73 """ 

74 return _css_specificity(selector) 

75 

76 

77class GenericTranslator: 

78 """ 

79 A cssselect-shaped translator: :meth:`css_to_xpath` is a method here, as in cssselect. 

80 

81 Unlike cssselect's generic (XML) translator, the translation always follows the HTML rules turbohtml's selector 

82 engine implements: element and attribute names are matched lowercase, and the WHATWG case-insensitive attribute set 

83 compares values case-insensitively. 

84 """ 

85 

86 def css_to_xpath(self, css: str, prefix: str = "descendant-or-self::") -> str: # ruff:ignore[no-self-use] # cssselect shape needs an instance method 

87 """ 

88 Translate a CSS selector list to an equivalent XPath 1.0 expression. 

89 

90 :param css: the CSS selector list to translate. 

91 :param prefix: prepended to each arm of the translated union. 

92 :raises SelectorSyntaxError: the selector does not parse. 

93 :raises ExpressionError: the selector parses but cannot be expressed in XPath 1.0. 

94 :returns: the XPath 1.0 expression. 

95 """ 

96 return css_to_xpath(css, prefix=prefix) 

97 

98 

99class HTMLTranslator(GenericTranslator): 

100 """ 

101 The cssselect ``HTMLTranslator`` shape. 

102 

103 :param xhtml: accepted for signature compatibility with cssselect; the translation applies the HTML lowercasing 

104 rules either way, because that is what turbohtml's parser and selector engine do. 

105 """ 

106 

107 def __init__(self, xhtml: bool = False) -> None: # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument] # positional bool mirrors cssselect 

108 """Record the cssselect-compatible ``xhtml`` flag.""" 

109 self.xhtml = xhtml