/
descriptors.py
150 lines (117 loc) · 4.56 KB
/
descriptors.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# coding: utf-8
"""
weasyprint.css.descriptors
--------------------------
Validate descriptors, currently used for @font-face rules.
See https://www.w3.org/TR/css-fonts-3/#font-resources.
:copyright: Copyright 2011-2016 Simon Sapin and contributors, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
from __future__ import division, unicode_literals
from tinycss.parsing import remove_whitespace
from ..compat import unquote
from ..logger import LOGGER
from .validation import (
InvalidValues, comma_separated_list, get_keyword, safe_urljoin,
single_keyword, single_token)
DESCRIPTORS = {}
def descriptor(descriptor_name=None, wants_base_url=False):
"""Decorator adding a function to the ``DESCRIPTORS``.
The name of the descriptor covered by the decorated function is set to
``descriptor_name`` if given, or is inferred from the function name
(replacing underscores by hyphens).
:param wants_base_url:
The function takes the stylesheet’s base URL as an additional
parameter.
"""
def decorator(function):
"""Add ``function`` to the ``DESCRIPTORS``."""
if descriptor_name is None:
name = function.__name__.replace('_', '-')
else:
name = descriptor_name
assert name not in DESCRIPTORS, name
function.wants_base_url = wants_base_url
DESCRIPTORS[name] = function
return function
return decorator
@descriptor()
def font_family(tokens, allow_spaces=False):
"""``font-family`` descriptor validation."""
allowed_types = ['IDENT']
if allow_spaces:
allowed_types.append('S')
if len(tokens) == 1 and tokens[0].type == 'STRING':
return tokens[0].value
if tokens and all(token.type in allowed_types for token in tokens):
return ' '.join(
token.value for token in tokens if token.type == 'IDENT')
@descriptor(wants_base_url=True)
@comma_separated_list
def src(tokens, base_url):
"""``src`` descriptor validation."""
if len(tokens) <= 2:
token = tokens.pop()
if token.type == 'FUNCTION' and token.function_name == 'format':
token = tokens.pop()
if token.type == 'FUNCTION' and token.function_name == 'local':
return 'local', font_family(token.content, allow_spaces=True)
if token.type == 'URI':
if token.value.startswith('#'):
return 'internal', unquote(token.value[1:])
else:
return 'external', safe_urljoin(base_url, token.value)
@descriptor()
@single_keyword
def font_style(keyword):
"""``font-style`` descriptor validation."""
return keyword in ('normal', 'italic', 'oblique')
@descriptor()
@single_token
def font_weight(token):
"""``font-weight`` descriptor validation."""
keyword = get_keyword(token)
if keyword in ('normal', 'bold'):
return keyword
if token.type == 'INTEGER':
if token.value in [100, 200, 300, 400, 500, 600, 700, 800, 900]:
return token.value
@descriptor()
@single_keyword
def font_stretch(keyword):
"""Validation for the ``font-stretch`` descriptor."""
return keyword in (
'ultra-condensed', 'extra-condensed', 'condensed', 'semi-condensed',
'normal',
'semi-expanded', 'expanded', 'extra-expanded', 'ultra-expanded')
def validate(base_url, name, tokens):
"""Default validator for descriptors."""
if name not in DESCRIPTORS:
raise InvalidValues('descriptor not supported')
function = DESCRIPTORS[name]
if function.wants_base_url:
value = function(tokens, base_url)
else:
value = function(tokens)
if value is None:
raise InvalidValues
return [(name, value)]
def preprocess_descriptors(base_url, descriptors):
"""Filter unsupported names and values for descriptors.
Log a warning for every ignored descriptor.
Return a iterable of ``(name, value)`` tuples.
"""
for descriptor in descriptors:
tokens = remove_whitespace(descriptor.value)
try:
# Use list() to consume generators now and catch any error.
result = list(validate(base_url, descriptor.name, tokens))
except InvalidValues as exc:
LOGGER.warning(
'Ignored `%s: %s` at %i:%i, %s.',
descriptor.name, descriptor.value.as_css(),
descriptor.line, descriptor.column,
exc.args[0] if exc.args and exc.args[0] else 'invalid value')
continue
for long_name, value in result:
yield long_name.replace('-', '_'), value