Best Python code snippet using autotest_python
css_parser.py
Source:css_parser.py  
1"""CSS selector parser."""2from __future__ import unicode_literals3import re4from . import util5from . import css_match as cm6from . import css_types as ct7from .util import SelectorSyntaxError8UNICODE_REPLACEMENT_CHAR = 0xFFFD9# Simple pseudo classes that take no parameters10PSEUDO_SIMPLE = {11    ":any-link",12    ":empty",13    ":first-child",14    ":first-of-type",15    ":in-range",16    ":out-of-range",17    ":last-child",18    ":last-of-type",19    ":link",20    ":only-child",21    ":only-of-type",22    ":root",23    ':checked',24    ':default',25    ':disabled',26    ':enabled',27    ':indeterminate',28    ':optional',29    ':placeholder-shown',30    ':read-only',31    ':read-write',32    ':required',33    ':scope',34    ':defined'35}36# Supported, simple pseudo classes that match nothing in the Soup Sieve environment37PSEUDO_SIMPLE_NO_MATCH = {38    ':active',39    ':current',40    ':focus',41    ':focus-visible',42    ':focus-within',43    ':future',44    ':host',45    ':hover',46    ':local-link',47    ':past',48    ':paused',49    ':playing',50    ':target',51    ':target-within',52    ':user-invalid',53    ':visited'54}55# Complex pseudo classes that take selector lists56PSEUDO_COMPLEX = {57    ':contains',58    ':has',59    ':is',60    ':matches',61    ':not',62    ':where'63}64PSEUDO_COMPLEX_NO_MATCH = {65    ':current',66    ':host',67    ':host-context'68}69# Complex pseudo classes that take very specific parameters and are handled special70PSEUDO_SPECIAL = {71    ':dir',72    ':lang',73    ':nth-child',74    ':nth-last-child',75    ':nth-last-of-type',76    ':nth-of-type'77}78PSEUDO_SUPPORTED = PSEUDO_SIMPLE | PSEUDO_SIMPLE_NO_MATCH | PSEUDO_COMPLEX | PSEUDO_COMPLEX_NO_MATCH | PSEUDO_SPECIAL79# Sub-patterns parts80# Whitespace81NEWLINE = r'(?:\r\n|(?!\r\n)[\n\f\r])'82WS = r'(?:[ \t]|{})'.format(NEWLINE)83# Comments84COMMENTS = r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)'85# Whitespace with comments included86WSC = r'(?:{ws}|{comments})'.format(ws=WS, comments=COMMENTS)87# CSS escapes88CSS_ESCAPES = r'(?:\\(?:[a-f0-9]{{1,6}}{ws}?|[^\r\n\f]|$))'.format(ws=WS)89CSS_STRING_ESCAPES = r'(?:\\(?:[a-f0-9]{{1,6}}{ws}?|[^\r\n\f]|$|{nl}))'.format(ws=WS, nl=NEWLINE)90# CSS Identifier91IDENTIFIER = r'''92(?:(?:-?(?:[^\x00-\x2f\x30-\x40\x5B-\x5E\x60\x7B-\x9f]|{esc})+|--)93(?:[^\x00-\x2c\x2e\x2f\x3A-\x40\x5B-\x5E\x60\x7B-\x9f]|{esc})*)94'''.format(esc=CSS_ESCAPES)95# `nth` content96NTH = r'(?:[-+])?(?:[0-9]+n?|n)(?:(?<=n){ws}*(?:[-+]){ws}*(?:[0-9]+))?'.format(ws=WSC)97# Value: quoted string or identifier98VALUE = r'''99(?:"(?:\\(?:.|{nl})|[^\\"\r\n\f]+)*?"|'(?:\\(?:.|{nl})|[^\\'\r\n\f]+)*?'|{ident}+)100'''.format(nl=NEWLINE, ident=IDENTIFIER)101# Attribute value comparison. `!=` is handled special as it is non-standard.102ATTR = r'''103(?:{ws}*(?P<cmp>[!~^|*$]?=){ws}*(?P<value>{value})(?:{ws}+(?P<case>[is]))?)?{ws}*\]104'''.format(ws=WSC, value=VALUE)105# Selector patterns106# IDs (`#id`)107PAT_ID = r'\#{ident}'.format(ident=IDENTIFIER)108# Classes (`.class`)109PAT_CLASS = r'\.{ident}'.format(ident=IDENTIFIER)110# Prefix:Tag (`prefix|tag`)111PAT_TAG = r'(?:(?:{ident}|\*)?\|)?(?:{ident}|\*)'.format(ident=IDENTIFIER)112# Attributes (`[attr]`, `[attr=value]`, etc.)113PAT_ATTR = r'\[{ws}*(?P<ns_attr>(?:(?:{ident}|\*)?\|)?{ident}){attr}'.format(ws=WSC, ident=IDENTIFIER, attr=ATTR)114# Pseudo class (`:pseudo-class`, `:pseudo-class(`)115PAT_PSEUDO_CLASS = r'(?P<name>:{ident})(?P<open>\({ws}*)?'.format(ws=WSC, ident=IDENTIFIER)116# Pseudo class special patterns. Matches `:pseudo-class(` for special case pseudo classes.117PAT_PSEUDO_CLASS_SPECIAL = r'(?P<name>:{ident})(?P<open>\({ws}*)'.format(ws=WSC, ident=IDENTIFIER)118# Custom pseudo class (`:--custom-pseudo`)119PAT_PSEUDO_CLASS_CUSTOM = r'(?P<name>:(?=--){ident})'.format(ident=IDENTIFIER)120# Closing pseudo group (`)`)121PAT_PSEUDO_CLOSE = r'{ws}*\)'.format(ws=WSC)122# Pseudo element (`::pseudo-element`)123PAT_PSEUDO_ELEMENT = r':{}'.format(PAT_PSEUDO_CLASS)124# At rule (`@page`, etc.) (not supported)125PAT_AT_RULE = r'@P{ident}'.format(ident=IDENTIFIER)126# Pseudo class `nth-child` (`:nth-child(an+b [of S]?)`, `:first-child`, etc.)127PAT_PSEUDO_NTH_CHILD = r'''128(?P<pseudo_nth_child>{name}129(?P<nth_child>{nth}|even|odd))(?:{wsc}*\)|(?P<of>{comments}*{ws}{wsc}*of{comments}*{ws}{wsc}*))130'''.format(name=PAT_PSEUDO_CLASS_SPECIAL, wsc=WSC, comments=COMMENTS, ws=WS, nth=NTH)131# Pseudo class `nth-of-type` (`:nth-of-type(an+b)`, `:first-of-type`, etc.)132PAT_PSEUDO_NTH_TYPE = r'''133(?P<pseudo_nth_type>{name}134(?P<nth_type>{nth}|even|odd)){ws}*\)135'''.format(name=PAT_PSEUDO_CLASS_SPECIAL, ws=WSC, nth=NTH)136# Pseudo class language (`:lang("*-de", en)`)137PAT_PSEUDO_LANG = r'{name}(?P<values>{value}(?:{ws}*,{ws}*{value})*){ws}*\)'.format(138    name=PAT_PSEUDO_CLASS_SPECIAL, ws=WSC, value=VALUE139)140# Pseudo class direction (`:dir(ltr)`)141PAT_PSEUDO_DIR = r'{name}(?P<dir>ltr|rtl){ws}*\)'.format(name=PAT_PSEUDO_CLASS_SPECIAL, ws=WSC)142# Combining characters (`>`, `~`, ` `, `+`, `,`)143PAT_COMBINE = r'{wsc}*?(?P<relation>[,+>~]|{ws}(?![,+>~])){wsc}*'.format(ws=WS, wsc=WSC)144# Extra: Contains (`:contains(text)`)145PAT_PSEUDO_CONTAINS = r'{name}(?P<values>{value}(?:{ws}*,{ws}*{value})*){ws}*\)'.format(146    name=PAT_PSEUDO_CLASS_SPECIAL, ws=WSC, value=VALUE147)148# Regular expressions149# CSS escape pattern150RE_CSS_ESC = re.compile(r'(?:(\\[a-f0-9]{{1,6}}{ws}?)|(\\[^\r\n\f])|(\\$))'.format(ws=WSC), re.I)151RE_CSS_STR_ESC = re.compile(152    r'(?:(\\[a-f0-9]{{1,6}}{ws}?)|(\\[^\r\n\f])|(\\$)|(\\{nl}))'.format(ws=WS, nl=NEWLINE), re.I153)154# Pattern to break up `nth` specifiers155RE_NTH = re.compile(156    r'(?P<s1>[-+])?(?P<a>[0-9]+n?|n)(?:(?<=n){ws}*(?P<s2>[-+]){ws}*(?P<b>[0-9]+))?'.format(ws=WSC),157    re.I158)159# Pattern to iterate multiple values.160RE_VALUES = re.compile(r'(?:(?P<value>{value})|(?P<split>{ws}*,{ws}*))'.format(ws=WSC, value=VALUE), re.X)161# Whitespace checks162RE_WS = re.compile(WS)163RE_WS_BEGIN = re.compile('^{}*'.format(WSC))164RE_WS_END = re.compile('{}*$'.format(WSC))165RE_CUSTOM = re.compile(r'^{}$'.format(PAT_PSEUDO_CLASS_CUSTOM), re.X)166# Constants167# List split token168COMMA_COMBINATOR = ','169# Relation token for descendant170WS_COMBINATOR = " "171# Parse flags172FLG_PSEUDO = 0x01173FLG_NOT = 0x02174FLG_RELATIVE = 0x04175FLG_DEFAULT = 0x08176FLG_HTML = 0x10177FLG_INDETERMINATE = 0x20178FLG_OPEN = 0x40179FLG_IN_RANGE = 0x80180FLG_OUT_OF_RANGE = 0x100181# Maximum cached patterns to store182_MAXCACHE = 500183@util.lru_cache(maxsize=_MAXCACHE)184def _cached_css_compile(pattern, namespaces, custom, flags):185    """Cached CSS compile."""186    custom_selectors = process_custom(custom)187    return cm.SoupSieve(188        pattern,189        CSSParser(pattern, custom=custom_selectors, flags=flags).process_selectors(),190        namespaces,191        custom,192        flags193    )194def _purge_cache():195    """Purge the cache."""196    _cached_css_compile.cache_clear()197def process_custom(custom):198    """Process custom."""199    custom_selectors = {}200    if custom is not None:201        for key, value in custom.items():202            name = util.lower(key)203            if RE_CUSTOM.match(name) is None:204                raise SelectorSyntaxError("The name '{}' is not a valid custom pseudo-class name".format(name))205            if name in custom_selectors:206                raise KeyError("The custom selector '{}' has already been registered".format(name))207            custom_selectors[css_unescape(name)] = value208    return custom_selectors209def css_unescape(content, string=False):210    """211    Unescape CSS value.212    Strings allow for spanning the value on multiple strings by escaping a new line.213    """214    def replace(m):215        """Replace with the appropriate substitute."""216        if m.group(1):217            codepoint = int(m.group(1)[1:], 16)218            if codepoint == 0:219                codepoint = UNICODE_REPLACEMENT_CHAR220            value = util.uchr(codepoint)221        elif m.group(2):222            value = m.group(2)[1:]223        elif m.group(3):224            value = '\ufffd'225        else:226            value = ''227        return value228    return (RE_CSS_ESC if not string else RE_CSS_STR_ESC).sub(replace, content)229def escape(ident):230    """Escape identifier."""231    string = []232    length = len(ident)233    start_dash = length > 0 and ident[0] == '-'234    if length == 1 and start_dash:235        # Need to escape identifier that is a single `-` with no other characters236        string.append('\\{}'.format(ident))237    else:238        for index, c in enumerate(ident):239            codepoint = util.uord(c)240            if codepoint == 0x00:241                string.append('\ufffd')242            elif (0x01 <= codepoint <= 0x1F) or codepoint == 0x7F:243                string.append('\\{:x} '.format(codepoint))244            elif (index == 0 or (start_dash and index == 1)) and (0x30 <= codepoint <= 0x39):245                string.append('\\{:x} '.format(codepoint))246            elif (247                codepoint in (0x2D, 0x5F) or codepoint >= 0x80 or (0x30 <= codepoint <= 0x39) or248                (0x30 <= codepoint <= 0x39) or (0x41 <= codepoint <= 0x5A) or (0x61 <= codepoint <= 0x7A)249            ):250                string.append(c)251            else:252                string.append('\\{}'.format(c))253    return ''.join(string)254class SelectorPattern(object):255    """Selector pattern."""256    def __init__(self, name, pattern):257        """Initialize."""258        self.name = name259        self.re_pattern = re.compile(pattern, re.I | re.X | re.U)260    def get_name(self):261        """Get name."""262        return self.name263    def enabled(self, flags):264        """Enabled."""265        return True266    def match(self, selector, index):267        """Match the selector."""268        return self.re_pattern.match(selector, index)269class SpecialPseudoPattern(SelectorPattern):270    """Selector pattern."""271    def __init__(self, patterns):272        """Initialize."""273        self.patterns = {}274        for p in patterns:275            name = p[0]276            pattern = SelectorPattern(name, p[2])277            for pseudo in p[1]:278                self.patterns[pseudo] = pattern279        self.matched_name = None280        self.re_pseudo_name = re.compile(PAT_PSEUDO_CLASS_SPECIAL, re.I | re.X | re.U)281    def get_name(self):282        """Get name."""283        return self.matched_name.get_name()284    def enabled(self, flags):285        """Enabled."""286        return True287    def match(self, selector, index):288        """Match the selector."""289        pseudo = None290        m = self.re_pseudo_name.match(selector, index)291        if m:292            name = util.lower(css_unescape(m.group('name')))293            pattern = self.patterns.get(name)294            if pattern:295                pseudo = pattern.match(selector, index)296                if pseudo:297                    self.matched_name = pattern298        return pseudo299class _Selector(object):300    """301    Intermediate selector class.302    This stores selector data for a compound selector as we are acquiring them.303    Once we are done collecting the data for a compound selector, we freeze304    the data in an object that can be pickled and hashed.305    """306    def __init__(self, **kwargs):307        """Initialize."""308        self.tag = kwargs.get('tag', None)309        self.ids = kwargs.get('ids', [])310        self.classes = kwargs.get('classes', [])311        self.attributes = kwargs.get('attributes', [])312        self.nth = kwargs.get('nth', [])313        self.selectors = kwargs.get('selectors', [])314        self.relations = kwargs.get('relations', [])315        self.rel_type = kwargs.get('rel_type', None)316        self.contains = kwargs.get('contains', [])317        self.lang = kwargs.get('lang', [])318        self.flags = kwargs.get('flags', 0)319        self.no_match = kwargs.get('no_match', False)320    def _freeze_relations(self, relations):321        """Freeze relation."""322        if relations:323            sel = relations[0]324            sel.relations.extend(relations[1:])325            return ct.SelectorList([sel.freeze()])326        else:327            return ct.SelectorList()328    def freeze(self):329        """Freeze self."""330        if self.no_match:331            return ct.SelectorNull()332        else:333            return ct.Selector(334                self.tag,335                tuple(self.ids),336                tuple(self.classes),337                tuple(self.attributes),338                tuple(self.nth),339                tuple(self.selectors),340                self._freeze_relations(self.relations),341                self.rel_type,342                tuple(self.contains),343                tuple(self.lang),344                self.flags345            )346    def __str__(self):  # pragma: no cover347        """String representation."""348        return (349            '_Selector(tag={!r}, ids={!r}, classes={!r}, attributes={!r}, nth={!r}, selectors={!r}, '350            'relations={!r}, rel_type={!r}, contains={!r}, lang={!r}, flags={!r}, no_match={!r})'351        ).format(352            self.tag, self.ids, self.classes, self.attributes, self.nth, self.selectors,353            self.relations, self.rel_type, self.contains, self.lang, self.flags, self.no_match354        )355    __repr__ = __str__356class CSSParser(object):357    """Parse CSS selectors."""358    css_tokens = (359        SelectorPattern("pseudo_close", PAT_PSEUDO_CLOSE),360        SpecialPseudoPattern(361            (362                ("pseudo_contains", (':contains',), PAT_PSEUDO_CONTAINS),363                ("pseudo_nth_child", (':nth-child', ':nth-last-child'), PAT_PSEUDO_NTH_CHILD),364                ("pseudo_nth_type", (':nth-of-type', ':nth-last-of-type'), PAT_PSEUDO_NTH_TYPE),365                ("pseudo_lang", (':lang',), PAT_PSEUDO_LANG),366                ("pseudo_dir", (':dir',), PAT_PSEUDO_DIR)367            )368        ),369        SelectorPattern("pseudo_class_custom", PAT_PSEUDO_CLASS_CUSTOM),370        SelectorPattern("pseudo_class", PAT_PSEUDO_CLASS),371        SelectorPattern("pseudo_element", PAT_PSEUDO_ELEMENT),372        SelectorPattern("at_rule", PAT_AT_RULE),373        SelectorPattern("id", PAT_ID),374        SelectorPattern("class", PAT_CLASS),375        SelectorPattern("tag", PAT_TAG),376        SelectorPattern("attribute", PAT_ATTR),377        SelectorPattern("combine", PAT_COMBINE)378    )379    def __init__(self, selector, custom=None, flags=0):380        """Initialize."""381        self.pattern = selector.replace('\x00', '\ufffd')382        self.flags = flags383        self.debug = self.flags & util.DEBUG384        self.custom = {} if custom is None else custom385    def parse_attribute_selector(self, sel, m, has_selector):386        """Create attribute selector from the returned regex match."""387        inverse = False388        op = m.group('cmp')389        case = util.lower(m.group('case')) if m.group('case') else None390        parts = [css_unescape(a) for a in m.group('ns_attr').split('|')]391        ns = ''392        is_type = False393        pattern2 = None394        if len(parts) > 1:395            ns = parts[0]396            attr = parts[1]397        else:398            attr = parts[0]399        if case:400            flags = re.I if case == 'i' else 0401        elif util.lower(attr) == 'type':402            flags = re.I403            is_type = True404        else:405            flags = 0406        if op:407            if m.group('value').startswith(('"', "'")):408                value = css_unescape(m.group('value')[1:-1], True)409            else:410                value = css_unescape(m.group('value'))411        else:412            value = None413        if not op:414            # Attribute name415            pattern = None416        elif op.startswith('^'):417            # Value start with418            pattern = re.compile(r'^%s.*' % re.escape(value), flags)419        elif op.startswith('$'):420            # Value ends with421            pattern = re.compile(r'.*?%s$' % re.escape(value), flags)422        elif op.startswith('*'):423            # Value contains424            pattern = re.compile(r'.*?%s.*' % re.escape(value), flags)425        elif op.startswith('~'):426            # Value contains word within space separated list427            # `~=` should match nothing if it is empty or contains whitespace,428            # so if either of these cases is present, use `[^\s\S]` which cannot be matched.429            value = r'[^\s\S]' if not value or RE_WS.search(value) else re.escape(value)430            pattern = re.compile(r'.*?(?:(?<=^)|(?<=[ \t\r\n\f]))%s(?=(?:[ \t\r\n\f]|$)).*' % value, flags)431        elif op.startswith('|'):432            # Value starts with word in dash separated list433            pattern = re.compile(r'^%s(?:-.*)?$' % re.escape(value), flags)434        else:435            # Value matches436            pattern = re.compile(r'^%s$' % re.escape(value), flags)437            if op.startswith('!'):438                # Equivalent to `:not([attr=value])`439                inverse = True440        if is_type and pattern:441            pattern2 = re.compile(pattern.pattern)442        # Append the attribute selector443        sel_attr = ct.SelectorAttribute(attr, ns, pattern, pattern2)444        if inverse:445            # If we are using `!=`, we need to nest the pattern under a `:not()`.446            sub_sel = _Selector()447            sub_sel.attributes.append(sel_attr)448            not_list = ct.SelectorList([sub_sel.freeze()], True, False)449            sel.selectors.append(not_list)450        else:451            sel.attributes.append(sel_attr)452        has_selector = True453        return has_selector454    def parse_tag_pattern(self, sel, m, has_selector):455        """Parse tag pattern from regex match."""456        parts = [css_unescape(x) for x in m.group(0).split('|')]457        if len(parts) > 1:458            prefix = parts[0]459            tag = parts[1]460        else:461            tag = parts[0]462            prefix = None463        sel.tag = ct.SelectorTag(tag, prefix)464        has_selector = True465        return has_selector466    def parse_pseudo_class_custom(self, sel, m, has_selector):467        """468        Parse custom pseudo class alias.469        Compile custom selectors as we need them. When compiling a custom selector,470        set it to `None` in the dictionary so we can avoid an infinite loop.471        """472        pseudo = util.lower(css_unescape(m.group('name')))473        selector = self.custom.get(pseudo)474        if selector is None:475            raise SelectorSyntaxError(476                "Undefined custom selector '{}' found at postion {}".format(pseudo, m.end(0)),477                self.pattern,478                m.end(0)479            )480        if not isinstance(selector, ct.SelectorList):481            self.custom[pseudo] = None482            selector = CSSParser(483                selector, custom=self.custom, flags=self.flags484            ).process_selectors(flags=FLG_PSEUDO)485            self.custom[pseudo] = selector486        sel.selectors.append(selector)487        has_selector = True488        return has_selector489    def parse_pseudo_class(self, sel, m, has_selector, iselector, is_html):490        """Parse pseudo class."""491        complex_pseudo = False492        pseudo = util.lower(css_unescape(m.group('name')))493        if m.group('open'):494            complex_pseudo = True495        if complex_pseudo and pseudo in PSEUDO_COMPLEX:496            has_selector = self.parse_pseudo_open(sel, pseudo, has_selector, iselector, m.end(0))497        elif not complex_pseudo and pseudo in PSEUDO_SIMPLE:498            if pseudo == ':root':499                sel.flags |= ct.SEL_ROOT500            elif pseudo == ':defined':501                sel.flags |= ct.SEL_DEFINED502                is_html = True503            elif pseudo == ':scope':504                sel.flags |= ct.SEL_SCOPE505            elif pseudo == ':empty':506                sel.flags |= ct.SEL_EMPTY507            elif pseudo in (':link', ':any-link'):508                sel.selectors.append(CSS_LINK)509            elif pseudo == ':checked':510                sel.selectors.append(CSS_CHECKED)511            elif pseudo == ':default':512                sel.selectors.append(CSS_DEFAULT)513            elif pseudo == ':indeterminate':514                sel.selectors.append(CSS_INDETERMINATE)515            elif pseudo == ":disabled":516                sel.selectors.append(CSS_DISABLED)517            elif pseudo == ":enabled":518                sel.selectors.append(CSS_ENABLED)519            elif pseudo == ":required":520                sel.selectors.append(CSS_REQUIRED)521            elif pseudo == ":optional":522                sel.selectors.append(CSS_OPTIONAL)523            elif pseudo == ":read-only":524                sel.selectors.append(CSS_READ_ONLY)525            elif pseudo == ":read-write":526                sel.selectors.append(CSS_READ_WRITE)527            elif pseudo == ":in-range":528                sel.selectors.append(CSS_IN_RANGE)529            elif pseudo == ":out-of-range":530                sel.selectors.append(CSS_OUT_OF_RANGE)531            elif pseudo == ":placeholder-shown":532                sel.selectors.append(CSS_PLACEHOLDER_SHOWN)533            elif pseudo == ':first-child':534                sel.nth.append(ct.SelectorNth(1, False, 0, False, False, ct.SelectorList()))535            elif pseudo == ':last-child':536                sel.nth.append(ct.SelectorNth(1, False, 0, False, True, ct.SelectorList()))537            elif pseudo == ':first-of-type':538                sel.nth.append(ct.SelectorNth(1, False, 0, True, False, ct.SelectorList()))539            elif pseudo == ':last-of-type':540                sel.nth.append(ct.SelectorNth(1, False, 0, True, True, ct.SelectorList()))541            elif pseudo == ':only-child':542                sel.nth.extend(543                    [544                        ct.SelectorNth(1, False, 0, False, False, ct.SelectorList()),545                        ct.SelectorNth(1, False, 0, False, True, ct.SelectorList())546                    ]547                )548            elif pseudo == ':only-of-type':549                sel.nth.extend(550                    [551                        ct.SelectorNth(1, False, 0, True, False, ct.SelectorList()),552                        ct.SelectorNth(1, False, 0, True, True, ct.SelectorList())553                    ]554                )555            has_selector = True556        elif complex_pseudo and pseudo in PSEUDO_COMPLEX_NO_MATCH:557            self.parse_selectors(iselector, m.end(0), FLG_PSEUDO | FLG_OPEN)558            sel.no_match = True559            has_selector = True560        elif not complex_pseudo and pseudo in PSEUDO_SIMPLE_NO_MATCH:561            sel.no_match = True562            has_selector = True563        elif pseudo in PSEUDO_SUPPORTED:564            raise SelectorSyntaxError(565                "Invalid syntax for pseudo class '{}'".format(pseudo),566                self.pattern,567                m.start(0)568            )569        else:570            raise NotImplementedError(571                "'{}' pseudo-class is not implemented at this time".format(pseudo)572            )573        return has_selector, is_html574    def parse_pseudo_nth(self, sel, m, has_selector, iselector):575        """Parse `nth` pseudo."""576        mdict = m.groupdict()577        if mdict.get('pseudo_nth_child'):578            postfix = '_child'579        else:580            postfix = '_type'581        mdict['name'] = util.lower(css_unescape(mdict['name']))582        content = util.lower(mdict.get('nth' + postfix))583        if content == 'even':584            # 2n585            s1 = 2586            s2 = 0587            var = True588        elif content == 'odd':589            # 2n+1590            s1 = 2591            s2 = 1592            var = True593        else:594            nth_parts = RE_NTH.match(content)595            s1 = '-' if nth_parts.group('s1') and nth_parts.group('s1') == '-' else ''596            a = nth_parts.group('a')597            var = a.endswith('n')598            if a.startswith('n'):599                s1 += '1'600            elif var:601                s1 += a[:-1]602            else:603                s1 += a604            s2 = '-' if nth_parts.group('s2') and nth_parts.group('s2') == '-' else ''605            if nth_parts.group('b'):606                s2 += nth_parts.group('b')607            else:608                s2 = '0'609            s1 = int(s1, 10)610            s2 = int(s2, 10)611        pseudo_sel = mdict['name']612        if postfix == '_child':613            if m.group('of'):614                # Parse the rest of `of S`.615                nth_sel = self.parse_selectors(iselector, m.end(0), FLG_PSEUDO | FLG_OPEN)616            else:617                # Use default `*|*` for `of S`.618                nth_sel = CSS_NTH_OF_S_DEFAULT619            if pseudo_sel == ':nth-child':620                sel.nth.append(ct.SelectorNth(s1, var, s2, False, False, nth_sel))621            elif pseudo_sel == ':nth-last-child':622                sel.nth.append(ct.SelectorNth(s1, var, s2, False, True, nth_sel))623        else:624            if pseudo_sel == ':nth-of-type':625                sel.nth.append(ct.SelectorNth(s1, var, s2, True, False, ct.SelectorList()))626            elif pseudo_sel == ':nth-last-of-type':627                sel.nth.append(ct.SelectorNth(s1, var, s2, True, True, ct.SelectorList()))628        has_selector = True629        return has_selector630    def parse_pseudo_open(self, sel, name, has_selector, iselector, index):631        """Parse pseudo with opening bracket."""632        flags = FLG_PSEUDO | FLG_OPEN633        if name == ':not':634            flags |= FLG_NOT635        if name == ':has':636            flags |= FLG_RELATIVE637        sel.selectors.append(self.parse_selectors(iselector, index, flags))638        has_selector = True639        return has_selector640    def parse_has_combinator(self, sel, m, has_selector, selectors, rel_type, index):641        """Parse combinator tokens."""642        combinator = m.group('relation').strip()643        if not combinator:644            combinator = WS_COMBINATOR645        if combinator == COMMA_COMBINATOR:646            if not has_selector:647                # If we've not captured any selector parts, the comma is either at the beginning of the pattern648                # or following another comma, both of which are unexpected. Commas must split selectors.649                raise SelectorSyntaxError(650                    "The combinator '{}' at postion {}, must have a selector before it".format(combinator, index),651                    self.pattern,652                    index653                )654            sel.rel_type = rel_type655            selectors[-1].relations.append(sel)656            rel_type = ":" + WS_COMBINATOR657            selectors.append(_Selector())658        else:659            if has_selector:660                # End the current selector and associate the leading combinator with this selector.661                sel.rel_type = rel_type662                selectors[-1].relations.append(sel)663            elif rel_type[1:] != WS_COMBINATOR:664                # It's impossible to have two whitespace combinators after each other as the patterns665                # will gobble up trailing whitespace. It is also impossible to have a whitespace666                # combinator after any other kind for the same reason. But we could have667                # multiple non-whitespace combinators. So if the current combinator is not a whitespace,668                # then we've hit the multiple combinator case, so we should fail.669                raise SelectorSyntaxError(670                    'The multiple combinators at position {}'.format(index),671                    self.pattern,672                    index673                )674            # Set the leading combinator for the next selector.675            rel_type = ':' + combinator676        sel = _Selector()677        has_selector = False678        return has_selector, sel, rel_type679    def parse_combinator(self, sel, m, has_selector, selectors, relations, is_pseudo, index):680        """Parse combinator tokens."""681        combinator = m.group('relation').strip()682        if not combinator:683            combinator = WS_COMBINATOR684        if not has_selector:685            raise SelectorSyntaxError(686                "The combinator '{}' at postion {}, must have a selector before it".format(combinator, index),687                self.pattern,688                index689            )690        if combinator == COMMA_COMBINATOR:691            if not sel.tag and not is_pseudo:692                # Implied `*`693                sel.tag = ct.SelectorTag('*', None)694            sel.relations.extend(relations)695            selectors.append(sel)696            del relations[:]697        else:698            sel.relations.extend(relations)699            sel.rel_type = combinator700            del relations[:]701            relations.append(sel)702        sel = _Selector()703        has_selector = False704        return has_selector, sel705    def parse_class_id(self, sel, m, has_selector):706        """Parse HTML classes and ids."""707        selector = m.group(0)708        if selector.startswith('.'):709            sel.classes.append(css_unescape(selector[1:]))710        else:711            sel.ids.append(css_unescape(selector[1:]))712        has_selector = True713        return has_selector714    def parse_pseudo_contains(self, sel, m, has_selector):715        """Parse contains."""716        values = m.group('values')717        patterns = []718        for token in RE_VALUES.finditer(values):719            if token.group('split'):720                continue721            value = token.group('value')722            if value.startswith(("'", '"')):723                value = css_unescape(value[1:-1], True)724            else:725                value = css_unescape(value)726            patterns.append(value)727        sel.contains.append(ct.SelectorContains(tuple(patterns)))728        has_selector = True729        return has_selector730    def parse_pseudo_lang(self, sel, m, has_selector):731        """Parse pseudo language."""732        values = m.group('values')733        patterns = []734        for token in RE_VALUES.finditer(values):735            if token.group('split'):736                continue737            value = token.group('value')738            if value.startswith(('"', "'")):739                parts = css_unescape(value[1:-1], True).split('-')740            else:741                parts = css_unescape(value).split('-')742            new_parts = []743            first = True744            for part in parts:745                if part == '*' and first:746                    new_parts.append('(?!x\b)[a-z0-9]+?')747                elif part != '*':748                    new_parts.append(('' if first else '(-(?!x\b)[a-z0-9]+)*?\\-') + re.escape(part))749                if first:750                    first = False751            patterns.append(re.compile(r'^{}(?:-.*)?$'.format(''.join(new_parts)), re.I))752        sel.lang.append(ct.SelectorLang(patterns))753        has_selector = True754        return has_selector755    def parse_pseudo_dir(self, sel, m, has_selector):756        """Parse pseudo direction."""757        value = ct.SEL_DIR_LTR if util.lower(m.group('dir')) == 'ltr' else ct.SEL_DIR_RTL758        sel.flags |= value759        has_selector = True760        return has_selector761    def parse_selectors(self, iselector, index=0, flags=0):762        """Parse selectors."""763        sel = _Selector()764        selectors = []765        has_selector = False766        closed = False767        relations = []768        rel_type = ":" + WS_COMBINATOR769        is_open = bool(flags & FLG_OPEN)770        is_pseudo = bool(flags & FLG_PSEUDO)771        is_relative = bool(flags & FLG_RELATIVE)772        is_not = bool(flags & FLG_NOT)773        is_html = bool(flags & FLG_HTML)774        is_default = bool(flags & FLG_DEFAULT)775        is_indeterminate = bool(flags & FLG_INDETERMINATE)776        is_in_range = bool(flags & FLG_IN_RANGE)777        is_out_of_range = bool(flags & FLG_OUT_OF_RANGE)778        if self.debug:  # pragma: no cover779            if is_pseudo:780                print('    is_pseudo: True')781            if is_open:782                print('    is_open: True')783            if is_relative:784                print('    is_relative: True')785            if is_not:786                print('    is_not: True')787            if is_html:788                print('    is_html: True')789            if is_default:790                print('    is_default: True')791            if is_indeterminate:792                print('    is_indeterminate: True')793            if is_in_range:794                print('    is_in_range: True')795            if is_out_of_range:796                print('    is_out_of_range: True')797        if is_relative:798            selectors.append(_Selector())799        try:800            while True:801                key, m = next(iselector)802                # Handle parts803                if key == "at_rule":804                    raise NotImplementedError("At-rules found at position {}".format(m.start(0)))805                elif key == 'pseudo_class_custom':806                    has_selector = self.parse_pseudo_class_custom(sel, m, has_selector)807                elif key == 'pseudo_class':808                    has_selector, is_html = self.parse_pseudo_class(sel, m, has_selector, iselector, is_html)809                elif key == 'pseudo_element':810                    raise NotImplementedError("Psuedo-element found at position {}".format(m.start(0)))811                elif key == 'pseudo_contains':812                    has_selector = self.parse_pseudo_contains(sel, m, has_selector)813                elif key in ('pseudo_nth_type', 'pseudo_nth_child'):814                    has_selector = self.parse_pseudo_nth(sel, m, has_selector, iselector)815                elif key == 'pseudo_lang':816                    has_selector = self.parse_pseudo_lang(sel, m, has_selector)817                elif key == 'pseudo_dir':818                    has_selector = self.parse_pseudo_dir(sel, m, has_selector)819                    # Currently only supports HTML820                    is_html = True821                elif key == 'pseudo_close':822                    if not has_selector:823                        raise SelectorSyntaxError(824                            "Expected a selector at postion {}".format(m.start(0)),825                            self.pattern,826                            m.start(0)827                        )828                    if is_open:829                        closed = True830                        break831                    else:832                        raise SelectorSyntaxError(833                            "Unmatched pseudo-class close at postion {}".format(m.start(0)),834                            self.pattern,835                            m.start(0)836                        )837                elif key == 'combine':838                    if is_relative:839                        has_selector, sel, rel_type = self.parse_has_combinator(840                            sel, m, has_selector, selectors, rel_type, index841                        )842                    else:843                        has_selector, sel = self.parse_combinator(844                            sel, m, has_selector, selectors, relations, is_pseudo, index845                        )846                elif key == 'attribute':847                    has_selector = self.parse_attribute_selector(sel, m, has_selector)848                elif key == 'tag':849                    if has_selector:850                        raise SelectorSyntaxError(851                            "Tag name found at position {} instead of at the start".format(m.start(0)),852                            self.pattern,853                            m.start(0)854                        )855                    has_selector = self.parse_tag_pattern(sel, m, has_selector)856                elif key in ('class', 'id'):857                    has_selector = self.parse_class_id(sel, m, has_selector)858                index = m.end(0)859        except StopIteration:860            pass861        if is_open and not closed:862            raise SelectorSyntaxError(863                "Unclosed pseudo-class at position {}".format(index),864                self.pattern,865                index866            )867        if has_selector:868            if not sel.tag and not is_pseudo:869                # Implied `*`870                sel.tag = ct.SelectorTag('*', None)871            if is_relative:872                sel.rel_type = rel_type873                selectors[-1].relations.append(sel)874            else:875                sel.relations.extend(relations)876                del relations[:]877                selectors.append(sel)878        else:879            # We will always need to finish a selector when `:has()` is used as it leads with combining.880            raise SelectorSyntaxError(881                'Expected a selector at position {}'.format(index),882                self.pattern,883                index884            )885        # Some patterns require additional logic, such as default. We try to make these the886        # last pattern, and append the appropriate flag to that selector which communicates887        # to the matcher what additional logic is required.888        if is_default:889            selectors[-1].flags = ct.SEL_DEFAULT890        if is_indeterminate:891            selectors[-1].flags = ct.SEL_INDETERMINATE892        if is_in_range:893            selectors[-1].flags = ct.SEL_IN_RANGE894        if is_out_of_range:895            selectors[-1].flags = ct.SEL_OUT_OF_RANGE896        return ct.SelectorList([s.freeze() for s in selectors], is_not, is_html)897    def selector_iter(self, pattern):898        """Iterate selector tokens."""899        # Ignore whitespace and comments at start and end of pattern900        m = RE_WS_BEGIN.search(pattern)901        index = m.end(0) if m else 0902        m = RE_WS_END.search(pattern)903        end = (m.start(0) - 1) if m else (len(pattern) - 1)904        if self.debug:  # pragma: no cover905            print('## PARSING: {!r}'.format(pattern))906        while index <= end:907            m = None908            for v in self.css_tokens:909                if not v.enabled(self.flags):  # pragma: no cover910                    continue911                m = v.match(pattern, index)912                if m:913                    name = v.get_name()914                    if self.debug:  # pragma: no cover915                        print("TOKEN: '{}' --> {!r} at position {}".format(name, m.group(0), m.start(0)))916                    index = m.end(0)917                    yield name, m918                    break919            if m is None:920                c = pattern[index]921                # If the character represents the start of one of the known selector types,922                # throw an exception mentioning that the known selector type is in error;923                # otherwise, report the invalid character.924                if c == '[':925                    msg = "Malformed attribute selector at position {}".format(index)926                elif c == '.':927                    msg = "Malformed class selector at position {}".format(index)928                elif c == '#':929                    msg = "Malformed id selector at position {}".format(index)930                elif c == ':':931                    msg = "Malformed pseudo-class selector at position {}".format(index)932                else:933                    msg = "Invalid character {!r} position {}".format(c, index)934                raise SelectorSyntaxError(msg, self.pattern, index)935        if self.debug:  # pragma: no cover936            print('## END PARSING')937    def process_selectors(self, index=0, flags=0):938        """Process selectors."""939        return self.parse_selectors(self.selector_iter(self.pattern), index, flags)940# Precompile CSS selector lists for pseudo-classes (additional logic may be required beyond the pattern)941# A few patterns are order dependent as they use patterns previous compiled.942# CSS pattern for `:link` and `:any-link`943CSS_LINK = CSSParser(944    'html|*:is(a, area, link)[href]'945).process_selectors(flags=FLG_PSEUDO | FLG_HTML)946# CSS pattern for `:checked`947CSS_CHECKED = CSSParser(948    '''949    html|*:is(input[type=checkbox], input[type=radio])[checked],950    html|select > html|option[selected]951    '''952).process_selectors(flags=FLG_PSEUDO | FLG_HTML)953# CSS pattern for `:default` (must compile CSS_CHECKED first)954CSS_DEFAULT = CSSParser(955    '''956    :checked,957    /*958    This pattern must be at the end.959    Special logic is applied to the last selector.960    */961    html|form html|*:is(button, input)[type="submit"]962    '''963).process_selectors(flags=FLG_PSEUDO | FLG_HTML | FLG_DEFAULT)964# CSS pattern for `:indeterminate`965CSS_INDETERMINATE = CSSParser(966    '''967    html|input[type="checkbox"][indeterminate],968    html|input[type="radio"]:is(:not([name]), [name=""]):not([checked]),969    html|progress:not([value]),970    /*971    This pattern must be at the end.972    Special logic is applied to the last selector.973    */974    html|input[type="radio"][name][name!='']:not([checked])975    '''976).process_selectors(flags=FLG_PSEUDO | FLG_HTML | FLG_INDETERMINATE)977# CSS pattern for `:disabled`978CSS_DISABLED = CSSParser(979    '''980    html|*:is(input[type!=hidden], button, select, textarea, fieldset, optgroup, option, fieldset)[disabled],981    html|optgroup[disabled] > html|option,982    html|fieldset[disabled] > html|*:is(input[type!=hidden], button, select, textarea, fieldset),983    html|fieldset[disabled] >984        html|*:not(legend:nth-of-type(1)) html|*:is(input[type!=hidden], button, select, textarea, fieldset)985    '''986).process_selectors(flags=FLG_PSEUDO | FLG_HTML)987# CSS pattern for `:enabled`988CSS_ENABLED = CSSParser(989    '''990    html|*:is(input[type!=hidden], button, select, textarea, fieldset, optgroup, option, fieldset):not(:disabled)991    '''992).process_selectors(flags=FLG_PSEUDO | FLG_HTML)993# CSS pattern for `:required`994CSS_REQUIRED = CSSParser(995    'html|*:is(input, textarea, select)[required]'996).process_selectors(flags=FLG_PSEUDO | FLG_HTML)997# CSS pattern for `:optional`998CSS_OPTIONAL = CSSParser(999    'html|*:is(input, textarea, select):not([required])'1000).process_selectors(flags=FLG_PSEUDO | FLG_HTML)1001# CSS pattern for `:placeholder-shown`1002CSS_PLACEHOLDER_SHOWN = CSSParser(1003    '''1004    html|*:is(1005        input:is(1006            :not([type]),1007            [type=""],1008            [type=text],1009            [type=search],1010            [type=url],1011            [type=tel],1012            [type=email],1013            [type=password],1014            [type=number]1015        ),1016        textarea1017    )[placeholder][placeholder!='']1018    '''1019).process_selectors(flags=FLG_PSEUDO | FLG_HTML)1020# CSS pattern default for `:nth-child` "of S" feature1021CSS_NTH_OF_S_DEFAULT = CSSParser(1022    '*|*'1023).process_selectors(flags=FLG_PSEUDO)1024# CSS pattern for `:read-write` (CSS_DISABLED must be compiled first)1025CSS_READ_WRITE = CSSParser(1026    '''1027    html|*:is(1028        textarea,1029        input:is(1030            :not([type]),1031            [type=""],1032            [type=text],1033            [type=search],1034            [type=url],1035            [type=tel],1036            [type=email],1037            [type=number],1038            [type=password],1039            [type=date],1040            [type=datetime-local],1041            [type=month],1042            [type=time],1043            [type=week]1044        )1045    ):not([readonly], :disabled),1046    html|*:is([contenteditable=""], [contenteditable="true" i])1047    '''1048).process_selectors(flags=FLG_PSEUDO | FLG_HTML)1049# CSS pattern for `:read-only`1050CSS_READ_ONLY = CSSParser(1051    '''1052    html|*:not(:read-write)1053    '''1054).process_selectors(flags=FLG_PSEUDO | FLG_HTML)1055# CSS pattern for `:in-range`1056CSS_IN_RANGE = CSSParser(1057    '''1058    html|input:is(1059        [type="date"],1060        [type="month"],1061        [type="week"],1062        [type="time"],1063        [type="datetime-local"],1064        [type="number"],1065        [type="range"]1066    ):is(1067        [min],1068        [max]1069    )1070    '''1071).process_selectors(flags=FLG_PSEUDO | FLG_IN_RANGE | FLG_HTML)1072# CSS pattern for `:out-of-range`1073CSS_OUT_OF_RANGE = CSSParser(1074    '''1075    html|input:is(1076        [type="date"],1077        [type="month"],1078        [type="week"],1079        [type="time"],1080        [type="datetime-local"],1081        [type="number"],1082        [type="range"]1083    ):is(1084        [min],1085        [max]1086    )1087    '''...Learn to execute automation testing from scratch with LambdaTest Learning Hub. Right from setting up the prerequisites to run your first automation test, to following best practices and diving deeper into advanced test scenarios. LambdaTest Learning Hubs compile a list of step-by-step guides to help you be proficient with different test automation frameworks i.e. Selenium, Cypress, TestNG etc.
You could also refer to video tutorials over LambdaTest YouTube channel to get step by step demonstration from industry experts.
Get 100 minutes of automation test minutes FREE!!
