123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773 |
- # -*- coding: utf-8 -*-
- """
- cssselect.xpath
- ===============
- Translation of parsed CSS selectors to XPath expressions.
- :copyright: (c) 2007-2012 Ian Bicking and contributors.
- See AUTHORS for more details.
- :license: BSD, see LICENSE for more details.
- """
- import sys
- import re
- from cssselect.parser import parse, parse_series, SelectorError
- if sys.version_info[0] < 3:
- _basestring = basestring
- _unicode = unicode
- else:
- _basestring = str
- _unicode = str
- def _unicode_safe_getattr(obj, name, default=None):
- # getattr() with a non-ASCII name fails on Python 2.x
- name = name.encode('ascii', 'replace').decode('ascii')
- return getattr(obj, name, default)
- class ExpressionError(SelectorError, RuntimeError):
- """Unknown or unsupported selector (eg. pseudo-class)."""
- #### XPath Helpers
- class XPathExpr(object):
- def __init__(self, path='', element='*', condition='', star_prefix=False):
- self.path = path
- self.element = element
- self.condition = condition
- def __str__(self):
- path = _unicode(self.path) + _unicode(self.element)
- if self.condition:
- path += '[%s]' % self.condition
- return path
- def __repr__(self):
- return '%s[%s]' % (self.__class__.__name__, self)
- def add_condition(self, condition):
- if self.condition:
- self.condition = '%s and (%s)' % (self.condition, condition)
- else:
- self.condition = condition
- return self
- def add_name_test(self):
- if self.element == '*':
- # We weren't doing a test anyway
- return
- self.add_condition(
- "name() = %s" % GenericTranslator.xpath_literal(self.element))
- self.element = '*'
- def add_star_prefix(self):
- """
- Append '*/' to the path to keep the context constrained
- to a single parent.
- """
- self.path += '*/'
- def join(self, combiner, other):
- path = _unicode(self) + combiner
- # Any "star prefix" is redundant when joining.
- if other.path != '*/':
- path += other.path
- self.path = path
- self.element = other.element
- self.condition = other.condition
- return self
- split_at_single_quotes = re.compile("('+)").split
- # The spec is actually more permissive than that, but don’t bother.
- # This is just for the fast path.
- # http://www.w3.org/TR/REC-xml/#NT-NameStartChar
- is_safe_name = re.compile('^[a-zA-Z_][a-zA-Z0-9_.-]*$').match
- # Test that the string is not empty and does not contain whitespace
- is_non_whitespace = re.compile(r'^[^ \t\r\n\f]+$').match
- #### Translation
- class GenericTranslator(object):
- """
- Translator for "generic" XML documents.
- Everything is case-sensitive, no assumption is made on the meaning
- of element names and attribute names.
- """
- ####
- #### HERE BE DRAGONS
- ####
- #### You are welcome to hook into this to change some behavior,
- #### but do so at your own risks.
- #### Until is has recieved a lot more work and review,
- #### I reserve the right to change this API in backward-incompatible ways
- #### with any minor version of cssselect.
- #### See https://github.com/scrapy/cssselect/pull/22
- #### -- Simon Sapin.
- ####
- combinator_mapping = {
- ' ': 'descendant',
- '>': 'child',
- '+': 'direct_adjacent',
- '~': 'indirect_adjacent',
- }
- attribute_operator_mapping = {
- 'exists': 'exists',
- '=': 'equals',
- '~=': 'includes',
- '|=': 'dashmatch',
- '^=': 'prefixmatch',
- '$=': 'suffixmatch',
- '*=': 'substringmatch',
- '!=': 'different', # XXX Not in Level 3 but meh
- }
- #: The attribute used for ID selectors depends on the document language:
- #: http://www.w3.org/TR/selectors/#id-selectors
- id_attribute = 'id'
- #: The attribute used for ``:lang()`` depends on the document language:
- #: http://www.w3.org/TR/selectors/#lang-pseudo
- lang_attribute = 'xml:lang'
- #: The case sensitivity of document language element names,
- #: attribute names, and attribute values in selectors depends
- #: on the document language.
- #: http://www.w3.org/TR/selectors/#casesens
- #:
- #: When a document language defines one of these as case-insensitive,
- #: cssselect assumes that the document parser makes the parsed values
- #: lower-case. Making the selector lower-case too makes the comparaison
- #: case-insensitive.
- #:
- #: In HTML, element names and attributes names (but not attribute values)
- #: are case-insensitive. All of lxml.html, html5lib, BeautifulSoup4
- #: and HTMLParser make them lower-case in their parse result, so
- #: the assumption holds.
- lower_case_element_names = False
- lower_case_attribute_names = False
- lower_case_attribute_values = False
- # class used to represent and xpath expression
- xpathexpr_cls = XPathExpr
- def css_to_xpath(self, css, prefix='descendant-or-self::'):
- """Translate a *group of selectors* to XPath.
- Pseudo-elements are not supported here since XPath only knows
- about "real" elements.
- :param css:
- A *group of selectors* as an Unicode string.
- :param prefix:
- This string is prepended to the XPath expression for each selector.
- The default makes selectors scoped to the context node’s subtree.
- :raises:
- :class:`SelectorSyntaxError` on invalid selectors,
- :class:`ExpressionError` on unknown/unsupported selectors,
- including pseudo-elements.
- :returns:
- The equivalent XPath 1.0 expression as an Unicode string.
- """
- return ' | '.join(self.selector_to_xpath(selector, prefix,
- translate_pseudo_elements=True)
- for selector in parse(css))
- def selector_to_xpath(self, selector, prefix='descendant-or-self::',
- translate_pseudo_elements=False):
- """Translate a parsed selector to XPath.
- :param selector:
- A parsed :class:`Selector` object.
- :param prefix:
- This string is prepended to the resulting XPath expression.
- The default makes selectors scoped to the context node’s subtree.
- :param translate_pseudo_elements:
- Unless this is set to ``True`` (as :meth:`css_to_xpath` does),
- the :attr:`~Selector.pseudo_element` attribute of the selector
- is ignored.
- It is the caller's responsibility to reject selectors
- with pseudo-elements, or to account for them somehow.
- :raises:
- :class:`ExpressionError` on unknown/unsupported selectors.
- :returns:
- The equivalent XPath 1.0 expression as an Unicode string.
- """
- tree = getattr(selector, 'parsed_tree', None)
- if not tree:
- raise TypeError('Expected a parsed selector, got %r' % (selector,))
- xpath = self.xpath(tree)
- assert isinstance(xpath, self.xpathexpr_cls) # help debug a missing 'return'
- if translate_pseudo_elements and selector.pseudo_element:
- xpath = self.xpath_pseudo_element(xpath, selector.pseudo_element)
- return (prefix or '') + _unicode(xpath)
- def xpath_pseudo_element(self, xpath, pseudo_element):
- """Translate a pseudo-element.
- Defaults to not supporting pseudo-elements at all,
- but can be overridden by sub-classes.
- """
- raise ExpressionError('Pseudo-elements are not supported.')
- @staticmethod
- def xpath_literal(s):
- s = _unicode(s)
- if "'" not in s:
- s = "'%s'" % s
- elif '"' not in s:
- s = '"%s"' % s
- else:
- s = "concat(%s)" % ','.join([
- (("'" in part) and '"%s"' or "'%s'") % part
- for part in split_at_single_quotes(s) if part
- ])
- return s
- def xpath(self, parsed_selector):
- """Translate any parsed selector object."""
- type_name = type(parsed_selector).__name__
- method = getattr(self, 'xpath_%s' % type_name.lower(), None)
- if method is None:
- raise ExpressionError('%s is not supported.' % type_name)
- return method(parsed_selector)
- # Dispatched by parsed object type
- def xpath_combinedselector(self, combined):
- """Translate a combined selector."""
- combinator = self.combinator_mapping[combined.combinator]
- method = getattr(self, 'xpath_%s_combinator' % combinator)
- return method(self.xpath(combined.selector),
- self.xpath(combined.subselector))
- def xpath_negation(self, negation):
- xpath = self.xpath(negation.selector)
- sub_xpath = self.xpath(negation.subselector)
- sub_xpath.add_name_test()
- if sub_xpath.condition:
- return xpath.add_condition('not(%s)' % sub_xpath.condition)
- else:
- return xpath.add_condition('0')
- def xpath_function(self, function):
- """Translate a functional pseudo-class."""
- method = 'xpath_%s_function' % function.name.replace('-', '_')
- method = _unicode_safe_getattr(self, method, None)
- if not method:
- raise ExpressionError(
- "The pseudo-class :%s() is unknown" % function.name)
- return method(self.xpath(function.selector), function)
- def xpath_pseudo(self, pseudo):
- """Translate a pseudo-class."""
- method = 'xpath_%s_pseudo' % pseudo.ident.replace('-', '_')
- method = _unicode_safe_getattr(self, method, None)
- if not method:
- # TODO: better error message for pseudo-elements?
- raise ExpressionError(
- "The pseudo-class :%s is unknown" % pseudo.ident)
- return method(self.xpath(pseudo.selector))
- def xpath_attrib(self, selector):
- """Translate an attribute selector."""
- operator = self.attribute_operator_mapping[selector.operator]
- method = getattr(self, 'xpath_attrib_%s' % operator)
- if self.lower_case_attribute_names:
- name = selector.attrib.lower()
- else:
- name = selector.attrib
- safe = is_safe_name(name)
- if selector.namespace:
- name = '%s:%s' % (selector.namespace, name)
- safe = safe and is_safe_name(selector.namespace)
- if safe:
- attrib = '@' + name
- else:
- attrib = 'attribute::*[name() = %s]' % self.xpath_literal(name)
- if self.lower_case_attribute_values:
- value = selector.value.lower()
- else:
- value = selector.value
- return method(self.xpath(selector.selector), attrib, value)
- def xpath_class(self, class_selector):
- """Translate a class selector."""
- # .foo is defined as [class~=foo] in the spec.
- xpath = self.xpath(class_selector.selector)
- return self.xpath_attrib_includes(
- xpath, '@class', class_selector.class_name)
- def xpath_hash(self, id_selector):
- """Translate an ID selector."""
- xpath = self.xpath(id_selector.selector)
- return self.xpath_attrib_equals(xpath, '@id', id_selector.id)
- def xpath_element(self, selector):
- """Translate a type or universal selector."""
- element = selector.element
- if not element:
- element = '*'
- safe = True
- else:
- safe = is_safe_name(element)
- if self.lower_case_element_names:
- element = element.lower()
- if selector.namespace:
- # Namespace prefixes are case-sensitive.
- # http://www.w3.org/TR/css3-namespace/#prefixes
- element = '%s:%s' % (selector.namespace, element)
- safe = safe and is_safe_name(selector.namespace)
- xpath = self.xpathexpr_cls(element=element)
- if not safe:
- xpath.add_name_test()
- return xpath
- # CombinedSelector: dispatch by combinator
- def xpath_descendant_combinator(self, left, right):
- """right is a child, grand-child or further descendant of left"""
- return left.join('/descendant-or-self::*/', right)
- def xpath_child_combinator(self, left, right):
- """right is an immediate child of left"""
- return left.join('/', right)
- def xpath_direct_adjacent_combinator(self, left, right):
- """right is a sibling immediately after left"""
- xpath = left.join('/following-sibling::', right)
- xpath.add_name_test()
- return xpath.add_condition('position() = 1')
- def xpath_indirect_adjacent_combinator(self, left, right):
- """right is a sibling after left, immediately or not"""
- return left.join('/following-sibling::', right)
- # Function: dispatch by function/pseudo-class name
- def xpath_nth_child_function(self, xpath, function, last=False,
- add_name_test=True):
- try:
- a, b = parse_series(function.arguments)
- except ValueError:
- raise ExpressionError("Invalid series: '%r'" % function.arguments)
- # From https://www.w3.org/TR/css3-selectors/#structural-pseudos:
- #
- # :nth-child(an+b)
- # an+b-1 siblings before
- #
- # :nth-last-child(an+b)
- # an+b-1 siblings after
- #
- # :nth-of-type(an+b)
- # an+b-1 siblings with the same expanded element name before
- #
- # :nth-last-of-type(an+b)
- # an+b-1 siblings with the same expanded element name after
- #
- # So,
- # for :nth-child and :nth-of-type
- #
- # count(preceding-sibling::<nodetest>) = an+b-1
- #
- # for :nth-last-child and :nth-last-of-type
- #
- # count(following-sibling::<nodetest>) = an+b-1
- #
- # therefore,
- # count(...) - (b-1) ≡ 0 (mod a)
- #
- # if a == 0:
- # ~~~~~~~~~~
- # count(...) = b-1
- #
- # if a < 0:
- # ~~~~~~~~~
- # count(...) - b +1 <= 0
- # -> count(...) <= b-1
- #
- # if a > 0:
- # ~~~~~~~~~
- # count(...) - b +1 >= 0
- # -> count(...) >= b-1
- # work with b-1 instead
- b_min_1 = b - 1
- # early-exit condition 1:
- # ~~~~~~~~~~~~~~~~~~~~~~~
- # for a == 1, nth-*(an+b) means n+b-1 siblings before/after,
- # and since n ∈ {0, 1, 2, ...}, if b-1<=0,
- # there is always an "n" matching any number of siblings (maybe none)
- if a == 1 and b_min_1 <=0:
- return xpath
- # early-exit condition 2:
- # ~~~~~~~~~~~~~~~~~~~~~~~
- # an+b-1 siblings with a<0 and (b-1)<0 is not possible
- if a < 0 and b_min_1 < 0:
- return xpath.add_condition('0')
- # `add_name_test` boolean is inverted and somewhat counter-intuitive:
- #
- # nth_of_type() calls nth_child(add_name_test=False)
- if add_name_test:
- nodetest = '*'
- else:
- nodetest = '%s' % xpath.element
- # count siblings before or after the element
- if not last:
- siblings_count = 'count(preceding-sibling::%s)' % nodetest
- else:
- siblings_count = 'count(following-sibling::%s)' % nodetest
- # special case of fixed position: nth-*(0n+b)
- # if a == 0:
- # ~~~~~~~~~~
- # count(***-sibling::***) = b-1
- if a == 0:
- return xpath.add_condition('%s = %s' % (siblings_count, b_min_1))
- expr = []
- if a > 0:
- # siblings count, an+b-1, is always >= 0,
- # so if a>0, and (b-1)<=0, an "n" exists to satisfy this,
- # therefore, the predicate is only interesting if (b-1)>0
- if b_min_1 > 0:
- expr.append('%s >= %s' % (siblings_count, b_min_1))
- else:
- # if a<0, and (b-1)<0, no "n" satisfies this,
- # this is tested above as an early exist condition
- # otherwise,
- expr.append('%s <= %s' % (siblings_count, b_min_1))
- # operations modulo 1 or -1 are simpler, one only needs to verify:
- #
- # - either:
- # count(***-sibling::***) - (b-1) = n = 0, 1, 2, 3, etc.,
- # i.e. count(***-sibling::***) >= (b-1)
- #
- # - or:
- # count(***-sibling::***) - (b-1) = -n = 0, -1, -2, -3, etc.,
- # i.e. count(***-sibling::***) <= (b-1)
- # we we just did above.
- #
- if abs(a) != 1:
- # count(***-sibling::***) - (b-1) ≡ 0 (mod a)
- left = siblings_count
- # apply "modulo a" on 2nd term, -(b-1),
- # to simplify things like "(... +6) % -3",
- # and also make it positive with |a|
- b_neg = (-b_min_1) % abs(a)
- if b_neg != 0:
- b_neg = '+%s' % (b_neg)
- left = '(%s %s)' % (left, b_neg)
- expr.append('%s mod %s = 0' % (left, a))
- xpath.add_condition(' and '.join(expr))
- return xpath
- def xpath_nth_last_child_function(self, xpath, function):
- return self.xpath_nth_child_function(xpath, function, last=True)
- def xpath_nth_of_type_function(self, xpath, function):
- if xpath.element == '*':
- raise ExpressionError(
- "*:nth-of-type() is not implemented")
- return self.xpath_nth_child_function(xpath, function,
- add_name_test=False)
- def xpath_nth_last_of_type_function(self, xpath, function):
- if xpath.element == '*':
- raise ExpressionError(
- "*:nth-of-type() is not implemented")
- return self.xpath_nth_child_function(xpath, function, last=True,
- add_name_test=False)
- def xpath_contains_function(self, xpath, function):
- # Defined there, removed in later drafts:
- # http://www.w3.org/TR/2001/CR-css3-selectors-20011113/#content-selectors
- if function.argument_types() not in (['STRING'], ['IDENT']):
- raise ExpressionError(
- "Expected a single string or ident for :contains(), got %r"
- % function.arguments)
- value = function.arguments[0].value
- return xpath.add_condition(
- 'contains(., %s)' % self.xpath_literal(value))
- def xpath_lang_function(self, xpath, function):
- if function.argument_types() not in (['STRING'], ['IDENT']):
- raise ExpressionError(
- "Expected a single string or ident for :lang(), got %r"
- % function.arguments)
- value = function.arguments[0].value
- return xpath.add_condition(
- "lang(%s)" % (self.xpath_literal(value)))
- # Pseudo: dispatch by pseudo-class name
- def xpath_root_pseudo(self, xpath):
- return xpath.add_condition("not(parent::*)")
- def xpath_first_child_pseudo(self, xpath):
- return xpath.add_condition('count(preceding-sibling::*) = 0')
- def xpath_last_child_pseudo(self, xpath):
- return xpath.add_condition('count(following-sibling::*) = 0')
- def xpath_first_of_type_pseudo(self, xpath):
- if xpath.element == '*':
- raise ExpressionError(
- "*:first-of-type is not implemented")
- return xpath.add_condition('count(preceding-sibling::%s) = 0' % xpath.element)
- def xpath_last_of_type_pseudo(self, xpath):
- if xpath.element == '*':
- raise ExpressionError(
- "*:last-of-type is not implemented")
- return xpath.add_condition('count(following-sibling::%s) = 0' % xpath.element)
- def xpath_only_child_pseudo(self, xpath):
- return xpath.add_condition('count(parent::*/child::*) = 1')
- def xpath_only_of_type_pseudo(self, xpath):
- if xpath.element == '*':
- raise ExpressionError(
- "*:only-of-type is not implemented")
- return xpath.add_condition('count(parent::*/child::%s) = 1' % xpath.element)
- def xpath_empty_pseudo(self, xpath):
- return xpath.add_condition("not(*) and not(string-length())")
- def pseudo_never_matches(self, xpath):
- """Common implementation for pseudo-classes that never match."""
- return xpath.add_condition("0")
- xpath_link_pseudo = pseudo_never_matches
- xpath_visited_pseudo = pseudo_never_matches
- xpath_hover_pseudo = pseudo_never_matches
- xpath_active_pseudo = pseudo_never_matches
- xpath_focus_pseudo = pseudo_never_matches
- xpath_target_pseudo = pseudo_never_matches
- xpath_enabled_pseudo = pseudo_never_matches
- xpath_disabled_pseudo = pseudo_never_matches
- xpath_checked_pseudo = pseudo_never_matches
- # Attrib: dispatch by attribute operator
- def xpath_attrib_exists(self, xpath, name, value):
- assert not value
- xpath.add_condition(name)
- return xpath
- def xpath_attrib_equals(self, xpath, name, value):
- xpath.add_condition('%s = %s' % (name, self.xpath_literal(value)))
- return xpath
- def xpath_attrib_different(self, xpath, name, value):
- # FIXME: this seems like a weird hack...
- if value:
- xpath.add_condition('not(%s) or %s != %s'
- % (name, name, self.xpath_literal(value)))
- else:
- xpath.add_condition('%s != %s'
- % (name, self.xpath_literal(value)))
- return xpath
- def xpath_attrib_includes(self, xpath, name, value):
- if is_non_whitespace(value):
- xpath.add_condition(
- "%s and contains(concat(' ', normalize-space(%s), ' '), %s)"
- % (name, name, self.xpath_literal(' '+value+' ')))
- else:
- xpath.add_condition('0')
- return xpath
- def xpath_attrib_dashmatch(self, xpath, name, value):
- # Weird, but true...
- xpath.add_condition('%s and (%s = %s or starts-with(%s, %s))' % (
- name,
- name, self.xpath_literal(value),
- name, self.xpath_literal(value + '-')))
- return xpath
- def xpath_attrib_prefixmatch(self, xpath, name, value):
- if value:
- xpath.add_condition('%s and starts-with(%s, %s)' % (
- name, name, self.xpath_literal(value)))
- else:
- xpath.add_condition('0')
- return xpath
- def xpath_attrib_suffixmatch(self, xpath, name, value):
- if value:
- # Oddly there is a starts-with in XPath 1.0, but not ends-with
- xpath.add_condition(
- '%s and substring(%s, string-length(%s)-%s) = %s'
- % (name, name, name, len(value)-1, self.xpath_literal(value)))
- else:
- xpath.add_condition('0')
- return xpath
- def xpath_attrib_substringmatch(self, xpath, name, value):
- if value:
- # Attribute selectors are case sensitive
- xpath.add_condition('%s and contains(%s, %s)' % (
- name, name, self.xpath_literal(value)))
- else:
- xpath.add_condition('0')
- return xpath
- class HTMLTranslator(GenericTranslator):
- """
- Translator for (X)HTML documents.
- Has a more useful implementation of some pseudo-classes based on
- HTML-specific element names and attribute names, as described in
- the `HTML5 specification`_. It assumes no-quirks mode.
- The API is the same as :class:`GenericTranslator`.
- .. _HTML5 specification: http://www.w3.org/TR/html5/links.html#selectors
- :param xhtml:
- If false (the default), element names and attribute names
- are case-insensitive.
- """
- lang_attribute = 'lang'
- def __init__(self, xhtml=False):
- self.xhtml = xhtml # Might be useful for sub-classes?
- if not xhtml:
- # See their definition in GenericTranslator.
- self.lower_case_element_names = True
- self.lower_case_attribute_names = True
- def xpath_checked_pseudo(self, xpath):
- # FIXME: is this really all the elements?
- return xpath.add_condition(
- "(@selected and name(.) = 'option') or "
- "(@checked "
- "and (name(.) = 'input' or name(.) = 'command')"
- "and (@type = 'checkbox' or @type = 'radio'))")
- def xpath_lang_function(self, xpath, function):
- if function.argument_types() not in (['STRING'], ['IDENT']):
- raise ExpressionError(
- "Expected a single string or ident for :lang(), got %r"
- % function.arguments)
- value = function.arguments[0].value
- return xpath.add_condition(
- "ancestor-or-self::*[@lang][1][starts-with(concat("
- # XPath 1.0 has no lower-case function...
- "translate(@%s, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', "
- "'abcdefghijklmnopqrstuvwxyz'), "
- "'-'), %s)]"
- % (self.lang_attribute, self.xpath_literal(value.lower() + '-')))
- def xpath_link_pseudo(self, xpath):
- return xpath.add_condition("@href and "
- "(name(.) = 'a' or name(.) = 'link' or name(.) = 'area')")
- # Links are never visited, the implementation for :visited is the same
- # as in GenericTranslator
- def xpath_disabled_pseudo(self, xpath):
- # http://www.w3.org/TR/html5/section-index.html#attributes-1
- return xpath.add_condition('''
- (
- @disabled and
- (
- (name(.) = 'input' and @type != 'hidden') or
- name(.) = 'button' or
- name(.) = 'select' or
- name(.) = 'textarea' or
- name(.) = 'command' or
- name(.) = 'fieldset' or
- name(.) = 'optgroup' or
- name(.) = 'option'
- )
- ) or (
- (
- (name(.) = 'input' and @type != 'hidden') or
- name(.) = 'button' or
- name(.) = 'select' or
- name(.) = 'textarea'
- )
- and ancestor::fieldset[@disabled]
- )
- ''')
- # FIXME: in the second half, add "and is not a descendant of that
- # fieldset element's first legend element child, if any."
- def xpath_enabled_pseudo(self, xpath):
- # http://www.w3.org/TR/html5/section-index.html#attributes-1
- return xpath.add_condition('''
- (
- @href and (
- name(.) = 'a' or
- name(.) = 'link' or
- name(.) = 'area'
- )
- ) or (
- (
- name(.) = 'command' or
- name(.) = 'fieldset' or
- name(.) = 'optgroup'
- )
- and not(@disabled)
- ) or (
- (
- (name(.) = 'input' and @type != 'hidden') or
- name(.) = 'button' or
- name(.) = 'select' or
- name(.) = 'textarea' or
- name(.) = 'keygen'
- )
- and not (@disabled or ancestor::fieldset[@disabled])
- ) or (
- name(.) = 'option' and not(
- @disabled or ancestor::optgroup[@disabled]
- )
- )
- ''')
- # FIXME: ... or "li elements that are children of menu elements,
- # and that have a child element that defines a command, if the first
- # such element's Disabled State facet is false (not disabled)".
- # FIXME: after ancestor::fieldset[@disabled], add "and is not a
- # descendant of that fieldset element's first legend element child,
- # if any."
|