Ejemplo n.º 1
0
 def lang(self):
     """The language of this element, as a string."""
     # http://whatwg.org/C#language
     xml_lang = self.etree_element.get(
         '{http://www.w3.org/XML/1998/namespace}lang')
     if xml_lang is not None:
         return ascii_lower(xml_lang)
     is_html = (self.in_html_document
                or self.namespace_url == 'http://www.w3.org/1999/xhtml')
     if is_html:
         lang = self.etree_element.get('lang')
         if lang is not None:
             return ascii_lower(lang)
     if self.parent is not None:
         return self.parent.lang
     # Root elememnt
     if is_html:
         content_language = None
         iterator = self.etree_element.iter(
             '{http://www.w3.org/1999/xhtml}meta')
         for meta in iterator:
             http_equiv = meta.get('http-equiv', '')
             if ascii_lower(http_equiv) == 'content-language':
                 content_language = _parse_content_language(
                     meta.get('content'))
         if content_language is not None:
             return ascii_lower(content_language)
     # Empty string means unknown
     return _parse_content_language(self.transport_content_language) or ''
Ejemplo n.º 2
0
 def lang(self):
     """The language of this element, as a string."""
     # http://whatwg.org/C#language
     xml_lang = self.etree_element.get(
         '{http://www.w3.org/XML/1998/namespace}lang')
     if xml_lang is not None:
         return ascii_lower(xml_lang)
     is_html = (
         self.in_html_document or
         self.namespace_url == 'http://www.w3.org/1999/xhtml')
     if is_html:
         lang = self.etree_element.get('lang')
         if lang is not None:
             return ascii_lower(lang)
     if self.parent is not None:
         return self.parent.lang
     # Root elememnt
     if is_html:
         content_language = None
         for meta in etree_iter(self.etree_element,
                                '{http://www.w3.org/1999/xhtml}meta'):
             http_equiv = meta.get('http-equiv', '')
             if ascii_lower(http_equiv) == 'content-language':
                 content_language = _parse_content_language(
                     meta.get('content'))
         if content_language is not None:
             return ascii_lower(content_language)
     # Empty string means unknown
     return _parse_content_language(self.transport_content_language) or ''
Ejemplo n.º 3
0
 def __init__(self, line, column, value):
     Node.__init__(self, line, column)
     self.value = value
     try:
         self.lower_value = ascii_lower(value)
     except UnicodeEncodeError:
         self.lower_value = value
Ejemplo n.º 4
0
 def __init__(self, line, column, value):
     Node.__init__(self, line, column)
     self.value = value
     try:
         self.lower_value = ascii_lower(value)
     except UnicodeEncodeError:
         self.lower_value = value
Ejemplo n.º 5
0
 def __init__(self, line, column, value, int_value, representation, unit):
     Node.__init__(self, line, column)
     self.value = value
     self.int_value = int_value
     self.is_integer = int_value is not None
     self.representation = representation
     self.unit = unit
     self.lower_unit = ascii_lower(unit)
Ejemplo n.º 6
0
 def __init__(self, line, column, value, int_value, representation, unit):
     Node.__init__(self, line, column)
     self.value = value
     self.int_value = int_value
     self.is_integer = int_value is not None
     self.representation = representation
     self.unit = unit
     self.lower_unit = ascii_lower(unit)
Ejemplo n.º 7
0
    def match(self, element):
        """Match selectors against the given element.

        :param element:
            An :class:`ElementWrapper`.
        :returns:
            A list of the payload objects associated to selectors that match
            element, in order of lowest to highest
            :attr:`compiler.CompiledSelector` specificity and in order of
            addition with :meth:`add_selector` among selectors of equal
            specificity.

        """
        relevant_selectors = []

        if element.id is not None and element.id in self.id_selectors:
            self.add_relevant_selectors(element, self.id_selectors[element.id],
                                        relevant_selectors)

        for class_name in element.classes:
            if class_name in self.class_selectors:
                self.add_relevant_selectors(element,
                                            self.class_selectors[class_name],
                                            relevant_selectors)

        lower_name = ascii_lower(element.local_name)
        if lower_name in self.lower_local_name_selectors:
            self.add_relevant_selectors(
                element, self.lower_local_name_selectors[lower_name],
                relevant_selectors)
        if element.namespace_url in self.namespace_selectors:
            self.add_relevant_selectors(
                element, self.namespace_selectors[element.namespace_url],
                relevant_selectors)

        if 'lang' in element.etree_element.attrib:
            self.add_relevant_selectors(element, self.lang_attr_selectors,
                                        relevant_selectors)

        self.add_relevant_selectors(element, self.other_selectors,
                                    relevant_selectors)

        relevant_selectors.sort(key=SORT_KEY)
        return relevant_selectors
Ejemplo n.º 8
0
    def match(self, element):
        """
        Match selectors against the given element.

        :param element:
            An :class:`ElementWrapper`.
        :returns:
            A list of the :obj:`payload` objects associated
            to selectors that match element,
            in order of lowest to highest :attr:`~CompiledSelector.specificity`
            and in order of addition with :meth:`add_selector`
            among selectors of equal specificity.

        """
        relevant_selectors = []

        if element.id is not None:
            relevant_selectors.append(self.id_selectors.get(element.id, []))

        for class_name in element.classes:
            relevant_selectors.append(self.class_selectors.get(class_name, []))

        relevant_selectors.append(
            self.lower_local_name_selectors.get(
                ascii_lower(element.local_name), []))
        relevant_selectors.append(
            self.namespace_selectors.get(element.namespace_url, []))

        if 'lang' in element.etree_element.attrib:
            relevant_selectors.append(self.lang_attr_selectors)

        relevant_selectors.append(self.other_selectors)

        results = [
            (specificity, order, pseudo, payload)
            for selector_list in relevant_selectors
            for test, specificity, order, pseudo, payload in selector_list
            if test(element)
        ]
        results.sort(key=SORT_KEY)
        return results
Ejemplo n.º 9
0
    def match(self, element):
        """
        Match selectors against the given element.

        :param element:
            An :class:`ElementWrapper`.
        :returns:
            A list of the :obj:`payload` objects associated
            to selectors that match element,
            in order of lowest to highest :attr:`~CompiledSelector.specificity`
            and in order of addition with :meth:`add_selector`
            among selectors of equal specificity.

        """
        relevant_selectors = []

        if element.id is not None:
            relevant_selectors.append(self.id_selectors.get(element.id, []))

        for class_name in element.classes:
            relevant_selectors.append(self.class_selectors.get(class_name, []))

        relevant_selectors.append(
            self.lower_local_name_selectors.get(
                ascii_lower(element.local_name), []))
        relevant_selectors.append(
            self.namespace_selectors.get(element.namespace_url, []))
        relevant_selectors.append(self.other_selectors)

        results = [
            (specificity, order, pseudo, payload)
            for selector_list in relevant_selectors
            for test, specificity, order, pseudo, payload in selector_list
            if test(element)
        ]
        results.sort(key=SORT_KEY)
        return results
Ejemplo n.º 10
0
 def __init__(self, line, column, name, arguments):
     Node.__init__(self, line, column)
     self.name = name
     self.lower_name = ascii_lower(name)
     self.arguments = arguments
Ejemplo n.º 11
0
 def __init__(self, line, column, value):
     Node.__init__(self, line, column)
     self.value = value
     self.lower_value = ascii_lower(value)
Ejemplo n.º 12
0
def parse_component_value_list(css, skip_comments=False):
    """Parse a list of component values.

    :type css: :obj:`str`
    :param css: A CSS string.
    :type skip_comments: :obj:`bool`
    :param skip_comments:
        Ignore CSS comments.
        The return values (and recursively its blocks and functions)
        will not contain any :class:`~tinycss2.ast.Comment` object.
    :returns: A list of :term:`component values`.

    """
    css = (css.replace('\0', '\uFFFD')
           # This turns out to be faster than a regexp:
           .replace('\r\n', '\n').replace('\r', '\n').replace('\f', '\n'))
    length = len(css)
    token_start_pos = pos = 0  # Character index in the css source.
    line = 1  # First line is line 1.
    last_newline = -1
    root = tokens = []
    end_char = None  # Pop the stack when encountering this character.
    stack = []  # Stack of nested blocks: (tokens, end_char) tuples.

    while pos < length:
        newline = css.rfind('\n', token_start_pos, pos)
        if newline != -1:
            line += 1 + css.count('\n', token_start_pos, newline)
            last_newline = newline
        # First character in a line is in column 1.
        column = pos - last_newline
        token_start_pos = pos
        c = css[pos]

        if c in ' \n\t':
            pos += 1
            while css.startswith((' ', '\n', '\t'), pos):
                pos += 1
            value = css[token_start_pos:pos]
            tokens.append(WhitespaceToken(line, column, value))
            continue
        elif (c in 'Uu' and pos + 2 < length and css[pos + 1] == '+' and
              css[pos + 2] in '0123456789abcdefABCDEF?'):
            start, end, pos = _consume_unicode_range(css, pos + 2)
            tokens.append(UnicodeRangeToken(line, column, start, end))
            continue
        elif css.startswith('-->', pos):  # Check before identifiers
            tokens.append(LiteralToken(line, column, '-->'))
            pos += 3
            continue
        elif _is_ident_start(css, pos):
            value, pos = _consume_ident(css, pos)
            if not css.startswith('(', pos):  # Not a function
                tokens.append(IdentToken(line, column, value))
                continue
            pos += 1  # Skip the '('
            if ascii_lower(value) == 'url':
                url_pos = pos
                while css.startswith((' ', '\n', '\t'), url_pos):
                    url_pos += 1
                if url_pos >= length or css[url_pos] not in ('"', "'"):
                    value, pos, error = _consume_url(css, pos)
                    if value is not None:
                        repr = 'url({})'.format(serialize_url(value))
                        if error is not None:
                            error_key = error[0]
                            if error_key == 'eof-in-string':
                                repr = repr[:-2]
                            else:
                                assert error_key == 'eof-in-url'
                                repr = repr[:-1]
                        tokens.append(URLToken(line, column, value, repr))
                    if error is not None:
                        tokens.append(ParseError(line, column, *error))
                    continue
            arguments = []
            tokens.append(FunctionBlock(line, column, value, arguments))
            stack.append((tokens, end_char))
            end_char = ')'
            tokens = arguments
            continue

        match = _NUMBER_RE.match(css, pos)
        if match:
            pos = match.end()
            repr_ = css[token_start_pos:pos]
            value = float(repr_)
            int_value = int(repr_) if not any(match.groups()) else None
            if pos < length and _is_ident_start(css, pos):
                unit, pos = _consume_ident(css, pos)
                tokens.append(DimensionToken(
                    line, column, value, int_value, repr_, unit))
            elif css.startswith('%', pos):
                pos += 1
                tokens.append(PercentageToken(
                    line, column, value, int_value, repr_))
            else:
                tokens.append(NumberToken(
                    line, column, value, int_value, repr_))
        elif c == '@':
            pos += 1
            if pos < length and _is_ident_start(css, pos):
                value, pos = _consume_ident(css, pos)
                tokens.append(AtKeywordToken(line, column, value))
            else:
                tokens.append(LiteralToken(line, column, '@'))
        elif c == '#':
            pos += 1
            if pos < length and (
                    css[pos] in '0123456789abcdefghijklmnopqrstuvwxyz'
                                '-_ABCDEFGHIJKLMNOPQRSTUVWXYZ' or
                    ord(css[pos]) > 0x7F or  # Non-ASCII
                    # Valid escape:
                    (css[pos] == '\\' and not css.startswith('\\\n', pos))):
                is_identifier = _is_ident_start(css, pos)
                value, pos = _consume_ident(css, pos)
                tokens.append(HashToken(line, column, value, is_identifier))
            else:
                tokens.append(LiteralToken(line, column, '#'))
        elif c == '{':
            content = []
            tokens.append(CurlyBracketsBlock(line, column, content))
            stack.append((tokens, end_char))
            end_char = '}'
            tokens = content
            pos += 1
        elif c == '[':
            content = []
            tokens.append(SquareBracketsBlock(line, column, content))
            stack.append((tokens, end_char))
            end_char = ']'
            tokens = content
            pos += 1
        elif c == '(':
            content = []
            tokens.append(ParenthesesBlock(line, column, content))
            stack.append((tokens, end_char))
            end_char = ')'
            tokens = content
            pos += 1
        elif c == end_char:  # Matching }, ] or )
            # The top-level end_char is None (never equal to a character),
            # so we never get here if the stack is empty.
            tokens, end_char = stack.pop()
            pos += 1
        elif c in '}])':
            tokens.append(ParseError(line, column, c, 'Unmatched ' + c))
            pos += 1
        elif c in ('"', "'"):
            value, pos, error = _consume_quoted_string(css, pos)
            if value is not None:
                repr = '"{}"'.format(serialize_string_value(value))
                if error is not None:
                    repr = repr[:-1]
                tokens.append(StringToken(line, column, value, repr))
            if error is not None:
                tokens.append(ParseError(line, column, *error))
        elif css.startswith('/*', pos):  # Comment
            pos = css.find('*/', pos + 2)
            if pos == -1:
                if not skip_comments:
                    tokens.append(
                        Comment(line, column, css[token_start_pos + 2:]))
                break
            if not skip_comments:
                tokens.append(
                    Comment(line, column, css[token_start_pos + 2:pos]))
            pos += 2
        elif css.startswith('<!--', pos):
            tokens.append(LiteralToken(line, column, '<!--'))
            pos += 4
        elif css.startswith('||', pos):
            tokens.append(LiteralToken(line, column, '||'))
            pos += 2
        elif c in '~|^$*':
            pos += 1
            if css.startswith('=', pos):
                pos += 1
                tokens.append(LiteralToken(line, column, c + '='))
            else:
                tokens.append(LiteralToken(line, column, c))
        else:
            tokens.append(LiteralToken(line, column, c))
            pos += 1
    return root
Ejemplo n.º 13
0
def parse_component_value_list(css, skip_comments=False):
    """Parse a list of component values.

    :param css: A :term:`string`.
    :param skip_comments:
        Ignore CSS comments.
        The return values (and recursively its blocks and functions)
        will not contain any :class:`~tinycss2.ast.Comment` object.
    :returns: A list of :term:`component values`.

    """
    css = (css.replace('\0', '\uFFFD')
           # This turns out to be faster than a regexp:
           .replace('\r\n', '\n').replace('\r', '\n').replace('\f', '\n'))
    length = len(css)
    token_start_pos = pos = 0  # Character index in the css source.
    line = 1  # First line is line 1.
    last_newline = -1
    root = tokens = []
    end_char = None  # Pop the stack when encountering this character.
    stack = []  # Stack of nested blocks: (tokens, end_char) tuples.

    while pos < length:
        newline = css.rfind('\n', token_start_pos, pos)
        if newline != -1:
            line += 1 + css.count('\n', token_start_pos, newline)
            last_newline = newline
        # First character in a line is in column 1.
        column = pos - last_newline
        token_start_pos = pos
        c = css[pos]

        if c in ' \n\t':
            pos += 1
            while css.startswith((' ', '\n', '\t'), pos):
                pos += 1
            value = css[token_start_pos:pos]
            tokens.append(WhitespaceToken(line, column, value))
            continue
        elif (c in 'Uu' and pos + 2 < length and css[pos + 1] == '+' and
              css[pos + 2] in '0123456789abcdefABCDEF?'):
            start, end, pos = _consume_unicode_range(css, pos + 2)
            tokens.append(UnicodeRangeToken(line, column, start, end))
            continue
        elif css.startswith('-->', pos):  # Check before identifiers
            tokens.append(LiteralToken(line, column, '-->'))
            pos += 3
            continue
        elif _is_ident_start(css, pos):
            value, pos = _consume_ident(css, pos)
            if not css.startswith('(', pos):  # Not a function
                tokens.append(IdentToken(line, column, value))
                continue
            pos += 1  # Skip the '('
            if ascii_lower(value) == 'url':
                value, pos = _consume_url(css, pos)
                tokens.append(
                    URLToken(line, column, value) if value is not None
                    else ParseError(line, column, 'bad-url', 'bad URL token'))
                continue
            arguments = []
            tokens.append(FunctionBlock(line, column, value, arguments))
            stack.append((tokens, end_char))
            end_char = ')'
            tokens = arguments
            continue

        match = _NUMBER_RE.match(css, pos)
        if match:
            pos = match.end()
            repr_ = css[token_start_pos:pos]
            value = float(repr_)
            int_value = int(repr_) if not any(match.groups()) else None
            if pos < length and _is_ident_start(css, pos):
                unit, pos = _consume_ident(css, pos)
                tokens.append(DimensionToken(
                    line, column, value, int_value, repr_, unit))
            elif css.startswith('%', pos):
                pos += 1
                tokens.append(PercentageToken(
                    line, column, value, int_value, repr_))
            else:
                tokens.append(NumberToken(
                    line, column, value, int_value, repr_))
        elif c == '@':
            pos += 1
            if pos < length and _is_ident_start(css, pos):
                value, pos = _consume_ident(css, pos)
                tokens.append(AtKeywordToken(line, column, value))
            else:
                tokens.append(LiteralToken(line, column, '@'))
        elif c == '#':
            pos += 1
            if pos < length and (
                    css[pos] in '0123456789abcdefghijklmnopqrstuvwxyz'
                                '-_ABCDEFGHIJKLMNOPQRSTUVWXYZ' or
                    ord(css[pos]) > 0x7F or  # Non-ASCII
                    # Valid escape:
                    (css[pos] == '\\' and not css.startswith('\\\n', pos))):
                is_identifier = _is_ident_start(css, pos)
                value, pos = _consume_ident(css, pos)
                tokens.append(HashToken(line, column, value, is_identifier))
            else:
                tokens.append(LiteralToken(line, column, '#'))
        elif c == '{':
            content = []
            tokens.append(CurlyBracketsBlock(line, column, content))
            stack.append((tokens, end_char))
            end_char = '}'
            tokens = content
            pos += 1
        elif c == '[':
            content = []
            tokens.append(SquareBracketsBlock(line, column, content))
            stack.append((tokens, end_char))
            end_char = ']'
            tokens = content
            pos += 1
        elif c == '(':
            content = []
            tokens.append(ParenthesesBlock(line, column, content))
            stack.append((tokens, end_char))
            end_char = ')'
            tokens = content
            pos += 1
        elif c == end_char:  # Matching }, ] or )
            # The top-level end_char is None (never equal to a character),
            # so we never get here if the stack is empty.
            tokens, end_char = stack.pop()
            pos += 1
        elif c in '}])':
            tokens.append(ParseError(line, column, c, 'Unmatched ' + c))
            pos += 1
        elif c in ('"', "'"):
            value, pos = _consume_quoted_string(css, pos)
            tokens.append(
                StringToken(line, column, value) if value is not None
                else ParseError(line, column, 'bad-string',
                                'bad string token'))
        elif css.startswith('/*', pos):  # Comment
            pos = css.find('*/', pos + 2)
            if pos == -1:
                if not skip_comments:
                    tokens.append(
                        Comment(line, column, css[token_start_pos + 2:]))
                break
            if not skip_comments:
                tokens.append(
                    Comment(line, column, css[token_start_pos + 2:pos]))
            pos += 2
        elif css.startswith('<!--', pos):
            tokens.append(LiteralToken(line, column, '<!--'))
            pos += 4
        elif css.startswith('||', pos):
            tokens.append(LiteralToken(line, column, '||'))
            pos += 2
        elif c in '~|^$*':
            pos += 1
            if css.startswith('=', pos):
                pos += 1
                tokens.append(LiteralToken(line, column, c + '='))
            else:
                tokens.append(LiteralToken(line, column, c))
        else:
            tokens.append(LiteralToken(line, column, c))
            pos += 1
    return root
Ejemplo n.º 14
0
 def __init__(self, line, column, name, arguments):
     Node.__init__(self, line, column)
     self.name = name
     self.lower_name = ascii_lower(name)
     self.arguments = arguments
Ejemplo n.º 15
0
 def __init__(self, line, column, value):
     Node.__init__(self, line, column)
     self.value = value
     self.lower_value = ascii_lower(value)
Ejemplo n.º 16
0
def _compile_node(selector):
    """Return a boolean expression, as a Python source string.

    When evaluated in a context where the `el` variable is an
    :class:`cssselect2.tree.Element` object,
    tells whether the element is a subject of `selector`.

    """
    # To avoid precedence-related bugs, any sub-expression that is passed
    # around must be "atomic": add parentheses when the top-level would be
    # an operator. Bare literals and function calls are fine.

    # 1 and 0 are used for True and False to avoid global lookups.

    if isinstance(selector, parser.CombinedSelector):
        left_inside = _compile_node(selector.left)
        if left_inside == '0':
            return '0'  # 0 and x == 0
        elif left_inside == '1':
            # 1 and x == x, but the element matching 1 still needs to exist.
            if selector.combinator in (' ', '>'):
                left = 'el.parent is not None'
            elif selector.combinator in ('~', '+'):
                left = 'el.previous is not None'
            else:
                raise SelectorError('Unknown combinator', selector.combinator)
        # Rebind the `el` name inside a generator-expressions (in a new scope)
        # so that 'left_inside' applies to different elements.
        elif selector.combinator == ' ':
            left = 'any((%s) for el in el.ancestors)' % left_inside
        elif selector.combinator == '>':
            left = ('next(el is not None and (%s) for el in [el.parent])' %
                    left_inside)
        elif selector.combinator == '+':
            left = ('next(el is not None and (%s) for el in [el.previous])' %
                    left_inside)
        elif selector.combinator == '~':
            left = 'any((%s) for el in el.previous_siblings)' % left_inside
        else:
            raise SelectorError('Unknown combinator', selector.combinator)

        right = _compile_node(selector.right)
        if right == '0':
            return '0'  # 0 and x == 0
        elif right == '1':
            return left  # 1 and x == x
        else:
            # Evaluate combinators right to left:
            return '(%s) and (%s)' % (right, left)

    elif isinstance(selector, parser.CompoundSelector):
        sub_expressions = [
            expr for expr in map(_compile_node, selector.simple_selectors)
            if expr != '1'
        ]
        if len(sub_expressions) == 1:
            test = sub_expressions[0]
        elif '0' in sub_expressions:
            test = '0'
        elif sub_expressions:
            test = ' and '.join('(%s)' % e for e in sub_expressions)
        else:
            test = '1'  # all([]) == True
        return test

    elif isinstance(selector, parser.NegationSelector):
        sub_expressions = [
            expr for expr in map(_compile_node, selector.selector_list)
            if expr != '1'
        ]
        if not sub_expressions:
            return '0'
        return f'not ({" or ".join(f"({expr})" for expr in sub_expressions)})'

    elif isinstance(
            selector,
        (parser.MatchesAnySelector, parser.SpecificityAdjustmentSelector)):
        sub_expressions = [
            expr for expr in map(_compile_node, selector.selector_list)
            if expr != '0'
        ]
        if not sub_expressions:
            return '0'
        return ' or '.join(f'({expr})' for expr in sub_expressions)

    elif isinstance(selector, parser.LocalNameSelector):
        if selector.lower_local_name == selector.local_name:
            return 'el.local_name == %r' % selector.local_name
        else:
            return ('el.local_name == (%r if el.in_html_document else %r)' %
                    (selector.lower_local_name, selector.local_name))

    elif isinstance(selector, parser.NamespaceSelector):
        return 'el.namespace_url == %r' % selector.namespace

    elif isinstance(selector, parser.ClassSelector):
        return '%r in el.classes' % selector.class_name

    elif isinstance(selector, parser.IDSelector):
        return 'el.id == %r' % selector.ident

    elif isinstance(selector, parser.AttributeSelector):
        if selector.namespace is not None:
            if selector.namespace:
                if selector.name == selector.lower_name:
                    key = repr('{%s}%s' % (selector.namespace, selector.name))
                else:
                    key = '(%r if el.in_html_document else %r)' % (
                        '{%s}%s' % (selector.namespace, selector.lower_name),
                        '{%s}%s' % (selector.namespace, selector.name),
                    )
            else:
                if selector.name == selector.lower_name:
                    key = repr(selector.name)
                else:
                    key = '(%r if el.in_html_document else %r)' % (
                        selector.lower_name, selector.name)
            value = selector.value
            if selector.operator is None:
                return '%s in el.etree_element.attrib' % key
            elif selector.operator == '=':
                return 'el.etree_element.get(%s) == %r' % (key, value)
            elif selector.operator == '~=':
                if len(value.split()) != 1 or value.strip() != value:
                    return '0'
                else:
                    return (
                        '%r in split_whitespace(el.etree_element.get(%s, ""))'
                        % (value, key))
            elif selector.operator == '|=':
                return ('next(v == %r or (v is not None and v.startswith(%r))'
                        '     for v in [el.etree_element.get(%s)])' %
                        (value, value + '-', key))
            elif selector.operator == '^=':
                if value:
                    return 'el.etree_element.get(%s, "").startswith(%r)' % (
                        key, value)
                else:
                    return '0'
            elif selector.operator == '$=':
                if value:
                    return 'el.etree_element.get(%s, "").endswith(%r)' % (
                        key, value)
                else:
                    return '0'
            elif selector.operator == '*=':
                if value:
                    return '%r in el.etree_element.get(%s, "")' % (value, key)
                else:
                    return '0'
            else:
                raise SelectorError('Unknown attribute operator',
                                    selector.operator)
        else:  # In any namespace
            raise NotImplementedError  # TODO

    elif isinstance(selector, parser.PseudoClassSelector):
        if selector.name in ('link', 'any-link', 'local-link'):
            test = '%s and el.etree_element.get("href") is not None '
            if selector.name == 'local-link':
                test += 'and not urlparse(el.etree_element.get("href")).scheme'
            return test % html_tag_eq('a', 'area', 'link')
        elif selector.name == 'enabled':
            return ('(%s and el.etree_element.get("disabled") is None'
                    ' and not el.in_disabled_fieldset) or'
                    '(%s and el.etree_element.get("disabled") is None) or '
                    '(%s and el.etree_element.get("href") is not None)' % (
                        html_tag_eq('button', 'input', 'select', 'textarea',
                                    'option'),
                        html_tag_eq('optgroup', 'menuitem', 'fieldset'),
                        html_tag_eq('a', 'area', 'link'),
                    ))
        elif selector.name == 'disabled':
            return ('(%s and (el.etree_element.get("disabled") is not None'
                    ' or el.in_disabled_fieldset)) or'
                    '(%s and el.etree_element.get("disabled") is not None)' % (
                        html_tag_eq('button', 'input', 'select', 'textarea',
                                    'option'),
                        html_tag_eq('optgroup', 'menuitem', 'fieldset'),
                    ))
        elif selector.name == 'checked':
            return (
                '(%s and el.etree_element.get("checked") is not None and'
                ' ascii_lower(el.etree_element.get("type", "")) '
                ' in ("checkbox", "radio"))'
                'or (%s and el.etree_element.get("selected") is not None)' % (
                    html_tag_eq('input', 'menuitem'),
                    html_tag_eq('option'),
                ))
        elif selector.name in ('visited', 'hover', 'active', 'focus',
                               'focus-within', 'focus-visible', 'target',
                               'target-within', 'current', 'past', 'future',
                               'playing', 'paused', 'seeking', 'buffering',
                               'stalled', 'muted', 'volume-locked',
                               'user-valid', 'user-invalid'):
            # Not applicable in a static context: never match.
            return '0'
        elif selector.name in ('root', 'scope'):
            return 'el.parent is None'
        elif selector.name == 'first-child':
            return 'el.index == 0'
        elif selector.name == 'last-child':
            return 'el.index + 1 == len(el.etree_siblings)'
        elif selector.name == 'first-of-type':
            return ('all(s.tag != el.etree_element.tag'
                    '    for s in el.etree_siblings[:el.index])')
        elif selector.name == 'last-of-type':
            return ('all(s.tag != el.etree_element.tag'
                    '    for s in el.etree_siblings[el.index + 1:])')
        elif selector.name == 'only-child':
            return 'len(el.etree_siblings) == 1'
        elif selector.name == 'only-of-type':
            return ('all(s.tag != el.etree_element.tag or i == el.index'
                    '    for i, s in enumerate(el.etree_siblings))')
        elif selector.name == 'empty':
            return 'not (el.etree_children or el.etree_element.text)'
        else:
            raise SelectorError('Unknown pseudo-class', selector.name)

    elif isinstance(selector, parser.FunctionalPseudoClassSelector):
        if selector.name == 'lang':
            langs = []
            tokens = [
                token for token in selector.arguments
                if token.type not in ('whitespace', 'comment')
            ]
            while tokens:
                token = tokens.pop(0)
                if token.type == 'ident':
                    langs.append(token.lower_value)
                elif token.type == 'string':
                    langs.append(ascii_lower(token.value))
                else:
                    raise SelectorError('Invalid arguments for :lang()')
                if tokens:
                    token = tokens.pop(0)
                    if token.type != 'ident' and token.value != ',':
                        raise SelectorError('Invalid arguments for :lang()')
            return ' or '.join(
                f'el.lang == {lang!r} or el.lang.startswith({lang + "-"!r})'
                for lang in langs)
        else:
            nth = []
            selector_list = []
            current_list = nth
            for argument in selector.arguments:
                if argument.type == 'ident' and argument.value == 'of':
                    if current_list is nth:
                        current_list = selector_list
                        continue
                current_list.append(argument)

            if selector_list:
                test = ' and '.join(
                    _compile_node(selector.parsed_tree)
                    for selector in parser.parse(selector_list))
                if selector.name == 'nth-child':
                    count = (
                        f'sum(1 for el in el.previous_siblings if ({test}))')
                elif selector.name == 'nth-last-child':
                    count = ('sum(1 for el in'
                             '    tuple(el.iter_siblings())[el.index + 1:]'
                             f'   if ({test}))')
                elif selector.name == 'nth-of-type':
                    count = (
                        'sum(1 for s in ('
                        '      el for el in el.previous_siblings'
                        f'     if ({test}))'
                        '    if s.etree_element.tag == el.etree_element.tag)')
                elif selector.name == 'nth-last-of-type':
                    count = (
                        'sum(1 for s in ('
                        '      el for el in'
                        '      tuple(el.iter_siblings())[el.index + 1:]'
                        f'     if ({test}))'
                        '    if s.etree_element.tag == el.etree_element.tag)')
                else:
                    raise SelectorError('Unknown pseudo-class', selector.name)
                count += f'if ({test}) else float("nan")'
            else:
                if current_list is selector_list:
                    raise SelectorError('Invalid arguments for :%s()' %
                                        selector.name)
                if selector.name == 'nth-child':
                    count = 'el.index'
                elif selector.name == 'nth-last-child':
                    count = 'len(el.etree_siblings) - el.index - 1'
                elif selector.name == 'nth-of-type':
                    count = ('sum(1 for s in el.etree_siblings[:el.index]'
                             '    if s.tag == el.etree_element.tag)')
                elif selector.name == 'nth-last-of-type':
                    count = ('sum(1 for s in el.etree_siblings[el.index + 1:]'
                             '    if s.tag == el.etree_element.tag)')
                else:
                    raise SelectorError('Unknown pseudo-class', selector.name)

            result = parse_nth(nth)
            if result is None:
                raise SelectorError('Invalid arguments for :%s()' %
                                    selector.name)
            a, b = result
            # x is the number of siblings before/after the element
            # Matches if a positive or zero integer n exists so that:
            # x = a*n + b-1
            # x = a*n + B
            B = b - 1
            if a == 0:
                # x = B
                return '(%s) == %i' % (count, B)
            else:
                # n = (x - B) / a
                return ('next(r == 0 and n >= 0'
                        '     for n, r in [divmod((%s) - %i, %i)])' %
                        (count, B, a))

    else:
        raise TypeError(type(selector), selector)