1604 lines
46 KiB
Python
1604 lines
46 KiB
Python
|
|
"""Validate properties.
|
|||
|
|
|
|||
|
|
See https://www.w3.org/TR/CSS21/propidx.html and various CSS3 modules.
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from math import inf
|
|||
|
|
|
|||
|
|
from tinycss2.color3 import parse_color
|
|||
|
|
|
|||
|
|
from .. import computed_values
|
|||
|
|
from ..properties import KNOWN_PROPERTIES, Dimension
|
|||
|
|
from ..utils import (
|
|||
|
|
InvalidValues, check_var_function, comma_separated_list, get_angle,
|
|||
|
|
get_content_list, get_content_list_token, get_custom_ident, get_image,
|
|||
|
|
get_keyword, get_length, get_resolution, get_single_keyword, get_url,
|
|||
|
|
parse_2d_position, parse_function, parse_position, remove_whitespace,
|
|||
|
|
single_keyword, single_token)
|
|||
|
|
|
|||
|
|
PREFIX = '-weasy-'
|
|||
|
|
PROPRIETARY = set()
|
|||
|
|
UNSTABLE = set()
|
|||
|
|
MULTIVAL_PROPERTIES = set()
|
|||
|
|
|
|||
|
|
# Yes/no validators for non-shorthand properties
|
|||
|
|
# Maps property names to functions taking a property name and a value list,
|
|||
|
|
# returning a value or None for invalid.
|
|||
|
|
# For properties that take a single value, that value is returned by itself
|
|||
|
|
# instead of a list.
|
|||
|
|
PROPERTIES = {}
|
|||
|
|
|
|||
|
|
|
|||
|
|
# Validators
|
|||
|
|
|
|||
|
|
def property(property_name=None, proprietary=False, unstable=False,
|
|||
|
|
multiple_values=False, wants_base_url=False):
|
|||
|
|
"""Decorator adding a function to the ``PROPERTIES``.
|
|||
|
|
|
|||
|
|
The name of the property covered by the decorated function is set to
|
|||
|
|
``property_name`` if given, or is inferred from the function name
|
|||
|
|
(replacing underscores by hyphens).
|
|||
|
|
|
|||
|
|
:param proprietary:
|
|||
|
|
Proprietary (vendor-specific, non-standard) are prefixed: anchors can
|
|||
|
|
for example be set using ``-weasy-anchor: attr(id)``.
|
|||
|
|
See https://www.w3.org/TR/CSS/#proprietary
|
|||
|
|
:param unstable:
|
|||
|
|
Mark properties that are defined in specifications that didn't reach
|
|||
|
|
the Candidate Recommandation stage. They can be used both
|
|||
|
|
vendor-prefixed or unprefixed.
|
|||
|
|
See https://www.w3.org/TR/CSS/#unstable-syntax
|
|||
|
|
:param wants_base_url:
|
|||
|
|
The function takes the stylesheet’s base URL as an additional
|
|||
|
|
parameter.
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
def decorator(function):
|
|||
|
|
"""Add ``function`` to the ``PROPERTIES``."""
|
|||
|
|
if property_name is None:
|
|||
|
|
name = function.__name__.replace('_', '-')
|
|||
|
|
else:
|
|||
|
|
name = property_name
|
|||
|
|
assert name in KNOWN_PROPERTIES, name
|
|||
|
|
assert name not in PROPERTIES, name
|
|||
|
|
|
|||
|
|
function.wants_base_url = wants_base_url
|
|||
|
|
PROPERTIES[name] = function
|
|||
|
|
if proprietary:
|
|||
|
|
PROPRIETARY.add(name)
|
|||
|
|
if unstable:
|
|||
|
|
UNSTABLE.add(name)
|
|||
|
|
if multiple_values:
|
|||
|
|
MULTIVAL_PROPERTIES.add(name)
|
|||
|
|
return function
|
|||
|
|
return decorator
|
|||
|
|
|
|||
|
|
|
|||
|
|
def validate_non_shorthand(base_url, name, tokens, required=False):
|
|||
|
|
"""Default validator for non-shorthand properties."""
|
|||
|
|
if name.startswith('--'):
|
|||
|
|
# TODO: validate content
|
|||
|
|
return ((name, tokens),)
|
|||
|
|
|
|||
|
|
if not required and name not in KNOWN_PROPERTIES:
|
|||
|
|
hyphens_name = name.replace('_', '-')
|
|||
|
|
if hyphens_name in KNOWN_PROPERTIES:
|
|||
|
|
raise InvalidValues(f'you probably mean "{hyphens_name}"')
|
|||
|
|
else:
|
|||
|
|
raise InvalidValues('unknown property')
|
|||
|
|
|
|||
|
|
if not required and name not in PROPERTIES:
|
|||
|
|
raise InvalidValues('property not supported yet')
|
|||
|
|
|
|||
|
|
if name not in MULTIVAL_PROPERTIES:
|
|||
|
|
for token in tokens:
|
|||
|
|
var_function = check_var_function(token)
|
|||
|
|
if var_function:
|
|||
|
|
return ((name, var_function),)
|
|||
|
|
|
|||
|
|
keyword = get_single_keyword(tokens)
|
|||
|
|
if keyword in ('initial', 'inherit'):
|
|||
|
|
value = keyword
|
|||
|
|
else:
|
|||
|
|
function = PROPERTIES[name]
|
|||
|
|
if function.wants_base_url:
|
|||
|
|
value = function(tokens, base_url)
|
|||
|
|
else:
|
|||
|
|
value = function(tokens)
|
|||
|
|
if value is None:
|
|||
|
|
raise InvalidValues
|
|||
|
|
return ((name, value),)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@comma_separated_list
|
|||
|
|
@single_keyword
|
|||
|
|
def background_attachment(keyword):
|
|||
|
|
"""``background-attachment`` property validation."""
|
|||
|
|
return keyword in ('scroll', 'fixed', 'local')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property('background-color')
|
|||
|
|
@property('border-top-color')
|
|||
|
|
@property('border-right-color')
|
|||
|
|
@property('border-bottom-color')
|
|||
|
|
@property('border-left-color')
|
|||
|
|
@property('column-rule-color', unstable=True)
|
|||
|
|
@property('text-decoration-color')
|
|||
|
|
@single_token
|
|||
|
|
def other_colors(token):
|
|||
|
|
return parse_color(token)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_token
|
|||
|
|
def outline_color(token):
|
|||
|
|
if get_keyword(token) == 'invert':
|
|||
|
|
return 'currentColor'
|
|||
|
|
else:
|
|||
|
|
return parse_color(token)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def border_collapse(keyword):
|
|||
|
|
return keyword in ('separate', 'collapse')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def empty_cells(keyword):
|
|||
|
|
"""``empty-cells`` property validation."""
|
|||
|
|
return keyword in ('show', 'hide')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property('color')
|
|||
|
|
@single_token
|
|||
|
|
def color(token):
|
|||
|
|
"""``*-color`` and ``color`` properties validation."""
|
|||
|
|
result = parse_color(token)
|
|||
|
|
if result == 'currentColor':
|
|||
|
|
return 'inherit'
|
|||
|
|
else:
|
|||
|
|
return result
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property('background-image', wants_base_url=True)
|
|||
|
|
@comma_separated_list
|
|||
|
|
@single_token
|
|||
|
|
def background_image(token, base_url):
|
|||
|
|
if token.type != 'function':
|
|||
|
|
if get_keyword(token) == 'none':
|
|||
|
|
return 'none', None
|
|||
|
|
return get_image(token, base_url)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property('list-style-image', wants_base_url=True)
|
|||
|
|
@single_token
|
|||
|
|
def list_style_image(token, base_url):
|
|||
|
|
"""``list-style-image`` property validation."""
|
|||
|
|
if get_keyword(token) == 'none':
|
|||
|
|
return 'none', None
|
|||
|
|
parsed_url = get_url(token, base_url)
|
|||
|
|
if parsed_url:
|
|||
|
|
if parsed_url[0] == 'url' and parsed_url[1][0] == 'external':
|
|||
|
|
return 'url', parsed_url[1][1]
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
def transform_origin(tokens):
|
|||
|
|
"""``transform-origin`` property validation."""
|
|||
|
|
if len(tokens) == 3:
|
|||
|
|
# Ignore third parameter as 3D transforms are ignored.
|
|||
|
|
tokens = tokens[:2]
|
|||
|
|
return parse_2d_position(tokens)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@comma_separated_list
|
|||
|
|
def background_position(tokens):
|
|||
|
|
"""``background-position`` property validation."""
|
|||
|
|
return parse_position(tokens)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@comma_separated_list
|
|||
|
|
def object_position(tokens):
|
|||
|
|
"""``object-position`` property validation."""
|
|||
|
|
return parse_position(tokens)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@comma_separated_list
|
|||
|
|
def background_repeat(tokens):
|
|||
|
|
"""``background-repeat`` property validation."""
|
|||
|
|
keywords = tuple(map(get_keyword, tokens))
|
|||
|
|
if keywords == ('repeat-x',):
|
|||
|
|
return ('repeat', 'no-repeat')
|
|||
|
|
if keywords == ('repeat-y',):
|
|||
|
|
return ('no-repeat', 'repeat')
|
|||
|
|
if keywords in (('no-repeat',), ('repeat',), ('space',), ('round',)):
|
|||
|
|
return keywords * 2
|
|||
|
|
if len(keywords) == 2 and all(
|
|||
|
|
k in ('no-repeat', 'repeat', 'space', 'round')
|
|||
|
|
for k in keywords):
|
|||
|
|
return keywords
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@comma_separated_list
|
|||
|
|
def background_size(tokens):
|
|||
|
|
"""Validation for ``background-size``."""
|
|||
|
|
if len(tokens) == 1:
|
|||
|
|
token = tokens[0]
|
|||
|
|
keyword = get_keyword(token)
|
|||
|
|
if keyword in ('contain', 'cover'):
|
|||
|
|
return keyword
|
|||
|
|
if keyword == 'auto':
|
|||
|
|
return ('auto', 'auto')
|
|||
|
|
length = get_length(token, negative=False, percentage=True)
|
|||
|
|
if length:
|
|||
|
|
return (length, 'auto')
|
|||
|
|
elif len(tokens) == 2:
|
|||
|
|
values = []
|
|||
|
|
for token in tokens:
|
|||
|
|
length = get_length(token, negative=False, percentage=True)
|
|||
|
|
if length:
|
|||
|
|
values.append(length)
|
|||
|
|
elif get_keyword(token) == 'auto':
|
|||
|
|
values.append('auto')
|
|||
|
|
if len(values) == 2:
|
|||
|
|
return tuple(values)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property('background-clip')
|
|||
|
|
@property('background-origin')
|
|||
|
|
@comma_separated_list
|
|||
|
|
@single_keyword
|
|||
|
|
def box(keyword):
|
|||
|
|
"""Validation for the ``<box>`` type used in ``background-clip``
|
|||
|
|
and ``background-origin``."""
|
|||
|
|
return keyword in ('border-box', 'padding-box', 'content-box')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
def border_spacing(tokens):
|
|||
|
|
"""Validator for the `border-spacing` property."""
|
|||
|
|
lengths = [get_length(token, negative=False) for token in tokens]
|
|||
|
|
if all(lengths):
|
|||
|
|
if len(lengths) == 1:
|
|||
|
|
return (lengths[0], lengths[0])
|
|||
|
|
elif len(lengths) == 2:
|
|||
|
|
return tuple(lengths)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property('border-top-right-radius')
|
|||
|
|
@property('border-bottom-right-radius')
|
|||
|
|
@property('border-bottom-left-radius')
|
|||
|
|
@property('border-top-left-radius')
|
|||
|
|
def border_corner_radius(tokens):
|
|||
|
|
"""Validator for the `border-*-radius` properties."""
|
|||
|
|
lengths = [
|
|||
|
|
get_length(token, negative=False, percentage=True) for token in tokens]
|
|||
|
|
if all(lengths):
|
|||
|
|
if len(lengths) == 1:
|
|||
|
|
return (lengths[0], lengths[0])
|
|||
|
|
elif len(lengths) == 2:
|
|||
|
|
return tuple(lengths)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property('border-top-style')
|
|||
|
|
@property('border-right-style')
|
|||
|
|
@property('border-left-style')
|
|||
|
|
@property('border-bottom-style')
|
|||
|
|
@property('column-rule-style', unstable=True)
|
|||
|
|
@single_keyword
|
|||
|
|
def border_style(keyword):
|
|||
|
|
"""``border-*-style`` properties validation."""
|
|||
|
|
return keyword in ('none', 'hidden', 'dotted', 'dashed', 'double',
|
|||
|
|
'inset', 'outset', 'groove', 'ridge', 'solid')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property('break-before')
|
|||
|
|
@property('break-after')
|
|||
|
|
@single_keyword
|
|||
|
|
def break_before_after(keyword):
|
|||
|
|
"""``break-before`` and ``break-after`` properties validation."""
|
|||
|
|
return keyword in ('auto', 'avoid', 'avoid-page', 'page', 'left', 'right',
|
|||
|
|
'recto', 'verso', 'avoid-column', 'column', 'always')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def break_inside(keyword):
|
|||
|
|
"""``break-inside`` property validation."""
|
|||
|
|
return keyword in ('auto', 'avoid', 'avoid-page', 'avoid-column')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def box_decoration_break(keyword):
|
|||
|
|
"""``box-decoration-break`` property validation."""
|
|||
|
|
return keyword in ('slice', 'clone')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_token
|
|||
|
|
def block_ellipsis(token):
|
|||
|
|
"""``box-ellipsis`` property validation."""
|
|||
|
|
if token.type == 'string':
|
|||
|
|
return ('string', token.value)
|
|||
|
|
else:
|
|||
|
|
keyword = get_keyword(token)
|
|||
|
|
if keyword in ('none', 'auto'):
|
|||
|
|
return keyword
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property('continue', unstable=True)
|
|||
|
|
@single_keyword
|
|||
|
|
def continue_(keyword):
|
|||
|
|
"""``continue`` property validation."""
|
|||
|
|
return keyword in ('auto', 'discard')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property(unstable=True)
|
|||
|
|
@single_token
|
|||
|
|
def max_lines(token):
|
|||
|
|
if token.type == 'number' and token.int_value is not None:
|
|||
|
|
if token.int_value >= 1:
|
|||
|
|
return token.int_value
|
|||
|
|
keyword = get_keyword(token)
|
|||
|
|
if keyword == 'none':
|
|||
|
|
return keyword
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property(unstable=True)
|
|||
|
|
@single_keyword
|
|||
|
|
def margin_break(keyword):
|
|||
|
|
"""``margin-break`` property validation."""
|
|||
|
|
return keyword in ('auto', 'keep', 'discard')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property(unstable=True)
|
|||
|
|
@single_token
|
|||
|
|
def page(token):
|
|||
|
|
"""``page`` property validation."""
|
|||
|
|
if token.type == 'ident':
|
|||
|
|
return 'auto' if token.lower_value == 'auto' else token.value
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property('bleed-left', unstable=True)
|
|||
|
|
@property('bleed-right', unstable=True)
|
|||
|
|
@property('bleed-top', unstable=True)
|
|||
|
|
@property('bleed-bottom', unstable=True)
|
|||
|
|
@single_token
|
|||
|
|
def bleed(token):
|
|||
|
|
"""``bleed`` property validation."""
|
|||
|
|
keyword = get_keyword(token)
|
|||
|
|
if keyword == 'auto':
|
|||
|
|
return 'auto'
|
|||
|
|
else:
|
|||
|
|
return get_length(token)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property(unstable=True)
|
|||
|
|
def marks(tokens):
|
|||
|
|
"""``marks`` property validation."""
|
|||
|
|
if len(tokens) == 2:
|
|||
|
|
keywords = tuple(get_keyword(token) for token in tokens)
|
|||
|
|
if 'crop' in keywords and 'cross' in keywords:
|
|||
|
|
return keywords
|
|||
|
|
elif len(tokens) == 1:
|
|||
|
|
keyword = get_keyword(tokens[0])
|
|||
|
|
if keyword in ('crop', 'cross'):
|
|||
|
|
return (keyword,)
|
|||
|
|
elif keyword == 'none':
|
|||
|
|
return ()
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property('outline-style')
|
|||
|
|
@single_keyword
|
|||
|
|
def outline_style(keyword):
|
|||
|
|
"""``outline-style`` properties validation."""
|
|||
|
|
return keyword in ('none', 'dotted', 'dashed', 'double', 'inset',
|
|||
|
|
'outset', 'groove', 'ridge', 'solid')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property('border-top-width')
|
|||
|
|
@property('border-right-width')
|
|||
|
|
@property('border-left-width')
|
|||
|
|
@property('border-bottom-width')
|
|||
|
|
@property('column-rule-width', unstable=True)
|
|||
|
|
@property('outline-width')
|
|||
|
|
@single_token
|
|||
|
|
def border_width(token):
|
|||
|
|
"""Border, column rule and outline widths properties validation."""
|
|||
|
|
length = get_length(token, negative=False)
|
|||
|
|
if length:
|
|||
|
|
return length
|
|||
|
|
keyword = get_keyword(token)
|
|||
|
|
if keyword in ('thin', 'medium', 'thick'):
|
|||
|
|
return keyword
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property(unstable=True)
|
|||
|
|
@single_token
|
|||
|
|
def column_width(token):
|
|||
|
|
"""``column-width`` property validation."""
|
|||
|
|
length = get_length(token, negative=False)
|
|||
|
|
if length:
|
|||
|
|
return length
|
|||
|
|
keyword = get_keyword(token)
|
|||
|
|
if keyword == 'auto':
|
|||
|
|
return keyword
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property(unstable=True)
|
|||
|
|
@single_keyword
|
|||
|
|
def column_span(keyword):
|
|||
|
|
"""``column-span`` property validation."""
|
|||
|
|
return keyword in ('all', 'none')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def box_sizing(keyword):
|
|||
|
|
"""Validation for the ``box-sizing`` property from css3-ui"""
|
|||
|
|
return keyword in ('padding-box', 'border-box', 'content-box')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def caption_side(keyword):
|
|||
|
|
"""``caption-side`` properties validation."""
|
|||
|
|
return keyword in ('top', 'bottom')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def clear(keyword):
|
|||
|
|
"""``clear`` property validation."""
|
|||
|
|
return keyword in ('left', 'right', 'both', 'none')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_token
|
|||
|
|
def clip(token):
|
|||
|
|
"""Validation for the ``clip`` property."""
|
|||
|
|
function = parse_function(token)
|
|||
|
|
if function:
|
|||
|
|
name, args = function
|
|||
|
|
if name == 'rect' and len(args) == 4:
|
|||
|
|
values = []
|
|||
|
|
for arg in args:
|
|||
|
|
if get_keyword(arg) == 'auto':
|
|||
|
|
values.append('auto')
|
|||
|
|
else:
|
|||
|
|
length = get_length(arg)
|
|||
|
|
if length:
|
|||
|
|
values.append(length)
|
|||
|
|
if len(values) == 4:
|
|||
|
|
return tuple(values)
|
|||
|
|
if get_keyword(token) == 'auto':
|
|||
|
|
return ()
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property(multiple_values=True, wants_base_url=True)
|
|||
|
|
def content(tokens, base_url):
|
|||
|
|
"""``content`` property validation."""
|
|||
|
|
# See https://www.w3.org/TR/css-content-3/#content-property
|
|||
|
|
tokens = list(tokens)
|
|||
|
|
parsed_tokens = []
|
|||
|
|
while tokens:
|
|||
|
|
if len(tokens) >= 2 and (
|
|||
|
|
tokens[1].type == 'literal' and tokens[1].value == ','):
|
|||
|
|
token, tokens = tokens[0], tokens[2:]
|
|||
|
|
parsed_token = (
|
|||
|
|
get_image(token, base_url) or get_url(token, base_url))
|
|||
|
|
if parsed_token:
|
|||
|
|
parsed_tokens.append(parsed_token)
|
|||
|
|
else:
|
|||
|
|
return
|
|||
|
|
else:
|
|||
|
|
break
|
|||
|
|
if len(tokens) == 0:
|
|||
|
|
return
|
|||
|
|
if len(tokens) >= 3 and tokens[-1].type == 'string' and (
|
|||
|
|
tokens[-2].type == 'literal' and tokens[-2].value == '/'):
|
|||
|
|
# Ignore text for speech
|
|||
|
|
tokens = tokens[:-2]
|
|||
|
|
keyword = get_single_keyword(tokens)
|
|||
|
|
if keyword in ('normal', 'none'):
|
|||
|
|
return (keyword,)
|
|||
|
|
return get_content_list(tokens, base_url)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
def counter_increment(tokens):
|
|||
|
|
"""``counter-increment`` property validation."""
|
|||
|
|
return counter(tokens, default_integer=1)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
def counter_reset(tokens):
|
|||
|
|
"""``counter-reset`` property validation."""
|
|||
|
|
return counter(tokens, default_integer=0)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
def counter_set(tokens):
|
|||
|
|
"""``counter-set`` property validation."""
|
|||
|
|
return counter(tokens, default_integer=0)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def counter(tokens, default_integer):
|
|||
|
|
"""``counter-increment`` and ``counter-reset`` properties validation."""
|
|||
|
|
if get_single_keyword(tokens) == 'none':
|
|||
|
|
return ()
|
|||
|
|
tokens = iter(tokens)
|
|||
|
|
token = next(tokens, None)
|
|||
|
|
assert token, 'got an empty token list'
|
|||
|
|
results = []
|
|||
|
|
while token is not None:
|
|||
|
|
if token.type != 'ident':
|
|||
|
|
return # expected a keyword here
|
|||
|
|
counter_name = token.value
|
|||
|
|
if counter_name in ('none', 'initial', 'inherit'):
|
|||
|
|
raise InvalidValues(f'Invalid counter name: {counter_name}')
|
|||
|
|
token = next(tokens, None)
|
|||
|
|
if token is not None and (
|
|||
|
|
token.type == 'number' and token.int_value is not None):
|
|||
|
|
# Found an integer. Use it and get the next token
|
|||
|
|
integer = token.int_value
|
|||
|
|
token = next(tokens, None)
|
|||
|
|
else:
|
|||
|
|
# Not an integer. Might be the next counter name.
|
|||
|
|
# Keep `token` for the next loop iteration.
|
|||
|
|
integer = default_integer
|
|||
|
|
results.append((counter_name, integer))
|
|||
|
|
return tuple(results)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property('top')
|
|||
|
|
@property('right')
|
|||
|
|
@property('left')
|
|||
|
|
@property('bottom')
|
|||
|
|
@property('margin-top')
|
|||
|
|
@property('margin-right')
|
|||
|
|
@property('margin-bottom')
|
|||
|
|
@property('margin-left')
|
|||
|
|
@single_token
|
|||
|
|
def lenght_precentage_or_auto(token):
|
|||
|
|
"""``margin-*`` properties validation."""
|
|||
|
|
length = get_length(token, percentage=True)
|
|||
|
|
if length:
|
|||
|
|
return length
|
|||
|
|
if get_keyword(token) == 'auto':
|
|||
|
|
return 'auto'
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property('height')
|
|||
|
|
@property('width')
|
|||
|
|
@single_token
|
|||
|
|
def width_height(token):
|
|||
|
|
"""Validation for the ``width`` and ``height`` properties."""
|
|||
|
|
length = get_length(token, negative=False, percentage=True)
|
|||
|
|
if length:
|
|||
|
|
return length
|
|||
|
|
if get_keyword(token) == 'auto':
|
|||
|
|
return 'auto'
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property(unstable=True)
|
|||
|
|
@single_token
|
|||
|
|
def column_gap(token):
|
|||
|
|
"""Validation for the ``column-gap`` property."""
|
|||
|
|
length = get_length(token, negative=False)
|
|||
|
|
if length:
|
|||
|
|
return length
|
|||
|
|
keyword = get_keyword(token)
|
|||
|
|
if keyword == 'normal':
|
|||
|
|
return keyword
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property(unstable=True)
|
|||
|
|
@single_keyword
|
|||
|
|
def column_fill(keyword):
|
|||
|
|
"""``column-fill`` property validation."""
|
|||
|
|
return keyword in ('auto', 'balance')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def direction(keyword):
|
|||
|
|
"""``direction`` property validation."""
|
|||
|
|
return keyword in ('ltr', 'rtl')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
def display(tokens):
|
|||
|
|
"""``display`` property validation."""
|
|||
|
|
for token in tokens:
|
|||
|
|
if token.type != 'ident':
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
if len(tokens) == 1:
|
|||
|
|
value = tokens[0].value
|
|||
|
|
if value in (
|
|||
|
|
'none', 'table-caption', 'table-row-group', 'table-cell',
|
|||
|
|
'table-header-group', 'table-footer-group', 'table-row',
|
|||
|
|
'table-column-group', 'table-column'):
|
|||
|
|
return (value,)
|
|||
|
|
elif value in ('inline-table', 'inline-flex'):
|
|||
|
|
return tuple(value.split('-'))
|
|||
|
|
elif value == 'inline-block':
|
|||
|
|
return ('inline', 'flow-root')
|
|||
|
|
|
|||
|
|
outside = inside = list_item = None
|
|||
|
|
for token in tokens:
|
|||
|
|
value = token.value
|
|||
|
|
if value in ('block', 'inline'):
|
|||
|
|
if outside:
|
|||
|
|
return
|
|||
|
|
outside = value
|
|||
|
|
elif value in ('flow', 'flow-root', 'table', 'flex'):
|
|||
|
|
if inside:
|
|||
|
|
return
|
|||
|
|
inside = value
|
|||
|
|
elif value == 'list-item':
|
|||
|
|
if list_item:
|
|||
|
|
return
|
|||
|
|
list_item = value
|
|||
|
|
else:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
outside = outside or 'block'
|
|||
|
|
inside = inside or 'flow'
|
|||
|
|
if list_item:
|
|||
|
|
if inside in ('flow', 'flow-root'):
|
|||
|
|
return (outside, inside, list_item)
|
|||
|
|
else:
|
|||
|
|
return (outside, inside)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property('float')
|
|||
|
|
@single_keyword
|
|||
|
|
def float_(keyword): # XXX do not hide the "float" builtin
|
|||
|
|
"""``float`` property validation."""
|
|||
|
|
return keyword in ('left', 'right', 'footnote', 'none')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@comma_separated_list
|
|||
|
|
def font_family(tokens):
|
|||
|
|
"""``font-family`` property validation."""
|
|||
|
|
if len(tokens) == 1 and tokens[0].type == 'string':
|
|||
|
|
return tokens[0].value
|
|||
|
|
elif tokens and all(token.type == 'ident' for token in tokens):
|
|||
|
|
return ' '.join(token.value for token in tokens)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def font_kerning(keyword):
|
|||
|
|
return keyword in ('auto', 'normal', 'none')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_token
|
|||
|
|
def font_language_override(token):
|
|||
|
|
keyword = get_keyword(token)
|
|||
|
|
if keyword == 'normal':
|
|||
|
|
return keyword
|
|||
|
|
elif token.type == 'string':
|
|||
|
|
return token.value
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
def font_variant_ligatures(tokens):
|
|||
|
|
if len(tokens) == 1:
|
|||
|
|
keyword = get_keyword(tokens[0])
|
|||
|
|
if keyword in ('normal', 'none'):
|
|||
|
|
return keyword
|
|||
|
|
values = []
|
|||
|
|
couples = (
|
|||
|
|
('common-ligatures', 'no-common-ligatures'),
|
|||
|
|
('historical-ligatures', 'no-historical-ligatures'),
|
|||
|
|
('discretionary-ligatures', 'no-discretionary-ligatures'),
|
|||
|
|
('contextual', 'no-contextual'))
|
|||
|
|
all_values = []
|
|||
|
|
for couple in couples:
|
|||
|
|
all_values.extend(couple)
|
|||
|
|
for token in tokens:
|
|||
|
|
if token.type != 'ident':
|
|||
|
|
return None
|
|||
|
|
if token.value in all_values:
|
|||
|
|
concurrent_values = [
|
|||
|
|
couple for couple in couples if token.value in couple][0]
|
|||
|
|
if any(value in values for value in concurrent_values):
|
|||
|
|
return None
|
|||
|
|
else:
|
|||
|
|
values.append(token.value)
|
|||
|
|
else:
|
|||
|
|
return None
|
|||
|
|
if values:
|
|||
|
|
return tuple(values)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def font_variant_position(keyword):
|
|||
|
|
return keyword in ('normal', 'sub', 'super')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def font_variant_caps(keyword):
|
|||
|
|
return keyword in (
|
|||
|
|
'normal', 'small-caps', 'all-small-caps', 'petite-caps',
|
|||
|
|
'all-petite-caps', 'unicase', 'titling-caps')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
def font_variant_numeric(tokens):
|
|||
|
|
if len(tokens) == 1:
|
|||
|
|
keyword = get_keyword(tokens[0])
|
|||
|
|
if keyword == 'normal':
|
|||
|
|
return keyword
|
|||
|
|
values = []
|
|||
|
|
couples = (
|
|||
|
|
('lining-nums', 'oldstyle-nums'),
|
|||
|
|
('proportional-nums', 'tabular-nums'),
|
|||
|
|
('diagonal-fractions', 'stacked-fractions'),
|
|||
|
|
('ordinal',), ('slashed-zero',))
|
|||
|
|
all_values = []
|
|||
|
|
for couple in couples:
|
|||
|
|
all_values.extend(couple)
|
|||
|
|
for token in tokens:
|
|||
|
|
if token.type != 'ident':
|
|||
|
|
return None
|
|||
|
|
if token.value in all_values:
|
|||
|
|
concurrent_values = [
|
|||
|
|
couple for couple in couples if token.value in couple][0]
|
|||
|
|
if any(value in values for value in concurrent_values):
|
|||
|
|
return None
|
|||
|
|
else:
|
|||
|
|
values.append(token.value)
|
|||
|
|
else:
|
|||
|
|
return None
|
|||
|
|
if values:
|
|||
|
|
return tuple(values)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
def font_feature_settings(tokens):
|
|||
|
|
"""``font-feature-settings`` property validation."""
|
|||
|
|
if len(tokens) == 1 and get_keyword(tokens[0]) == 'normal':
|
|||
|
|
return 'normal'
|
|||
|
|
|
|||
|
|
@comma_separated_list
|
|||
|
|
def font_feature_settings_list(tokens):
|
|||
|
|
feature, value = None, None
|
|||
|
|
|
|||
|
|
if len(tokens) == 2:
|
|||
|
|
tokens, token = tokens[:-1], tokens[-1]
|
|||
|
|
if token.type == 'ident':
|
|||
|
|
value = {'on': 1, 'off': 0}.get(token.value)
|
|||
|
|
elif (token.type == 'number' and
|
|||
|
|
token.int_value is not None and token.int_value >= 0):
|
|||
|
|
value = token.int_value
|
|||
|
|
elif len(tokens) == 1:
|
|||
|
|
value = 1
|
|||
|
|
|
|||
|
|
if len(tokens) == 1:
|
|||
|
|
token, = tokens
|
|||
|
|
if token.type == 'string' and len(token.value) == 4:
|
|||
|
|
if all(0x20 <= ord(letter) <= 0x7f for letter in token.value):
|
|||
|
|
feature = token.value
|
|||
|
|
|
|||
|
|
if feature is not None and value is not None:
|
|||
|
|
return feature, value
|
|||
|
|
|
|||
|
|
return font_feature_settings_list(tokens)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def font_variant_alternates(keyword):
|
|||
|
|
# TODO: support other values
|
|||
|
|
# See https://www.w3.org/TR/css-fonts-3/#font-variant-caps-prop
|
|||
|
|
return keyword in ('normal', 'historical-forms')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
def font_variant_east_asian(tokens):
|
|||
|
|
if len(tokens) == 1:
|
|||
|
|
keyword = get_keyword(tokens[0])
|
|||
|
|
if keyword == 'normal':
|
|||
|
|
return keyword
|
|||
|
|
values = []
|
|||
|
|
couples = (
|
|||
|
|
('jis78', 'jis83', 'jis90', 'jis04', 'simplified', 'traditional'),
|
|||
|
|
('full-width', 'proportional-width'),
|
|||
|
|
('ruby',))
|
|||
|
|
all_values = []
|
|||
|
|
for couple in couples:
|
|||
|
|
all_values.extend(couple)
|
|||
|
|
for token in tokens:
|
|||
|
|
if token.type != 'ident':
|
|||
|
|
return None
|
|||
|
|
if token.value in all_values:
|
|||
|
|
concurrent_values = [
|
|||
|
|
couple for couple in couples if token.value in couple][0]
|
|||
|
|
if any(value in values for value in concurrent_values):
|
|||
|
|
return None
|
|||
|
|
else:
|
|||
|
|
values.append(token.value)
|
|||
|
|
else:
|
|||
|
|
return None
|
|||
|
|
if values:
|
|||
|
|
return tuple(values)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
def font_variation_settings(tokens):
|
|||
|
|
"""``font-variation-settings`` property validation."""
|
|||
|
|
if len(tokens) == 1 and get_keyword(tokens[0]) == 'normal':
|
|||
|
|
return 'normal'
|
|||
|
|
|
|||
|
|
@comma_separated_list
|
|||
|
|
def font_variation_settings_list(tokens):
|
|||
|
|
if len(tokens) == 2:
|
|||
|
|
key, value = tokens
|
|||
|
|
if key.type == 'string' and value.type == 'number':
|
|||
|
|
return key.value, value.value
|
|||
|
|
|
|||
|
|
return font_variation_settings_list(tokens)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_token
|
|||
|
|
def font_size(token):
|
|||
|
|
"""``font-size`` property validation."""
|
|||
|
|
length = get_length(token, negative=False, percentage=True)
|
|||
|
|
if length:
|
|||
|
|
return length
|
|||
|
|
font_size_keyword = get_keyword(token)
|
|||
|
|
if font_size_keyword in ('smaller', 'larger'):
|
|||
|
|
return font_size_keyword
|
|||
|
|
if font_size_keyword in computed_values.FONT_SIZE_KEYWORDS:
|
|||
|
|
return font_size_keyword
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def font_style(keyword):
|
|||
|
|
"""``font-style`` property validation."""
|
|||
|
|
return keyword in ('normal', 'italic', 'oblique')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def font_stretch(keyword):
|
|||
|
|
"""Validation for the ``font-stretch`` property."""
|
|||
|
|
return keyword in (
|
|||
|
|
'ultra-condensed', 'extra-condensed', 'condensed', 'semi-condensed',
|
|||
|
|
'normal',
|
|||
|
|
'semi-expanded', 'expanded', 'extra-expanded', 'ultra-expanded')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_token
|
|||
|
|
def font_weight(token):
|
|||
|
|
"""``font-weight`` property validation."""
|
|||
|
|
keyword = get_keyword(token)
|
|||
|
|
if keyword in ('normal', 'bold', 'bolder', 'lighter'):
|
|||
|
|
return keyword
|
|||
|
|
if token.type == 'number' and token.int_value is not None:
|
|||
|
|
if token.int_value in (100, 200, 300, 400, 500, 600, 700, 800, 900):
|
|||
|
|
return token.int_value
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def object_fit(keyword):
|
|||
|
|
# TODO: Figure out what the spec means by "'scale-down' flag".
|
|||
|
|
# As of this writing, neither Firefox nor chrome support
|
|||
|
|
# anything other than a single keyword as is done here.
|
|||
|
|
return keyword in ('fill', 'contain', 'cover', 'none', 'scale-down')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property(unstable=True)
|
|||
|
|
@single_token
|
|||
|
|
def image_resolution(token):
|
|||
|
|
# TODO: support 'snap' and 'from-image'
|
|||
|
|
return get_resolution(token)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property('letter-spacing')
|
|||
|
|
@property('word-spacing')
|
|||
|
|
@single_token
|
|||
|
|
def spacing(token):
|
|||
|
|
"""Validation for ``letter-spacing`` and ``word-spacing``."""
|
|||
|
|
if get_keyword(token) == 'normal':
|
|||
|
|
return 'normal'
|
|||
|
|
length = get_length(token)
|
|||
|
|
if length:
|
|||
|
|
return length
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_token
|
|||
|
|
def line_height(token):
|
|||
|
|
"""``line-height`` property validation."""
|
|||
|
|
if get_keyword(token) == 'normal':
|
|||
|
|
return 'normal'
|
|||
|
|
if token.type == 'number' and token.value >= 0:
|
|||
|
|
return Dimension(token.value, None)
|
|||
|
|
if token.type == 'percentage' and token.value >= 0:
|
|||
|
|
return Dimension(token.value, '%')
|
|||
|
|
elif token.type == 'dimension' and token.value >= 0:
|
|||
|
|
return get_length(token)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def list_style_position(keyword):
|
|||
|
|
"""``list-style-position`` property validation."""
|
|||
|
|
return keyword in ('inside', 'outside')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_token
|
|||
|
|
def list_style_type(token):
|
|||
|
|
"""``list-style-type`` property validation."""
|
|||
|
|
if token.type == 'ident':
|
|||
|
|
return token.value
|
|||
|
|
elif token.type == 'string':
|
|||
|
|
return ('string', token.value)
|
|||
|
|
elif token.type == 'function' and token.name == 'symbols':
|
|||
|
|
allowed_types = (
|
|||
|
|
'cyclic', 'numeric', 'alphabetic', 'symbolic', 'fixed')
|
|||
|
|
function_arguments = remove_whitespace(token.arguments)
|
|||
|
|
if len(function_arguments) >= 1:
|
|||
|
|
arguments = []
|
|||
|
|
if function_arguments[0].type == 'ident':
|
|||
|
|
if function_arguments[0].value in allowed_types:
|
|||
|
|
index = 1
|
|||
|
|
arguments.append(function_arguments[0].value)
|
|||
|
|
else:
|
|||
|
|
return
|
|||
|
|
else:
|
|||
|
|
arguments.append('symbolic')
|
|||
|
|
index = 0
|
|||
|
|
if len(function_arguments) < index + 1:
|
|||
|
|
return
|
|||
|
|
for i in range(index, len(function_arguments)):
|
|||
|
|
if function_arguments[i].type != 'string':
|
|||
|
|
return
|
|||
|
|
arguments.append(function_arguments[i].value)
|
|||
|
|
if arguments[0] in ('alphabetic', 'numeric'):
|
|||
|
|
if len(arguments) < 3:
|
|||
|
|
return
|
|||
|
|
return ('symbols()', tuple(arguments))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property('min-width')
|
|||
|
|
@property('min-height')
|
|||
|
|
@single_token
|
|||
|
|
def min_width_height(token):
|
|||
|
|
"""``min-width`` and ``min-height`` properties validation."""
|
|||
|
|
# See https://www.w3.org/TR/css-flexbox-1/#min-size-auto
|
|||
|
|
keyword = get_keyword(token)
|
|||
|
|
if keyword == 'auto':
|
|||
|
|
return keyword
|
|||
|
|
else:
|
|||
|
|
return length_or_precentage([token])
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property('padding-top')
|
|||
|
|
@property('padding-right')
|
|||
|
|
@property('padding-bottom')
|
|||
|
|
@property('padding-left')
|
|||
|
|
@single_token
|
|||
|
|
def length_or_precentage(token):
|
|||
|
|
"""``padding-*`` properties validation."""
|
|||
|
|
length = get_length(token, negative=False, percentage=True)
|
|||
|
|
if length:
|
|||
|
|
return length
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property('max-width')
|
|||
|
|
@property('max-height')
|
|||
|
|
@single_token
|
|||
|
|
def max_width_height(token):
|
|||
|
|
"""Validation for max-width and max-height"""
|
|||
|
|
length = get_length(token, negative=False, percentage=True)
|
|||
|
|
if length:
|
|||
|
|
return length
|
|||
|
|
if get_keyword(token) == 'none':
|
|||
|
|
return Dimension(inf, 'px')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_token
|
|||
|
|
def opacity(token):
|
|||
|
|
"""Validation for the ``opacity`` property."""
|
|||
|
|
if token.type == 'number':
|
|||
|
|
return min(1, max(0, token.value))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_token
|
|||
|
|
def z_index(token):
|
|||
|
|
"""Validation for the ``z-index`` property."""
|
|||
|
|
if get_keyword(token) == 'auto':
|
|||
|
|
return 'auto'
|
|||
|
|
if token.type == 'number' and token.int_value is not None:
|
|||
|
|
return token.int_value
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property('orphans')
|
|||
|
|
@property('widows')
|
|||
|
|
@single_token
|
|||
|
|
def orphans_widows(token):
|
|||
|
|
"""Validation for the ``orphans`` and ``widows`` properties."""
|
|||
|
|
if token.type == 'number' and token.int_value is not None:
|
|||
|
|
value = token.int_value
|
|||
|
|
if value >= 1:
|
|||
|
|
return value
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property(unstable=True)
|
|||
|
|
@single_token
|
|||
|
|
def column_count(token):
|
|||
|
|
"""Validation for the ``column-count`` property."""
|
|||
|
|
if token.type == 'number' and token.int_value is not None:
|
|||
|
|
value = token.int_value
|
|||
|
|
if value >= 1:
|
|||
|
|
return value
|
|||
|
|
if get_keyword(token) == 'auto':
|
|||
|
|
return 'auto'
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def overflow(keyword):
|
|||
|
|
"""Validation for the ``overflow`` property."""
|
|||
|
|
return keyword in ('auto', 'visible', 'hidden', 'scroll')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def text_overflow(keyword):
|
|||
|
|
"""Validation for the ``text-overflow`` property."""
|
|||
|
|
return keyword in ('clip', 'ellipsis')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_token
|
|||
|
|
def position(token):
|
|||
|
|
"""``position`` property validation."""
|
|||
|
|
if token.type == 'function' and token.name == 'running':
|
|||
|
|
if len(token.arguments) == 1 and token.arguments[0].type == 'ident':
|
|||
|
|
return ('running()', token.arguments[0].value)
|
|||
|
|
keyword = get_single_keyword([token])
|
|||
|
|
if keyword in ('static', 'relative', 'absolute', 'fixed'):
|
|||
|
|
return keyword
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
def quotes(tokens):
|
|||
|
|
"""``quotes`` property validation."""
|
|||
|
|
if (tokens and len(tokens) % 2 == 0 and
|
|||
|
|
all(token.type == 'string' for token in tokens)):
|
|||
|
|
strings = tuple(token.value for token in tokens)
|
|||
|
|
# Separate open and close quotes.
|
|||
|
|
# eg. ('«', '»', '“', '”') -> (('«', '“'), ('»', '”'))
|
|||
|
|
return strings[::2], strings[1::2]
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def table_layout(keyword):
|
|||
|
|
"""Validation for the ``table-layout`` property"""
|
|||
|
|
if keyword in ('fixed', 'auto'):
|
|||
|
|
return keyword
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def text_align_all(keyword):
|
|||
|
|
"""``text-align-all`` property validation."""
|
|||
|
|
return keyword in ('left', 'right', 'center', 'justify', 'start', 'end')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def text_align_last(keyword):
|
|||
|
|
"""``text-align-last`` property validation."""
|
|||
|
|
return keyword in (
|
|||
|
|
'auto', 'left', 'right', 'center', 'justify', 'start', 'end')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
def text_decoration_line(tokens):
|
|||
|
|
"""``text-decoration-line`` property validation."""
|
|||
|
|
keywords = {get_keyword(token) for token in tokens}
|
|||
|
|
if keywords == {'none'}:
|
|||
|
|
return 'none'
|
|||
|
|
allowed_values = {'underline', 'overline', 'line-through', 'blink'}
|
|||
|
|
if len(tokens) == len(keywords) and keywords.issubset(allowed_values):
|
|||
|
|
return keywords
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def text_decoration_style(keyword):
|
|||
|
|
"""``text-decoration-style`` property validation."""
|
|||
|
|
if keyword in ('solid', 'double', 'dotted', 'dashed', 'wavy'):
|
|||
|
|
return keyword
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_token
|
|||
|
|
def text_indent(token):
|
|||
|
|
"""``text-indent`` property validation."""
|
|||
|
|
length = get_length(token, percentage=True)
|
|||
|
|
if length:
|
|||
|
|
return length
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def text_transform(keyword):
|
|||
|
|
"""``text-align`` property validation."""
|
|||
|
|
return keyword in (
|
|||
|
|
'none', 'uppercase', 'lowercase', 'capitalize', 'full-width')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_token
|
|||
|
|
def vertical_align(token):
|
|||
|
|
"""Validation for the ``vertical-align`` property"""
|
|||
|
|
length = get_length(token, percentage=True)
|
|||
|
|
if length:
|
|||
|
|
return length
|
|||
|
|
keyword = get_keyword(token)
|
|||
|
|
if keyword in ('baseline', 'middle', 'sub', 'super',
|
|||
|
|
'text-top', 'text-bottom', 'top', 'bottom'):
|
|||
|
|
return keyword
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def visibility(keyword):
|
|||
|
|
"""``white-space`` property validation."""
|
|||
|
|
return keyword in ('visible', 'hidden', 'collapse')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def white_space(keyword):
|
|||
|
|
"""``white-space`` property validation."""
|
|||
|
|
return keyword in ('normal', 'pre', 'nowrap', 'pre-wrap', 'pre-line')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def overflow_wrap(keyword):
|
|||
|
|
"""``overflow-wrap`` property validation."""
|
|||
|
|
return keyword in ('anywhere', 'normal', 'break-word')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def word_break(keyword):
|
|||
|
|
"""``word-break`` property validation."""
|
|||
|
|
return keyword in ('normal', 'break-all')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_token
|
|||
|
|
def flex_basis(token):
|
|||
|
|
"""``flex-basis`` property validation."""
|
|||
|
|
basis = width_height([token])
|
|||
|
|
if basis is not None:
|
|||
|
|
return basis
|
|||
|
|
if get_keyword(token) == 'content':
|
|||
|
|
return 'content'
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def flex_direction(keyword):
|
|||
|
|
"""``flex-direction`` property validation."""
|
|||
|
|
return keyword in ('row', 'row-reverse', 'column', 'column-reverse')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property('flex-grow')
|
|||
|
|
@property('flex-shrink')
|
|||
|
|
@single_token
|
|||
|
|
def flex_grow_shrink(token):
|
|||
|
|
if token.type == 'number':
|
|||
|
|
return token.value
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_token
|
|||
|
|
def order(token):
|
|||
|
|
if token.type == 'number' and token.int_value is not None:
|
|||
|
|
return token.int_value
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def flex_wrap(keyword):
|
|||
|
|
"""``flex-wrap`` property validation."""
|
|||
|
|
return keyword in ('nowrap', 'wrap', 'wrap-reverse')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def justify_content(keyword):
|
|||
|
|
"""``justify-content`` property validation."""
|
|||
|
|
return keyword in (
|
|||
|
|
'flex-start', 'flex-end', 'center', 'space-between', 'space-around',
|
|||
|
|
'space-evenly', 'stretch')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def align_items(keyword):
|
|||
|
|
"""``align-items`` property validation."""
|
|||
|
|
return keyword in (
|
|||
|
|
'flex-start', 'flex-end', 'center', 'baseline', 'stretch')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def align_self(keyword):
|
|||
|
|
"""``align-self`` property validation."""
|
|||
|
|
return keyword in (
|
|||
|
|
'auto', 'flex-start', 'flex-end', 'center', 'baseline', 'stretch')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_keyword
|
|||
|
|
def align_content(keyword):
|
|||
|
|
"""``align-content`` property validation."""
|
|||
|
|
return keyword in (
|
|||
|
|
'flex-start', 'flex-end', 'center', 'space-between', 'space-around',
|
|||
|
|
'space-evenly', 'stretch')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property(unstable=True)
|
|||
|
|
@single_keyword
|
|||
|
|
def image_rendering(keyword):
|
|||
|
|
"""Validation for ``image-rendering``."""
|
|||
|
|
return keyword in ('auto', 'crisp-edges', 'pixelated')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property(unstable=True)
|
|||
|
|
def image_orientation(tokens):
|
|||
|
|
"""Validation for ``image-orientation``."""
|
|||
|
|
keyword = get_single_keyword(tokens)
|
|||
|
|
if keyword in ('none', 'from-image'):
|
|||
|
|
return keyword
|
|||
|
|
angle, flip = None, None
|
|||
|
|
for token in tokens:
|
|||
|
|
keyword = get_keyword(token)
|
|||
|
|
if keyword == 'flip':
|
|||
|
|
if flip is not None:
|
|||
|
|
return
|
|||
|
|
flip = True
|
|||
|
|
continue
|
|||
|
|
if angle is None:
|
|||
|
|
angle = get_angle(token)
|
|||
|
|
if angle is not None:
|
|||
|
|
continue
|
|||
|
|
return
|
|||
|
|
angle = 0 if angle is None else angle
|
|||
|
|
flip = False if flip is None else flip
|
|||
|
|
return (angle, flip)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property(unstable=True)
|
|||
|
|
def size(tokens):
|
|||
|
|
"""``size`` property validation.
|
|||
|
|
|
|||
|
|
See https://www.w3.org/TR/css-page-3/#page-size-prop
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
lengths = [get_length(token, negative=False) for token in tokens]
|
|||
|
|
if all(lengths):
|
|||
|
|
if len(lengths) == 1:
|
|||
|
|
return (lengths[0], lengths[0])
|
|||
|
|
elif len(lengths) == 2:
|
|||
|
|
return tuple(lengths)
|
|||
|
|
|
|||
|
|
keywords = [get_keyword(token) for token in tokens]
|
|||
|
|
if len(keywords) == 1:
|
|||
|
|
keyword = keywords[0]
|
|||
|
|
if keyword in computed_values.PAGE_SIZES:
|
|||
|
|
return computed_values.PAGE_SIZES[keyword]
|
|||
|
|
elif keyword in ('auto', 'portrait'):
|
|||
|
|
return computed_values.INITIAL_PAGE_SIZE
|
|||
|
|
elif keyword == 'landscape':
|
|||
|
|
return computed_values.INITIAL_PAGE_SIZE[::-1]
|
|||
|
|
|
|||
|
|
if len(keywords) == 2:
|
|||
|
|
if keywords[0] in ('portrait', 'landscape'):
|
|||
|
|
orientation, page_size = keywords
|
|||
|
|
elif keywords[1] in ('portrait', 'landscape'):
|
|||
|
|
page_size, orientation = keywords
|
|||
|
|
else:
|
|||
|
|
page_size = None
|
|||
|
|
if page_size in computed_values.PAGE_SIZES:
|
|||
|
|
width_height = computed_values.PAGE_SIZES[page_size]
|
|||
|
|
if orientation == 'portrait':
|
|||
|
|
return width_height
|
|||
|
|
else:
|
|||
|
|
height, width = width_height
|
|||
|
|
return width, height
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property(proprietary=True)
|
|||
|
|
@single_token
|
|||
|
|
def anchor(token):
|
|||
|
|
"""Validation for ``anchor``."""
|
|||
|
|
if get_keyword(token) == 'none':
|
|||
|
|
return 'none'
|
|||
|
|
function = parse_function(token)
|
|||
|
|
if function:
|
|||
|
|
name, args = function
|
|||
|
|
prototype = (name, [arg.type for arg in args])
|
|||
|
|
if prototype == ('attr', ['ident']):
|
|||
|
|
return ('attr()', args[0].value)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property(proprietary=True, wants_base_url=True)
|
|||
|
|
@single_token
|
|||
|
|
def link(token, base_url):
|
|||
|
|
"""Validation for ``link``."""
|
|||
|
|
if get_keyword(token) == 'none':
|
|||
|
|
return 'none'
|
|||
|
|
parsed_url = get_url(token, base_url)
|
|||
|
|
if parsed_url:
|
|||
|
|
return parsed_url
|
|||
|
|
function = parse_function(token)
|
|||
|
|
if function:
|
|||
|
|
name, args = function
|
|||
|
|
prototype = (name, [arg.type for arg in args])
|
|||
|
|
if prototype == ('attr', ['ident']):
|
|||
|
|
return ('attr()', args[0].value)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_token
|
|||
|
|
def tab_size(token):
|
|||
|
|
"""Validation for ``tab-size``.
|
|||
|
|
|
|||
|
|
See https://www.w3.org/TR/css-text-3/#tab-size
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
if token.type == 'number' and token.int_value is not None:
|
|||
|
|
value = token.int_value
|
|||
|
|
if value >= 0:
|
|||
|
|
return value
|
|||
|
|
return get_length(token, negative=False)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property(unstable=True)
|
|||
|
|
@single_token
|
|||
|
|
def hyphens(token):
|
|||
|
|
"""Validation for ``hyphens``."""
|
|||
|
|
keyword = get_keyword(token)
|
|||
|
|
if keyword in ('none', 'manual', 'auto'):
|
|||
|
|
return keyword
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property(unstable=True)
|
|||
|
|
@single_token
|
|||
|
|
def hyphenate_character(token):
|
|||
|
|
"""Validation for ``hyphenate-character``."""
|
|||
|
|
keyword = get_keyword(token)
|
|||
|
|
if keyword == 'auto':
|
|||
|
|
return '‐'
|
|||
|
|
elif token.type == 'string':
|
|||
|
|
return token.value
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property(unstable=True)
|
|||
|
|
@single_token
|
|||
|
|
def hyphenate_limit_zone(token):
|
|||
|
|
"""Validation for ``hyphenate-limit-zone``."""
|
|||
|
|
return get_length(token, negative=False, percentage=True)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property(unstable=True)
|
|||
|
|
def hyphenate_limit_chars(tokens):
|
|||
|
|
"""Validation for ``hyphenate-limit-chars``."""
|
|||
|
|
if len(tokens) == 1:
|
|||
|
|
token, = tokens
|
|||
|
|
keyword = get_keyword(token)
|
|||
|
|
if keyword == 'auto':
|
|||
|
|
return (5, 2, 2)
|
|||
|
|
elif token.type == 'number' and token.int_value is not None:
|
|||
|
|
return (token.int_value, 2, 2)
|
|||
|
|
elif len(tokens) == 2:
|
|||
|
|
total, left = tokens
|
|||
|
|
total_keyword = get_keyword(total)
|
|||
|
|
left_keyword = get_keyword(left)
|
|||
|
|
if total.type == 'number' and total.int_value is not None:
|
|||
|
|
if left.type == 'number' and left.int_value is not None:
|
|||
|
|
return (total.int_value, left.int_value, left.int_value)
|
|||
|
|
elif left_keyword == 'auto':
|
|||
|
|
return (total.value, 2, 2)
|
|||
|
|
elif total_keyword == 'auto':
|
|||
|
|
if left.type == 'number' and left.int_value is not None:
|
|||
|
|
return (5, left.int_value, left.int_value)
|
|||
|
|
elif left_keyword == 'auto':
|
|||
|
|
return (5, 2, 2)
|
|||
|
|
elif len(tokens) == 3:
|
|||
|
|
total, left, right = tokens
|
|||
|
|
if (
|
|||
|
|
(get_keyword(total) == 'auto' or
|
|||
|
|
(total.type == 'number' and total.int_value is not None)) and
|
|||
|
|
(get_keyword(left) == 'auto' or
|
|||
|
|
(left.type == 'number' and left.int_value is not None)) and
|
|||
|
|
(get_keyword(right) == 'auto' or
|
|||
|
|
(right.type == 'number' and right.int_value is not None))
|
|||
|
|
):
|
|||
|
|
total = total.int_value if total.type == 'number' else 5
|
|||
|
|
left = left.int_value if left.type == 'number' else 2
|
|||
|
|
right = right.int_value if right.type == 'number' else 2
|
|||
|
|
return (total, left, right)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property(proprietary=True)
|
|||
|
|
@single_token
|
|||
|
|
def lang(token):
|
|||
|
|
"""Validation for ``lang``."""
|
|||
|
|
if get_keyword(token) == 'none':
|
|||
|
|
return 'none'
|
|||
|
|
function = parse_function(token)
|
|||
|
|
if function:
|
|||
|
|
name, args = function
|
|||
|
|
prototype = (name, [arg.type for arg in args])
|
|||
|
|
if prototype == ('attr', ['ident']):
|
|||
|
|
return ('attr()', args[0].value)
|
|||
|
|
elif token.type == 'string':
|
|||
|
|
return ('string', token.value)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property(unstable=True, wants_base_url=True)
|
|||
|
|
def bookmark_label(tokens, base_url):
|
|||
|
|
"""Validation for ``bookmark-label``."""
|
|||
|
|
parsed_tokens = tuple(
|
|||
|
|
get_content_list_token(token, base_url) for token in tokens)
|
|||
|
|
if None not in parsed_tokens:
|
|||
|
|
return parsed_tokens
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property(unstable=True)
|
|||
|
|
@single_token
|
|||
|
|
def bookmark_level(token):
|
|||
|
|
"""Validation for ``bookmark-level``."""
|
|||
|
|
if token.type == 'number' and token.int_value is not None:
|
|||
|
|
value = token.int_value
|
|||
|
|
if value >= 1:
|
|||
|
|
return value
|
|||
|
|
elif get_keyword(token) == 'none':
|
|||
|
|
return 'none'
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property(unstable=True)
|
|||
|
|
@single_keyword
|
|||
|
|
def bookmark_state(keyword):
|
|||
|
|
"""Validation for ``bookmark-state``."""
|
|||
|
|
return keyword in ('open', 'closed')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property(unstable=True)
|
|||
|
|
@single_keyword
|
|||
|
|
def footnote_display(keyword):
|
|||
|
|
"""Validation for ``footnote-display``."""
|
|||
|
|
return keyword in ('block', 'inline', 'compact')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property(unstable=True)
|
|||
|
|
@single_keyword
|
|||
|
|
def footnote_policy(keyword):
|
|||
|
|
"""Validation for ``footnote-policy``."""
|
|||
|
|
return keyword in ('auto', 'line', 'block')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property(unstable=True, wants_base_url=True)
|
|||
|
|
@comma_separated_list
|
|||
|
|
def string_set(tokens, base_url):
|
|||
|
|
"""Validation for ``string-set``."""
|
|||
|
|
# Spec asks for strings after custom keywords, but we allow content-lists
|
|||
|
|
if len(tokens) >= 2:
|
|||
|
|
var_name = get_custom_ident(tokens[0])
|
|||
|
|
if var_name is None:
|
|||
|
|
return
|
|||
|
|
parsed_tokens = tuple(
|
|||
|
|
get_content_list_token(token, base_url) for token in tokens[1:])
|
|||
|
|
if None not in parsed_tokens:
|
|||
|
|
return (var_name, parsed_tokens)
|
|||
|
|
elif tokens and get_keyword(tokens[0]) == 'none':
|
|||
|
|
return 'none', ()
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
def transform(tokens):
|
|||
|
|
"""Validation for ``transform``."""
|
|||
|
|
if get_single_keyword(tokens) == 'none':
|
|||
|
|
return ()
|
|||
|
|
else:
|
|||
|
|
transforms = []
|
|||
|
|
for token in tokens:
|
|||
|
|
function = parse_function(token)
|
|||
|
|
if not function:
|
|||
|
|
return
|
|||
|
|
name, args = function
|
|||
|
|
|
|||
|
|
if len(args) == 1:
|
|||
|
|
angle = get_angle(args[0])
|
|||
|
|
length = get_length(args[0], percentage=True)
|
|||
|
|
if name == 'rotate' and angle is not None:
|
|||
|
|
transforms.append((name, angle))
|
|||
|
|
elif name in ('skewx', 'skew') and angle is not None:
|
|||
|
|
transforms.append(('skew', (angle, 0)))
|
|||
|
|
elif name == 'skewy' and angle is not None:
|
|||
|
|
transforms.append(('skew', (0, angle)))
|
|||
|
|
elif name in ('translatex', 'translate') and length:
|
|||
|
|
transforms.append((
|
|||
|
|
'translate', (length, computed_values.ZERO_PIXELS)))
|
|||
|
|
elif name == 'translatey' and length:
|
|||
|
|
transforms.append((
|
|||
|
|
'translate', (computed_values.ZERO_PIXELS, length)))
|
|||
|
|
elif name == 'scalex' and args[0].type == 'number':
|
|||
|
|
transforms.append(('scale', (args[0].value, 1)))
|
|||
|
|
elif name == 'scaley' and args[0].type == 'number':
|
|||
|
|
transforms.append(('scale', (1, args[0].value)))
|
|||
|
|
elif name == 'scale' and args[0].type == 'number':
|
|||
|
|
transforms.append(('scale', (args[0].value,) * 2))
|
|||
|
|
else:
|
|||
|
|
return
|
|||
|
|
elif len(args) == 2:
|
|||
|
|
if name == 'scale' and all(a.type == 'number' for a in args):
|
|||
|
|
transforms.append((name, tuple(arg.value for arg in args)))
|
|||
|
|
elif name == 'translate':
|
|||
|
|
lengths = tuple(
|
|||
|
|
get_length(token, percentage=True) for token in args)
|
|||
|
|
if all(lengths):
|
|||
|
|
transforms.append((name, lengths))
|
|||
|
|
else:
|
|||
|
|
return
|
|||
|
|
elif name == 'skew':
|
|||
|
|
angles = tuple(get_angle(token) for token in args)
|
|||
|
|
if all(angle is not None for angle in angles):
|
|||
|
|
transforms.append((name, angles))
|
|||
|
|
else:
|
|||
|
|
return
|
|||
|
|
else:
|
|||
|
|
return
|
|||
|
|
elif len(args) == 6 and name == 'matrix' and all(
|
|||
|
|
a.type == 'number' for a in args):
|
|||
|
|
transforms.append((name, tuple(arg.value for arg in args)))
|
|||
|
|
else:
|
|||
|
|
return
|
|||
|
|
return tuple(transforms)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@property()
|
|||
|
|
@single_token
|
|||
|
|
def appearance(token):
|
|||
|
|
"""``appearance`` property validation."""
|
|||
|
|
keyword = get_keyword(token)
|
|||
|
|
if keyword in ('none', 'auto'):
|
|||
|
|
return keyword
|