def parse_import_rule(self, rule, previous_rules, errors, context):
     if context != 'stylesheet':
         raise ParseError(rule,
             '@import rule not allowed in ' + context)
     for previous_rule in previous_rules:
         if previous_rule.at_keyword not in ('@charset', '@import'):
             if previous_rule.at_keyword:
                 type_ = 'an {0} rule'.format(previous_rule.at_keyword)
             else:
                 type_ = 'a ruleset'
             raise ParseError(previous_rule,
                 '@import rule not allowed after ' + type_)
     head = rule.head
     if not head:
         raise ParseError(rule,
             'expected URI or STRING for @import rule')
     if head[0].type not in ('URI', 'STRING'):
         raise ParseError(rule,
             'expected URI or STRING for @import rule, got '
             + head[0].type)
     uri = head[0].value
     media = self.parse_media(strip_whitespace(head[1:]), errors)
     if rule.body is not None:
         # The position of the ';' token would be best, but we don’t
         # have it anymore here.
         raise ParseError(head[-1], "expected ';', got a block")
     return ImportRule(uri, media, rule.line, rule.column)
 def parse_media_rule(self, rule, previous_rules, errors, context):
     if context != 'stylesheet':
         raise ParseError(rule, '@media rule not allowed in ' + context)
     media = self.parse_media(rule.head, errors)
     if rule.body is None:
         raise ParseError(rule,
             'invalid {0} rule: missing block'.format(rule.at_keyword))
     rules, rule_errors = self.parse_rules(rule.body, '@media')
     errors.extend(rule_errors)
     return MediaRule(media, rules, rule.line, rule.column)
 def parse_page_rule(self, rule, previous_rules, errors, context):
     if context != 'stylesheet':
         raise ParseError(rule, '@page rule not allowed in ' + context)
     selector, specificity = self.parse_page_selector(rule.head)
     if rule.body is None:
         raise ParseError(rule,
             'invalid {0} rule: missing block'.format(rule.at_keyword))
     declarations, at_rules, rule_errors = \
         self.parse_declarations_and_at_rules(rule.body, '@page')
     errors.extend(rule_errors)
     return PageRule(selector, specificity, declarations, at_rules,
                     rule.line, rule.column)
    def parse_value_priority(self, tokens):
        """Separate any ``!important`` marker at the end of a property value.

        :param tokens:
            A list of tokens for the property value.
        :returns:
            A tuple of the actual property value (a list of tokens)
            and the :attr:`~Declaration.priority`.
        """
        value = list(tokens)
        # Walk the token list from the end
        token = value.pop()
        if token.type == 'IDENT' and token.value.lower() == 'important':
            while value:
                token = value.pop()
                if token.type == 'DELIM' and token.value == '!':
                    # Skip any white space before the '!'
                    while value and value[-1].type == 'S':
                        value.pop()
                    if not value:
                        raise ParseError(
                            token, 'expected a value before !important')
                    return value, 'important'
                # Skip white space between '!' and 'important'
                elif token.type != 'S':
                    break
        return tokens, None
    def parse_at_rule(self, rule, previous_rules, errors, context):
        """Parse an at-rule.

        Subclasses that override this method must use ``super()`` and
        pass its return value for at-rules they do not know.

        In CSS 2.1, this method handles @charset, @import, @media and @page
        rules.

        :param rule:
            An unparsed :class:`AtRule`.
        :param previous_rules:
            The list of at-rules and rulesets that have been parsed so far
            in this context. This list can be used to decide if the current
            rule is valid. (For example, @import rules are only allowed
            before anything but a @charset rule.)
        :param context:
            Either ``'stylesheet'`` or an at-keyword such as ``'@media'``.
            (Most at-rules are only allowed in some contexts.)
        :raises:
            :class:`~.parsing.ParseError` if the rule is invalid.
        :return:
            A parsed at-rule

        """
        try:
            parser = self.at_parsers[rule.at_keyword]
        except KeyError:
            raise ParseError(rule, 'unknown at-rule in {0} context: {1}'
                                    .format(context, rule.at_keyword))
        else:
            return parser(rule, previous_rules, errors, context)
Beispiel #6
0
    def parse_page_selector(self, tokens):
        """Parse an @page selector.

        :param tokens:
            An iterable of token, typically from the  ``head`` attribute of
            an unparsed :class:`AtRule`.
        :returns:
            A page selector. For CSS 2.1, this is ``'first'``, ``'left'``,
            ``'right'`` or ``None``.
        :raises:
            :class:`~.parsing.ParseError` on invalid selectors

        """
        if not tokens:
            return None, (0, 0)
        if (len(tokens) == 2 and tokens[0].type == ':'
                and tokens[1].type == 'IDENT'):
            pseudo_class = tokens[1].value
            specificity = {
                'first': (1, 0),
                'left': (0, 1),
                'right': (0, 1),
            }.get(pseudo_class)
            if specificity:
                return pseudo_class, specificity
        raise ParseError(tokens[0], 'invalid @page selector')
Beispiel #7
0
    def parse_declaration(self, tokens):
        """Parse a single declaration.

        :param tokens:
            an iterable of at least one token. Should stop at (before)
            the end of the declaration, as marked by a ``;`` or ``}``.
            Empty declarations (ie. consecutive ``;`` with only white space
            in-between) should be skipped earlier and not passed to
            this method.
        :returns:
            a :class:`Declaration`
        :raises:
            :class:`~.parsing.ParseError` if the tokens do not match the
            'declaration' production of the core grammar.

        """
        tokens = iter(tokens)

        name_token = next(tokens)  # assume there is at least one
        if name_token.type == 'IDENT':
            # CSS syntax is case-insensitive
            property_name = name_token.value.lower()
        else:
            raise ParseError(
                name_token,
                'expected a property name, got {0}'.format(name_token.type))

        token = name_token  # In case ``tokens`` is now empty
        for token in tokens:
            if token.type == ':':
                break
            elif token.type != 'S':
                raise ParseError(token,
                                 "expected ':', got {0}".format(token.type))
        else:
            raise ParseError(token, "expected ':'")

        value = strip_whitespace(list(tokens))
        if not value:
            raise ParseError(token, 'expected a property value')
        validate_value(value)
        value, priority = self.parse_value_priority(value)
        return Declaration(property_name, value, priority, name_token.line,
                           name_token.column)
    def parse_ruleset(self, first_token, tokens):
        """Parse a ruleset: a selector followed by declaration block.

        :param first_token:
            The first token of the ruleset (probably of the selector).
            You may have read it already to distinguish the rule
            from an at-rule.
        :param tokens:
            an iterator of subsequent tokens. Will be consumed just enough
            for one ruleset.
        :return:
            a tuple of a :class:`RuleSet` and an error list.
            The errors are recovered :class:`~.parsing.ParseError` in declarations.
            (Parsing continues from the next declaration on such errors.)
        :raises:
            :class:`~.parsing.ParseError` if the selector is invalid for the
            core grammar.
            Note a that a selector can be valid for the core grammar but
            not for CSS 2.1 or another level.

        """
        selector = []
        for token in chain([first_token], tokens):
            if token.type == '{':
                # Parse/validate once we’ve read the whole rule
                selector = strip_whitespace(selector)
                if not selector:
                    raise ParseError(first_token, 'empty selector')
                for selector_token in selector:
                    validate_any(selector_token, 'selector')
                declarations, errors = self.parse_declaration_list(
                    token.content)
                ruleset = RuleSet(selector, declarations,
                                  first_token.line, first_token.column)
                return ruleset, errors
            else:
                selector.append(token)
        raise ParseError(token, 'no declaration block found for ruleset')
    def parse_media(self, tokens, errors):
        """For CSS 2.1, parse a list of media types.

        Media Queries are expected to override this.

        :param tokens:
            A list of tokens
        :raises:
            :class:`~.parsing.ParseError` on invalid media types/queries
        :returns:
            For CSS 2.1, a list of media types as strings
        """
        if not tokens:
            return ['all']
        media_types = []
        for part in split_on_comma(remove_whitespace(tokens)):
            types = [token.type for token in part]
            if types == ['IDENT']:
                media_types.append(part[0].value)
            else:
                raise ParseError(tokens[0], 'expected a media type'
                    + ((', got ' + ', '.join(types)) if types else ''))
        return media_types
Beispiel #10
0
 def parse_charset_rule(self, rule, previous_rules, errors, context):
     raise ParseError(rule, 'mis-placed or malformed @charset rule')
Beispiel #11
0
    def parse_media(self, tokens, errors):
        if not tokens:
            return [MediaQuery('all')]
        queries = []

        for part in split_on_comma(remove_whitespace(tokens)):
            negated = False
            media_type = None
            expressions = []
            try:
                for i, tok in enumerate(part):
                    if i == 0 and tok.type == 'IDENT':
                        val = tok.value.lower()
                        if val == 'only':
                            continue  # ignore leading ONLY
                        if val == 'not':
                            negated = True
                            continue
                    if media_type is None and tok.type == 'IDENT':
                        media_type = tok.value
                        continue
                    elif media_type is None:
                        media_type = 'all'

                    if tok.type == 'IDENT' and tok.value.lower() == 'and':
                        continue
                    if not tok.is_container:
                        raise MalformedExpression(
                            tok,
                            'expected a media expression not a %s' % tok.type)
                    if tok.type != '(':
                        raise MalformedExpression(
                            tok,
                            'media expressions must be in parentheses not %s' %
                            tok.type)
                    content = remove_whitespace(tok.content)
                    if len(content) == 0:
                        raise MalformedExpression(
                            tok, 'media expressions cannot be empty')
                    if content[0].type != 'IDENT':
                        raise MalformedExpression(
                            content[0],
                            'expected a media feature not a %s' % tok.type)
                    media_feature, expr = content[0].value, None
                    if len(content) > 1:
                        if len(content) < 3:
                            raise MalformedExpression(
                                content[1],
                                'malformed media feature definition')
                        if content[1].type != ':':
                            raise MalformedExpression(content[1],
                                                      'expected a :')
                        expr = content[2:]
                        if len(expr) == 1:
                            expr = expr[0]
                        elif len(expr) == 3 and (
                                expr[0].type, expr[1].type, expr[1].value,
                                expr[2].type) == ('INTEGER', 'DELIM', '/',
                                                  'INTEGER'):
                            # This should really be moved into token_data, but
                            # since RATIO is not part of CSS 2.1 and does not
                            # occur anywhere else, we special case it here.
                            r = expr[0]
                            r.value = (expr[0].value, expr[2].value)
                            r.type = 'RATIO'
                            r._as_css = expr[0]._as_css + expr[
                                1]._as_css + expr[2]._as_css
                            expr = r
                        else:
                            raise MalformedExpression(
                                expr[0], 'malformed media feature definition')

                    expressions.append((media_feature, expr))
            except MalformedExpression as err:
                errors.extend(ParseError(err.tok, err.message))
                media_type, negated, expressions = 'all', True, ()
            queries.append(
                MediaQuery(media_type or 'all',
                           expressions=tuple(expressions),
                           negated=negated))

        return queries