def wrapper(tokens, *args): results = [] for part in split_on_comma(tokens): result = function(remove_whitespace(part), *args) if result is None: return None results.append(result) return results
def background_image(token, base_url): if token.type != 'FUNCTION': return image_url([token], base_url) name = token.function_name.lower() if name in ('linear-gradient', 'repeating-linear-gradient'): arguments = split_on_comma(t for t in token.content if t.type != 'S') direction, color_stops = parse_linear_gradient_parameters(arguments) if color_stops: return 'gradient', LinearGradient( [parse_color_stop(stop) for stop in color_stops], direction, 'repeating' in name)
def _selector_as_string(self, selector): """ Returns a selector as a CSS string :param selector: A list of tinycss Tokens :type selector: list :returns: The CSS string for the selector :rtype: str """ return ','.join(''.join(token.as_css() for token in strip_whitespace(token_list)) for token_list in split_on_comma(selector))
def font_family(tokens): """``font-family`` property validation.""" parts = split_on_comma(tokens) families = [] for part in parts: if len(part) == 1 and part[0].type == 'STRING': families.append(part[0].value) elif part and all(token.type == 'IDENT' for token in part): families.append(' '.join(token.value for token in part)) else: break else: return families
def _clean_rule(self, rule): """ Cleans a css Rule by removing Selectors without matches on the tree Returns None if the whole rule do not match :param rule: CSS Rule to check :type rule: A tinycss Rule object :returns: A cleaned tinycss Rule with only Selectors matching the tree or None :rtype: tinycss Rule or None """ # Always match @ rules if rule.at_keyword is not None: return rule # Clean selectors cleaned_token_list = [] for token_list in split_on_comma(rule.selector): # If the token list matches the tree if self._token_list_matches_tree(token_list): # Add a Comma if multiple token lists matched if len(cleaned_token_list) > 0: cleaned_token_list.append( cssselect.parser.Token('DELIM', ',', len(cleaned_token_list) + 1)) # Append it to the list of cleaned token list cleaned_token_list += token_list # Return None if selectors list is empty if not cleaned_token_list: return None # Update rule token list rule.selector = cleaned_token_list # Return cleaned rule return rule
def _clean_rule(self, rule): """ Cleans a css Rule by removing Selectors without matches on the tree Returns None if the whole rule do not match :param rule: CSS Rule to check :type rule: A tinycss Rule object :returns: A cleaned tinycss Rule with only Selectors matching the tree or None :rtype: tinycss Rule or None """ # Always match @ rules if rule.at_keyword is not None: return rule # Clean selectors cleaned_token_list = [] for token_list in split_on_comma(rule.selector): # If the token list matches the tree if self._token_list_matches_tree(token_list): # Add a Comma if multiple token lists matched if len(cleaned_token_list) > 0: cleaned_token_list.append(cssselect.parser.Token("DELIM", ",", len(cleaned_token_list) + 1)) # Append it to the list of cleaned token list cleaned_token_list += token_list # Return None if selectors list is empty if not cleaned_token_list: return None # Update rule token list rule.selector = cleaned_token_list # Return cleaned rule return rule
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_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
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
def expand_background(base_url, name, tokens): """Expand the ``background`` shorthand property. See http://dev.w3.org/csswg/css3-background/#the-background """ properties = [ 'background_color', 'background_image', 'background_repeat', 'background_attachment', 'background_position', 'background_size', 'background_clip', 'background_origin'] keyword = get_single_keyword(tokens) if keyword in ('initial', 'inherit'): for name in properties: yield name, keyword return def parse_layer(tokens, final_layer=False): results = {} def add(name, value): if value is None: return False name = 'background_' + name if name in results: raise InvalidValues results[name] = value return True # Make `tokens` a stack tokens = tokens[::-1] while tokens: if add('repeat', background_repeat.single_value(tokens[-2:][::-1])): del tokens[-2:] continue token = tokens[-1:] if ( (final_layer and add('color', other_colors(token))) or add('image', image.single_value(token, base_url)) or add('repeat', background_repeat.single_value(token)) or add('attachment', background_attachment.single_value(token)) ): tokens.pop() continue for n in (4, 3, 2, 1)[-len(tokens):]: n_tokens = tokens[-n:][::-1] position = background_position.single_value(n_tokens) if position is not None: assert add('position', position) del tokens[-n:] if (tokens and tokens[-1].type == 'DELIM' and tokens[-1].value == '/'): for n in (3, 2)[-len(tokens):]: # n includes the '/' delimiter. n_tokens = tokens[-n:-1][::-1] size = background_size.single_value(n_tokens) if size is not None: assert add('size', size) del tokens[-n:] break if position is not None: continue if add('origin', box.single_value(token)): tokens.pop() next_token = tokens[-1:] if add('clip', box.single_value(next_token)): tokens.pop() else: # The same keyword sets both: assert add('clip', box.single_value(token)) continue raise InvalidValues color = results.pop( 'background_color', INITIAL_VALUES['background_color']) for name in properties: if name not in results: results[name] = INITIAL_VALUES[name][0] return color, results layers = reversed(split_on_comma(tokens)) color, last_layer = parse_layer(next(layers), final_layer=True) results = dict((k, [v]) for k, v in last_layer.items()) for tokens in layers: _, layer = parse_layer(tokens) for name, value in layer.items(): results[name].append(value) for name, values in results.items(): yield name, values[::-1] # "Un-reverse" yield 'background-color', color
def _selector_as_string(self, selector): """ Returns a selector as a CSS string :param selector: A list of tinycss Tokens :type selector: list :returns: The CSS string for the selector :rtype: str """ return ",".join( "".join(token.as_css() for token in strip_whitespace(token_list)) for token_list in split_on_comma(selector) )