csstranslator.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. try:
  2. from functools import lru_cache
  3. except ImportError:
  4. from functools32 import lru_cache
  5. from cssselect import GenericTranslator as OriginalGenericTranslator
  6. from cssselect import HTMLTranslator as OriginalHTMLTranslator
  7. from cssselect.xpath import XPathExpr as OriginalXPathExpr
  8. from cssselect.xpath import _unicode_safe_getattr, ExpressionError
  9. from cssselect.parser import FunctionalPseudoElement
  10. class XPathExpr(OriginalXPathExpr):
  11. textnode = False
  12. attribute = None
  13. @classmethod
  14. def from_xpath(cls, xpath, textnode=False, attribute=None):
  15. x = cls(path=xpath.path, element=xpath.element, condition=xpath.condition)
  16. x.textnode = textnode
  17. x.attribute = attribute
  18. return x
  19. def __str__(self):
  20. path = super(XPathExpr, self).__str__()
  21. if self.textnode:
  22. if path == '*':
  23. path = 'text()'
  24. elif path.endswith('::*/*'):
  25. path = path[:-3] + 'text()'
  26. else:
  27. path += '/text()'
  28. if self.attribute is not None:
  29. if path.endswith('::*/*'):
  30. path = path[:-2]
  31. path += '/@%s' % self.attribute
  32. return path
  33. def join(self, combiner, other):
  34. super(XPathExpr, self).join(combiner, other)
  35. self.textnode = other.textnode
  36. self.attribute = other.attribute
  37. return self
  38. class TranslatorMixin(object):
  39. """This mixin adds support to CSS pseudo elements via dynamic dispatch.
  40. Currently supported pseudo-elements are ``::text`` and ``::attr(ATTR_NAME)``.
  41. """
  42. def xpath_element(self, selector):
  43. xpath = super(TranslatorMixin, self).xpath_element(selector)
  44. return XPathExpr.from_xpath(xpath)
  45. def xpath_pseudo_element(self, xpath, pseudo_element):
  46. """
  47. Dispatch method that transforms XPath to support pseudo-element
  48. """
  49. if isinstance(pseudo_element, FunctionalPseudoElement):
  50. method = 'xpath_%s_functional_pseudo_element' % (
  51. pseudo_element.name.replace('-', '_'))
  52. method = _unicode_safe_getattr(self, method, None)
  53. if not method:
  54. raise ExpressionError(
  55. "The functional pseudo-element ::%s() is unknown"
  56. % pseudo_element.name)
  57. xpath = method(xpath, pseudo_element)
  58. else:
  59. method = 'xpath_%s_simple_pseudo_element' % (
  60. pseudo_element.replace('-', '_'))
  61. method = _unicode_safe_getattr(self, method, None)
  62. if not method:
  63. raise ExpressionError(
  64. "The pseudo-element ::%s is unknown"
  65. % pseudo_element)
  66. xpath = method(xpath)
  67. return xpath
  68. def xpath_attr_functional_pseudo_element(self, xpath, function):
  69. """Support selecting attribute values using ::attr() pseudo-element
  70. """
  71. if function.argument_types() not in (['STRING'], ['IDENT']):
  72. raise ExpressionError(
  73. "Expected a single string or ident for ::attr(), got %r"
  74. % function.arguments)
  75. return XPathExpr.from_xpath(xpath,
  76. attribute=function.arguments[0].value)
  77. def xpath_text_simple_pseudo_element(self, xpath):
  78. """Support selecting text nodes using ::text pseudo-element"""
  79. return XPathExpr.from_xpath(xpath, textnode=True)
  80. class GenericTranslator(TranslatorMixin, OriginalGenericTranslator):
  81. @lru_cache(maxsize=256)
  82. def css_to_xpath(self, css, prefix='descendant-or-self::'):
  83. return super(GenericTranslator, self).css_to_xpath(css, prefix)
  84. class HTMLTranslator(TranslatorMixin, OriginalHTMLTranslator):
  85. @lru_cache(maxsize=256)
  86. def css_to_xpath(self, css, prefix='descendant-or-self::'):
  87. return super(HTMLTranslator, self).css_to_xpath(css, prefix)
  88. _translator = HTMLTranslator()
  89. def css2xpath(query):
  90. "Return translated XPath version of a given CSS query"
  91. return _translator.css_to_xpath(query)