feat: add comprehensive GitHub workflow and development tools
This commit is contained in:
782
app/.venv/Lib/site-packages/weasyprint/css/utils.py
Normal file
782
app/.venv/Lib/site-packages/weasyprint/css/utils.py
Normal file
@@ -0,0 +1,782 @@
|
||||
"""Utils for CSS properties."""
|
||||
|
||||
import functools
|
||||
import math
|
||||
from abc import ABC, abstractmethod
|
||||
from urllib.parse import unquote, urljoin
|
||||
|
||||
from tinycss2.color3 import parse_color
|
||||
|
||||
from .. import LOGGER
|
||||
from ..urls import iri_to_uri, url_is_absolute
|
||||
from .properties import Dimension
|
||||
|
||||
# https://drafts.csswg.org/css-values-3/#angles
|
||||
# 1<unit> is this many radians.
|
||||
ANGLE_TO_RADIANS = {
|
||||
'rad': 1,
|
||||
'turn': 2 * math.pi,
|
||||
'deg': math.pi / 180,
|
||||
'grad': math.pi / 200,
|
||||
}
|
||||
|
||||
# How many CSS pixels is one <unit>?
|
||||
# https://www.w3.org/TR/CSS21/syndata.html#length-units
|
||||
LENGTHS_TO_PIXELS = {
|
||||
'px': 1,
|
||||
'pt': 1. / 0.75,
|
||||
'pc': 16., # LENGTHS_TO_PIXELS['pt'] * 12
|
||||
'in': 96., # LENGTHS_TO_PIXELS['pt'] * 72
|
||||
'cm': 96. / 2.54, # LENGTHS_TO_PIXELS['in'] / 2.54
|
||||
'mm': 96. / 25.4, # LENGTHS_TO_PIXELS['in'] / 25.4
|
||||
'q': 96. / 25.4 / 4, # LENGTHS_TO_PIXELS['mm'] / 4
|
||||
}
|
||||
|
||||
# https://drafts.csswg.org/css-values/#resolution
|
||||
RESOLUTION_TO_DPPX = {
|
||||
'dppx': 1,
|
||||
'dpi': 1 / LENGTHS_TO_PIXELS['in'],
|
||||
'dpcm': 1 / LENGTHS_TO_PIXELS['cm'],
|
||||
}
|
||||
|
||||
# Sets of possible length units
|
||||
LENGTH_UNITS = set(LENGTHS_TO_PIXELS) | set(['ex', 'em', 'ch', 'rem'])
|
||||
|
||||
# Constants about background positions
|
||||
ZERO_PERCENT = Dimension(0, '%')
|
||||
FIFTY_PERCENT = Dimension(50, '%')
|
||||
HUNDRED_PERCENT = Dimension(100, '%')
|
||||
BACKGROUND_POSITION_PERCENTAGES = {
|
||||
'top': ZERO_PERCENT,
|
||||
'left': ZERO_PERCENT,
|
||||
'center': FIFTY_PERCENT,
|
||||
'bottom': HUNDRED_PERCENT,
|
||||
'right': HUNDRED_PERCENT,
|
||||
}
|
||||
|
||||
# Direction keywords used for gradients
|
||||
DIRECTION_KEYWORDS = {
|
||||
# ('angle', radians) 0 upwards, then clockwise
|
||||
('to', 'top'): ('angle', 0),
|
||||
('to', 'right'): ('angle', math.pi / 2),
|
||||
('to', 'bottom'): ('angle', math.pi),
|
||||
('to', 'left'): ('angle', math.pi * 3 / 2),
|
||||
# ('corner', keyword)
|
||||
('to', 'top', 'left'): ('corner', 'top_left'),
|
||||
('to', 'left', 'top'): ('corner', 'top_left'),
|
||||
('to', 'top', 'right'): ('corner', 'top_right'),
|
||||
('to', 'right', 'top'): ('corner', 'top_right'),
|
||||
('to', 'bottom', 'left'): ('corner', 'bottom_left'),
|
||||
('to', 'left', 'bottom'): ('corner', 'bottom_left'),
|
||||
('to', 'bottom', 'right'): ('corner', 'bottom_right'),
|
||||
('to', 'right', 'bottom'): ('corner', 'bottom_right'),
|
||||
}
|
||||
|
||||
# Default fallback values used in attr() functions
|
||||
ATTR_FALLBACKS = {
|
||||
'string': ('string', ''),
|
||||
'color': ('ident', 'currentcolor'),
|
||||
'url': ('external', 'about:invalid'),
|
||||
'integer': ('number', 0),
|
||||
'number': ('number', 0),
|
||||
'%': ('number', 0),
|
||||
}
|
||||
for unit in LENGTH_UNITS:
|
||||
ATTR_FALLBACKS[unit] = ('length', Dimension('0', unit))
|
||||
for unit in ANGLE_TO_RADIANS:
|
||||
ATTR_FALLBACKS[unit] = ('angle', Dimension('0', unit))
|
||||
|
||||
|
||||
class InvalidValues(ValueError): # noqa: N818
|
||||
"""Invalid or unsupported values for a known CSS property."""
|
||||
|
||||
|
||||
class CenterKeywordFakeToken:
|
||||
type = 'ident'
|
||||
lower_value = 'center'
|
||||
unit = None
|
||||
|
||||
|
||||
class Pending(ABC):
|
||||
"""Abstract class representing property value with pending validation."""
|
||||
# See https://drafts.csswg.org/css-variables-2/#variables-in-shorthands.
|
||||
def __init__(self, tokens, name):
|
||||
self.tokens = tokens
|
||||
self.name = name
|
||||
self._reported_error = False
|
||||
|
||||
@abstractmethod
|
||||
def validate(self, tokens, wanted_key):
|
||||
"""Get validated value for wanted key."""
|
||||
raise NotImplementedError
|
||||
|
||||
def solve(self, tokens, wanted_key):
|
||||
"""Get validated value or raise error."""
|
||||
try:
|
||||
if not tokens:
|
||||
# Having no tokens is allowed by grammar but refused by all
|
||||
# properties and expanders.
|
||||
raise InvalidValues('no value')
|
||||
return self.validate(tokens, wanted_key)
|
||||
except InvalidValues as exc:
|
||||
if self._reported_error:
|
||||
raise exc
|
||||
source_line = self.tokens[0].source_line
|
||||
source_column = self.tokens[0].source_column
|
||||
value = ' '.join(token.serialize() for token in tokens)
|
||||
message = (exc.args and exc.args[0]) or 'invalid value'
|
||||
LOGGER.warning(
|
||||
'Ignored `%s: %s` at %d:%d, %s.',
|
||||
self.name, value, source_line, source_column, message)
|
||||
self._reported_error = True
|
||||
raise exc
|
||||
|
||||
|
||||
def split_on_comma(tokens):
|
||||
"""Split a list of tokens on commas, ie ``LiteralToken(',')``.
|
||||
|
||||
Only "top-level" comma tokens are splitting points, not commas inside a
|
||||
function or blocks.
|
||||
|
||||
"""
|
||||
parts = []
|
||||
this_part = []
|
||||
for token in tokens:
|
||||
if token.type == 'literal' and token.value == ',':
|
||||
parts.append(this_part)
|
||||
this_part = []
|
||||
else:
|
||||
this_part.append(token)
|
||||
parts.append(this_part)
|
||||
return tuple(parts)
|
||||
|
||||
|
||||
def split_on_optional_comma(tokens):
|
||||
"""Split a list of tokens on optional commas, ie ``LiteralToken(',')``."""
|
||||
parts = []
|
||||
for split_part in split_on_comma(tokens):
|
||||
if not split_part:
|
||||
# Happens when there's a comma at the beginning, at the end, or
|
||||
# when two commas are next to each other.
|
||||
return
|
||||
for part in split_part:
|
||||
parts.append(part)
|
||||
return parts
|
||||
|
||||
|
||||
def remove_whitespace(tokens):
|
||||
"""Remove any top-level whitespace and comments in a token list."""
|
||||
return tuple(
|
||||
token for token in tokens
|
||||
if token.type not in ('whitespace', 'comment'))
|
||||
|
||||
|
||||
def safe_urljoin(base_url, url):
|
||||
if url_is_absolute(url):
|
||||
return iri_to_uri(url)
|
||||
elif base_url:
|
||||
return iri_to_uri(urljoin(base_url, url))
|
||||
else:
|
||||
raise InvalidValues(
|
||||
f'Relative URI reference without a base URI: {url!r}')
|
||||
|
||||
|
||||
def comma_separated_list(function):
|
||||
"""Decorator for validators that accept a comma separated list."""
|
||||
@functools.wraps(function)
|
||||
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 tuple(results)
|
||||
wrapper.single_value = function
|
||||
return wrapper
|
||||
|
||||
|
||||
def get_keyword(token):
|
||||
"""If ``token`` is a keyword, return its lowercase name.
|
||||
|
||||
Otherwise return ``None``.
|
||||
|
||||
"""
|
||||
if token.type == 'ident':
|
||||
return token.lower_value
|
||||
|
||||
|
||||
def get_custom_ident(token):
|
||||
"""If ``token`` is a keyword, return its name.
|
||||
|
||||
Otherwise return ``None``.
|
||||
|
||||
"""
|
||||
if token.type == 'ident':
|
||||
return token.value
|
||||
|
||||
|
||||
def get_single_keyword(tokens):
|
||||
"""If ``values`` is a 1-element list of keywords, return its name.
|
||||
|
||||
Otherwise return ``None``.
|
||||
|
||||
"""
|
||||
if len(tokens) == 1:
|
||||
token = tokens[0]
|
||||
if token.type == 'ident':
|
||||
return token.lower_value
|
||||
|
||||
|
||||
def single_keyword(function):
|
||||
"""Decorator for validators that only accept a single keyword."""
|
||||
@functools.wraps(function)
|
||||
def keyword_validator(tokens):
|
||||
"""Wrap a validator to call get_single_keyword on tokens."""
|
||||
keyword = get_single_keyword(tokens)
|
||||
if function(keyword):
|
||||
return keyword
|
||||
return keyword_validator
|
||||
|
||||
|
||||
def single_token(function):
|
||||
"""Decorator for validators that only accept a single token."""
|
||||
@functools.wraps(function)
|
||||
def single_token_validator(tokens, *args):
|
||||
"""Validate a property whose token is single."""
|
||||
if len(tokens) == 1:
|
||||
return function(tokens[0], *args)
|
||||
single_token_validator.__func__ = function
|
||||
return single_token_validator
|
||||
|
||||
|
||||
def parse_linear_gradient_parameters(arguments):
|
||||
first_arg = arguments[0]
|
||||
if len(first_arg) == 1:
|
||||
angle = get_angle(first_arg[0])
|
||||
if angle is not None:
|
||||
return ('angle', angle), arguments[1:]
|
||||
else:
|
||||
result = DIRECTION_KEYWORDS.get(tuple(map(get_keyword, first_arg)))
|
||||
if result is not None:
|
||||
return result, arguments[1:]
|
||||
return ('angle', math.pi), arguments # Default direction is 'to bottom'
|
||||
|
||||
|
||||
def parse_2d_position(tokens):
|
||||
"""Common syntax of background-position and transform-origin."""
|
||||
if len(tokens) == 1:
|
||||
tokens = [tokens[0], CenterKeywordFakeToken]
|
||||
elif len(tokens) != 2:
|
||||
return None
|
||||
|
||||
token_1, token_2 = tokens
|
||||
length_1 = get_length(token_1, percentage=True)
|
||||
length_2 = get_length(token_2, percentage=True)
|
||||
if length_1 and length_2:
|
||||
return length_1, length_2
|
||||
keyword_1, keyword_2 = map(get_keyword, tokens)
|
||||
if length_1 and keyword_2 in ('top', 'center', 'bottom'):
|
||||
return length_1, BACKGROUND_POSITION_PERCENTAGES[keyword_2]
|
||||
elif length_2 and keyword_1 in ('left', 'center', 'right'):
|
||||
return BACKGROUND_POSITION_PERCENTAGES[keyword_1], length_2
|
||||
elif (keyword_1 in ('left', 'center', 'right') and
|
||||
keyword_2 in ('top', 'center', 'bottom')):
|
||||
return (BACKGROUND_POSITION_PERCENTAGES[keyword_1],
|
||||
BACKGROUND_POSITION_PERCENTAGES[keyword_2])
|
||||
elif (keyword_1 in ('top', 'center', 'bottom') and
|
||||
keyword_2 in ('left', 'center', 'right')):
|
||||
# Swap tokens. They need to be in (horizontal, vertical) order.
|
||||
return (BACKGROUND_POSITION_PERCENTAGES[keyword_2],
|
||||
BACKGROUND_POSITION_PERCENTAGES[keyword_1])
|
||||
|
||||
|
||||
def parse_position(tokens):
|
||||
"""Parse background-position and object-position.
|
||||
|
||||
See https://drafts.csswg.org/css-backgrounds-3/#the-background-position
|
||||
https://drafts.csswg.org/css-images-3/#propdef-object-position
|
||||
|
||||
"""
|
||||
result = parse_2d_position(tokens)
|
||||
if result is not None:
|
||||
pos_x, pos_y = result
|
||||
return 'left', pos_x, 'top', pos_y
|
||||
|
||||
if len(tokens) == 4:
|
||||
keyword_1 = get_keyword(tokens[0])
|
||||
keyword_2 = get_keyword(tokens[2])
|
||||
length_1 = get_length(tokens[1], percentage=True)
|
||||
length_2 = get_length(tokens[3], percentage=True)
|
||||
if length_1 and length_2:
|
||||
if (keyword_1 in ('left', 'right') and
|
||||
keyword_2 in ('top', 'bottom')):
|
||||
return keyword_1, length_1, keyword_2, length_2
|
||||
if (keyword_2 in ('left', 'right') and
|
||||
keyword_1 in ('top', 'bottom')):
|
||||
return keyword_2, length_2, keyword_1, length_1
|
||||
|
||||
if len(tokens) == 3:
|
||||
length = get_length(tokens[2], percentage=True)
|
||||
if length is not None:
|
||||
keyword = get_keyword(tokens[1])
|
||||
other_keyword = get_keyword(tokens[0])
|
||||
else:
|
||||
length = get_length(tokens[1], percentage=True)
|
||||
other_keyword = get_keyword(tokens[2])
|
||||
keyword = get_keyword(tokens[0])
|
||||
|
||||
if length is not None:
|
||||
if other_keyword == 'center':
|
||||
if keyword in ('top', 'bottom'):
|
||||
return 'left', FIFTY_PERCENT, keyword, length
|
||||
if keyword in ('left', 'right'):
|
||||
return keyword, length, 'top', FIFTY_PERCENT
|
||||
elif (keyword in ('left', 'right') and
|
||||
other_keyword in ('top', 'bottom')):
|
||||
return keyword, length, other_keyword, ZERO_PERCENT
|
||||
elif (keyword in ('top', 'bottom') and
|
||||
other_keyword in ('left', 'right')):
|
||||
return other_keyword, ZERO_PERCENT, keyword, length
|
||||
|
||||
|
||||
def parse_radial_gradient_parameters(arguments):
|
||||
shape = None
|
||||
position = None
|
||||
size = None
|
||||
size_shape = None
|
||||
stack = arguments[0][::-1]
|
||||
while stack:
|
||||
token = stack.pop()
|
||||
keyword = get_keyword(token)
|
||||
if keyword == 'at':
|
||||
position = parse_position(stack[::-1])
|
||||
if position is None:
|
||||
return
|
||||
break
|
||||
elif keyword in ('circle', 'ellipse') and shape is None:
|
||||
shape = keyword
|
||||
elif keyword in ('closest-corner', 'farthest-corner',
|
||||
'closest-side', 'farthest-side') and size is None:
|
||||
size = 'keyword', keyword
|
||||
else:
|
||||
if stack and size is None:
|
||||
length_1 = get_length(token, percentage=True)
|
||||
length_2 = get_length(stack[-1], percentage=True)
|
||||
if None not in (length_1, length_2):
|
||||
size = 'explicit', (length_1, length_2)
|
||||
size_shape = 'ellipse'
|
||||
stack.pop()
|
||||
if size is None:
|
||||
length_1 = get_length(token)
|
||||
if length_1 is not None:
|
||||
size = 'explicit', (length_1, length_1)
|
||||
size_shape = 'circle'
|
||||
if size is None:
|
||||
return
|
||||
if (shape, size_shape) in (('circle', 'ellipse'), ('circle', 'ellipse')):
|
||||
return
|
||||
return (
|
||||
shape or size_shape or 'ellipse',
|
||||
size or ('keyword', 'farthest-corner'),
|
||||
position or ('left', FIFTY_PERCENT, 'top', FIFTY_PERCENT),
|
||||
arguments[1:])
|
||||
|
||||
|
||||
def parse_color_stop(tokens):
|
||||
if len(tokens) == 1:
|
||||
color = parse_color(tokens[0])
|
||||
if color == 'currentColor':
|
||||
# TODO: return the current color instead
|
||||
return parse_color('black'), None
|
||||
if color is not None:
|
||||
return color, None
|
||||
elif len(tokens) == 2:
|
||||
color = parse_color(tokens[0])
|
||||
position = get_length(tokens[1], negative=True, percentage=True)
|
||||
if color is not None and position is not None:
|
||||
return color, position
|
||||
raise InvalidValues
|
||||
|
||||
|
||||
def parse_function(function_token):
|
||||
"""Parse functional notation.
|
||||
|
||||
Return ``(name, args)`` if the given token is a function with comma- or
|
||||
space-separated arguments. Return ``None`` otherwise.
|
||||
|
||||
"""
|
||||
if function_token.type != 'function':
|
||||
return
|
||||
|
||||
content = list(remove_whitespace(function_token.arguments))
|
||||
arguments = []
|
||||
last_is_comma = False
|
||||
while content:
|
||||
token = content.pop(0)
|
||||
is_comma = token.type == 'literal' and token.value == ','
|
||||
if last_is_comma and is_comma:
|
||||
return
|
||||
if is_comma:
|
||||
last_is_comma = True
|
||||
else:
|
||||
last_is_comma = False
|
||||
if token.type == 'function':
|
||||
argument_function = parse_function(token)
|
||||
if argument_function is None:
|
||||
return
|
||||
arguments.append(token)
|
||||
if last_is_comma:
|
||||
return
|
||||
return function_token.lower_name, arguments
|
||||
|
||||
|
||||
def check_attr_function(token, allowed_type=None):
|
||||
function = parse_function(token)
|
||||
if function is None:
|
||||
return
|
||||
name, args = function
|
||||
if name == 'attr' and len(args) in (1, 2, 3):
|
||||
if args[0].type != 'ident':
|
||||
return
|
||||
attr_name = args[0].value
|
||||
if len(args) == 1:
|
||||
type_or_unit = 'string'
|
||||
fallback = ''
|
||||
else:
|
||||
if args[1].type != 'ident':
|
||||
return
|
||||
type_or_unit = args[1].value
|
||||
if type_or_unit not in ATTR_FALLBACKS:
|
||||
return
|
||||
if len(args) == 2:
|
||||
fallback = ATTR_FALLBACKS[type_or_unit]
|
||||
else:
|
||||
fallback_type = args[2].type
|
||||
if fallback_type == 'string':
|
||||
fallback = args[2].value
|
||||
else:
|
||||
# TODO: handle other fallback types
|
||||
return
|
||||
if allowed_type in (None, type_or_unit):
|
||||
return ('attr()', (attr_name, type_or_unit, fallback))
|
||||
|
||||
|
||||
def check_counter_function(token, allowed_type=None):
|
||||
from .validation.properties import list_style_type
|
||||
|
||||
function = parse_function(token)
|
||||
if function is None:
|
||||
return
|
||||
name, args = function
|
||||
arguments = []
|
||||
if (name == 'counter' and len(args) in (1, 2)) or (
|
||||
name == 'counters' and len(args) in (2, 3)):
|
||||
ident = args.pop(0)
|
||||
if ident.type != 'ident':
|
||||
return
|
||||
arguments.append(ident.value)
|
||||
|
||||
if name == 'counters':
|
||||
string = args.pop(0)
|
||||
if string.type != 'string':
|
||||
return
|
||||
arguments.append(string.value)
|
||||
|
||||
if args:
|
||||
counter_style = list_style_type((args.pop(0),))
|
||||
if counter_style is None:
|
||||
return
|
||||
arguments.append(counter_style)
|
||||
else:
|
||||
arguments.append('decimal')
|
||||
|
||||
return (f'{name}()', tuple(arguments))
|
||||
|
||||
|
||||
def check_content_function(token):
|
||||
function = parse_function(token)
|
||||
if function is None:
|
||||
return
|
||||
name, args = function
|
||||
if name == 'content':
|
||||
if len(args) == 0:
|
||||
return ('content()', 'text')
|
||||
elif len(args) == 1:
|
||||
ident = args.pop(0)
|
||||
if ident.type == 'ident' and ident.lower_value in (
|
||||
'text', 'before', 'after', 'first-letter', 'marker'):
|
||||
return ('content()', ident.lower_value)
|
||||
|
||||
|
||||
def check_string_or_element_function(string_or_element, token):
|
||||
function = parse_function(token)
|
||||
if function is None:
|
||||
return
|
||||
name, args = function
|
||||
if name == string_or_element and len(args) in (1, 2):
|
||||
custom_ident = args.pop(0)
|
||||
if custom_ident.type != 'ident':
|
||||
return
|
||||
custom_ident = custom_ident.value
|
||||
|
||||
if args:
|
||||
ident = args.pop(0)
|
||||
if ident.type != 'ident' or ident.lower_value not in (
|
||||
'first', 'start', 'last', 'first-except'):
|
||||
return
|
||||
ident = ident.lower_value
|
||||
else:
|
||||
ident = 'first'
|
||||
|
||||
return (f'{string_or_element}()', (custom_ident, ident))
|
||||
|
||||
|
||||
def check_var_function(token):
|
||||
if function := parse_function(token):
|
||||
name, args = function
|
||||
if name == 'var' and args:
|
||||
ident = args.pop(0)
|
||||
# TODO: we should check authorized tokens
|
||||
# https://drafts.csswg.org/css-syntax-3/#typedef-declaration-value
|
||||
return ident.type == 'ident' and ident.value.startswith('--')
|
||||
for arg in args:
|
||||
if check_var_function(arg):
|
||||
return True
|
||||
|
||||
|
||||
def get_string(token):
|
||||
"""Parse a <string> token."""
|
||||
if token.type == 'string':
|
||||
return ('string', token.value)
|
||||
if token.type == 'function':
|
||||
if token.name == 'attr':
|
||||
return check_attr_function(token, 'string')
|
||||
elif token.name in ('counter', 'counters'):
|
||||
return check_counter_function(token)
|
||||
elif token.name == 'content':
|
||||
return check_content_function(token)
|
||||
elif token.name == 'string':
|
||||
return check_string_or_element_function('string', token)
|
||||
|
||||
|
||||
def get_length(token, negative=True, percentage=False):
|
||||
"""Parse a <length> token."""
|
||||
if percentage and token.type == 'percentage':
|
||||
if negative or token.value >= 0:
|
||||
return Dimension(token.value, '%')
|
||||
if token.type == 'dimension' and token.unit in LENGTH_UNITS:
|
||||
if negative or token.value >= 0:
|
||||
return Dimension(token.value, token.unit)
|
||||
if token.type == 'number' and token.value == 0:
|
||||
return Dimension(0, None)
|
||||
|
||||
|
||||
def get_angle(token):
|
||||
"""Parse an <angle> token in radians."""
|
||||
if token.type == 'dimension':
|
||||
factor = ANGLE_TO_RADIANS.get(token.unit)
|
||||
if factor is not None:
|
||||
return token.value * factor
|
||||
|
||||
|
||||
def get_resolution(token):
|
||||
"""Parse a <resolution> token in ddpx."""
|
||||
if token.type == 'dimension':
|
||||
factor = RESOLUTION_TO_DPPX.get(token.unit)
|
||||
if factor is not None:
|
||||
return token.value * factor
|
||||
|
||||
|
||||
def get_image(token, base_url):
|
||||
"""Parse an <image> token."""
|
||||
from ..images import LinearGradient, RadialGradient
|
||||
|
||||
parsed_url = get_url(token, base_url)
|
||||
if parsed_url:
|
||||
assert parsed_url[0] == 'url'
|
||||
if parsed_url[1][0] == 'external':
|
||||
return 'url', parsed_url[1][1]
|
||||
if token.type != 'function':
|
||||
return
|
||||
arguments = split_on_comma(remove_whitespace(token.arguments))
|
||||
name = token.lower_name
|
||||
if name in ('linear-gradient', 'repeating-linear-gradient'):
|
||||
direction, color_stops = parse_linear_gradient_parameters(arguments)
|
||||
if color_stops:
|
||||
return 'linear-gradient', LinearGradient(
|
||||
[parse_color_stop(stop) for stop in color_stops],
|
||||
direction, 'repeating' in name)
|
||||
elif name in ('radial-gradient', 'repeating-radial-gradient'):
|
||||
result = parse_radial_gradient_parameters(arguments)
|
||||
if result is not None:
|
||||
shape, size, position, color_stops = result
|
||||
else:
|
||||
shape = 'ellipse'
|
||||
size = 'keyword', 'farthest-corner'
|
||||
position = 'left', FIFTY_PERCENT, 'top', FIFTY_PERCENT
|
||||
color_stops = arguments
|
||||
if color_stops:
|
||||
return 'radial-gradient', RadialGradient(
|
||||
[parse_color_stop(stop) for stop in color_stops],
|
||||
shape, size, position, 'repeating' in name)
|
||||
|
||||
|
||||
def _get_url_tuple(string, base_url):
|
||||
if string.startswith('#'):
|
||||
return ('url', ('internal', unquote(string[1:])))
|
||||
else:
|
||||
return ('url', ('external', safe_urljoin(base_url, string)))
|
||||
|
||||
|
||||
def get_url(token, base_url):
|
||||
"""Parse an <url> token."""
|
||||
if token.type == 'url':
|
||||
return _get_url_tuple(token.value, base_url)
|
||||
elif token.type == 'function':
|
||||
if token.name == 'attr':
|
||||
return check_attr_function(token, 'url')
|
||||
elif token.name == 'url' and len(token.arguments) in (1, 2):
|
||||
# Ignore url modifiers
|
||||
# See https://drafts.csswg.org/css-values-3/#urls
|
||||
return _get_url_tuple(token.arguments[0].value, base_url)
|
||||
|
||||
|
||||
def get_quote(token):
|
||||
"""Parse a <quote> token."""
|
||||
keyword = get_keyword(token)
|
||||
if keyword in (
|
||||
'open-quote', 'close-quote',
|
||||
'no-open-quote', 'no-close-quote'):
|
||||
return keyword
|
||||
|
||||
|
||||
def get_target(token, base_url):
|
||||
"""Parse a <target> token."""
|
||||
function = parse_function(token)
|
||||
if function is None:
|
||||
return
|
||||
name, args = function
|
||||
args = split_on_optional_comma(args)
|
||||
if not args:
|
||||
return
|
||||
|
||||
if name == 'target-counter':
|
||||
if len(args) not in (2, 3):
|
||||
return
|
||||
elif name == 'target-counters':
|
||||
if len(args) not in (3, 4):
|
||||
return
|
||||
elif name == 'target-text':
|
||||
if len(args) not in (1, 2):
|
||||
return
|
||||
else:
|
||||
return
|
||||
|
||||
values = []
|
||||
|
||||
link = args.pop(0)
|
||||
string_link = get_string(link)
|
||||
if string_link is None:
|
||||
url = get_url(link, base_url)
|
||||
if url is None:
|
||||
return
|
||||
values.append(url)
|
||||
else:
|
||||
values.append(string_link)
|
||||
|
||||
if name.startswith('target-counter'):
|
||||
if not args:
|
||||
return
|
||||
|
||||
ident = args.pop(0)
|
||||
if ident.type != 'ident':
|
||||
return
|
||||
values.append(ident.value)
|
||||
|
||||
if name == 'target-counters':
|
||||
string = get_string(args.pop(0))
|
||||
if string is None:
|
||||
return
|
||||
values.append(string)
|
||||
|
||||
if args:
|
||||
counter_style = get_keyword(args.pop(0))
|
||||
else:
|
||||
counter_style = 'decimal'
|
||||
values.append(counter_style)
|
||||
else:
|
||||
if args:
|
||||
content = get_keyword(args.pop(0))
|
||||
if content not in ('content', 'before', 'after', 'first-letter'):
|
||||
return
|
||||
else:
|
||||
content = 'content'
|
||||
values.append(content)
|
||||
|
||||
return (f'{name}()', tuple(values))
|
||||
|
||||
|
||||
def get_content_list(tokens, base_url):
|
||||
"""Parse <content-list> tokens."""
|
||||
# See https://www.w3.org/TR/css-content-3/#typedef-content-list
|
||||
parsed_tokens = [
|
||||
get_content_list_token(token, base_url) for token in tokens]
|
||||
if None not in parsed_tokens:
|
||||
return parsed_tokens
|
||||
|
||||
|
||||
def get_content_list_token(token, base_url):
|
||||
"""Parse one of the <content-list> tokens."""
|
||||
# See https://www.w3.org/TR/css-content-3/#typedef-content-list
|
||||
|
||||
# <string>
|
||||
string = get_string(token)
|
||||
if string is not None:
|
||||
return string
|
||||
|
||||
# contents
|
||||
if get_keyword(token) == 'contents':
|
||||
return ('content()', 'text')
|
||||
|
||||
# <uri>
|
||||
url = get_url(token, base_url)
|
||||
if url is not None:
|
||||
return url
|
||||
|
||||
# <quote>
|
||||
quote = get_quote(token)
|
||||
if quote is not None:
|
||||
return ('quote', quote)
|
||||
|
||||
# <target>
|
||||
target = get_target(token, base_url)
|
||||
if target is not None:
|
||||
return target
|
||||
|
||||
function = parse_function(token)
|
||||
if function is None:
|
||||
return
|
||||
name, args = function
|
||||
|
||||
# <leader()>
|
||||
if name == 'leader':
|
||||
if len(args) != 1:
|
||||
return
|
||||
arg, = args
|
||||
if arg.type == 'ident':
|
||||
if arg.value == 'dotted':
|
||||
string = '.'
|
||||
elif arg.value == 'solid':
|
||||
string = '_'
|
||||
elif arg.value == 'space':
|
||||
string = ' '
|
||||
else:
|
||||
return
|
||||
elif arg.type == 'string':
|
||||
string = arg.value
|
||||
return ('leader()', ('string', string))
|
||||
|
||||
# <element()>
|
||||
elif name == 'element':
|
||||
return check_string_or_element_function('element', token)
|
||||
Reference in New Issue
Block a user