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 ''
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 ''
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
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)
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
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
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
def __init__(self, line, column, name, arguments): Node.__init__(self, line, column) self.name = name self.lower_name = ascii_lower(name) self.arguments = arguments
def __init__(self, line, column, value): Node.__init__(self, line, column) self.value = value self.lower_value = ascii_lower(value)
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
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
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)