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)
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')
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
def parse_charset_rule(self, rule, previous_rules, errors, context): raise ParseError(rule, 'mis-placed or malformed @charset rule')
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