feat: add comprehensive GitHub workflow and development tools
This commit is contained in:
1172
app/.venv/Lib/site-packages/weasyprint/css/__init__.py
Normal file
1172
app/.venv/Lib/site-packages/weasyprint/css/__init__.py
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
824
app/.venv/Lib/site-packages/weasyprint/css/computed_values.py
Normal file
824
app/.venv/Lib/site-packages/weasyprint/css/computed_values.py
Normal file
@@ -0,0 +1,824 @@
|
||||
"""Convert specified property values into computed values."""
|
||||
|
||||
from math import pi
|
||||
from urllib.parse import unquote
|
||||
|
||||
from tinycss2.color3 import parse_color
|
||||
|
||||
from ..logger import LOGGER
|
||||
from ..text.ffi import ffi, pango, units_to_double
|
||||
from ..text.line_break import Layout, first_line_metrics
|
||||
from ..urls import get_link_attribute
|
||||
from .properties import INITIAL_VALUES, ZERO_PIXELS, Dimension
|
||||
from .utils import ANGLE_TO_RADIANS, LENGTH_UNITS, LENGTHS_TO_PIXELS, safe_urljoin
|
||||
|
||||
# Value in pixels of font-size for <absolute-size> keywords: 12pt (16px) for
|
||||
# medium, and scaling factors given in CSS3 for others:
|
||||
# https://www.w3.org/TR/css-fonts-3/#font-size-prop
|
||||
FONT_SIZE_KEYWORDS = {
|
||||
# medium is 16px, others are a ratio of medium
|
||||
name: INITIAL_VALUES['font_size'] * factor
|
||||
for name, factor in (
|
||||
('xx-small', 3 / 5),
|
||||
('x-small', 3 / 4),
|
||||
('small', 8 / 9),
|
||||
('medium', 1),
|
||||
('large', 6 / 5),
|
||||
('x-large', 3 / 2),
|
||||
('xx-large', 2),
|
||||
)
|
||||
}
|
||||
|
||||
# These are unspecified, other than 'thin' <= 'medium' <= 'thick'.
|
||||
# Values are in pixels.
|
||||
BORDER_WIDTH_KEYWORDS = {
|
||||
'thin': 1,
|
||||
'medium': 3,
|
||||
'thick': 5,
|
||||
}
|
||||
assert INITIAL_VALUES['border_top_width'] == BORDER_WIDTH_KEYWORDS['medium']
|
||||
|
||||
# https://www.w3.org/TR/CSS21/fonts.html#propdef-font-weight
|
||||
FONT_WEIGHT_RELATIVE = {
|
||||
'bolder': {
|
||||
100: 400,
|
||||
200: 400,
|
||||
300: 400,
|
||||
400: 700,
|
||||
500: 700,
|
||||
600: 900,
|
||||
700: 900,
|
||||
800: 900,
|
||||
900: 900,
|
||||
},
|
||||
'lighter': {
|
||||
100: 100,
|
||||
200: 100,
|
||||
300: 100,
|
||||
400: 100,
|
||||
500: 100,
|
||||
600: 400,
|
||||
700: 400,
|
||||
800: 700,
|
||||
900: 700,
|
||||
},
|
||||
}
|
||||
|
||||
# https://www.w3.org/TR/css-page-3/#size
|
||||
PAGE_SIZES = {
|
||||
page_size: (Dimension(width, unit), Dimension(height, unit))
|
||||
for page_size, width, height, unit in (
|
||||
('a10', 26, 37, 'mm'),
|
||||
('a9', 37, 52, 'mm'),
|
||||
('a8', 52, 74, 'mm'),
|
||||
('a7', 74, 105, 'mm'),
|
||||
('a6', 105, 148, 'mm'),
|
||||
('a5', 148, 210, 'mm'),
|
||||
('a4', 210, 297, 'mm'),
|
||||
('a3', 297, 420, 'mm'),
|
||||
('a2', 420, 594, 'mm'),
|
||||
('a1', 594, 841, 'mm'),
|
||||
('a0', 841, 1189, 'mm'),
|
||||
('b10', 31, 44, 'mm'),
|
||||
('b9', 44, 62, 'mm'),
|
||||
('b8', 62, 88, 'mm'),
|
||||
('b7', 88, 125, 'mm'),
|
||||
('b6', 125, 176, 'mm'),
|
||||
('b5', 176, 250, 'mm'),
|
||||
('b4', 250, 353, 'mm'),
|
||||
('b3', 353, 500, 'mm'),
|
||||
('b2', 500, 707, 'mm'),
|
||||
('b1', 707, 1000, 'mm'),
|
||||
('b0', 1000, 1414, 'mm'),
|
||||
('c10', 28, 40, 'mm'),
|
||||
('c9', 40, 57, 'mm'),
|
||||
('c8', 57, 81, 'mm'),
|
||||
('c7', 81, 114, 'mm'),
|
||||
('c6', 114, 162, 'mm'),
|
||||
('c5', 162, 229, 'mm'),
|
||||
('c4', 229, 324, 'mm'),
|
||||
('c3', 324, 458, 'mm'),
|
||||
('c2', 458, 648, 'mm'),
|
||||
('c1', 648, 917, 'mm'),
|
||||
('c0', 917, 1297, 'mm'),
|
||||
('jis-b10', 32, 45, 'mm'),
|
||||
('jis-b9', 45, 64, 'mm'),
|
||||
('jis-b8', 64, 91, 'mm'),
|
||||
('jis-b7', 91, 128, 'mm'),
|
||||
('jis-b6', 128, 182, 'mm'),
|
||||
('jis-b5', 182, 257, 'mm'),
|
||||
('jis-b4', 257, 364, 'mm'),
|
||||
('jis-b3', 364, 515, 'mm'),
|
||||
('jis-b2', 515, 728, 'mm'),
|
||||
('jis-b1', 728, 1030, 'mm'),
|
||||
('jis-b0', 1030, 1456, 'mm'),
|
||||
('letter', 8.5, 11, 'in'),
|
||||
('legal', 8.5, 14, 'in'),
|
||||
('ledger', 11, 17, 'in'),
|
||||
)
|
||||
}
|
||||
# In "portrait" orientation.
|
||||
assert all(width.value < height.value for width, height in PAGE_SIZES.values())
|
||||
|
||||
INITIAL_PAGE_SIZE = PAGE_SIZES['a4']
|
||||
INITIAL_VALUES['size'] = tuple(
|
||||
size.value * LENGTHS_TO_PIXELS[size.unit] for size in INITIAL_PAGE_SIZE)
|
||||
|
||||
|
||||
# Maps property names to functions returning the computed values
|
||||
COMPUTER_FUNCTIONS = {}
|
||||
|
||||
|
||||
def _font_style_cache_key(style, include_size=False):
|
||||
key = str((
|
||||
style['font_family'],
|
||||
style['font_style'],
|
||||
style['font_stretch'],
|
||||
style['font_weight'],
|
||||
style['font_variant_ligatures'],
|
||||
style['font_variant_position'],
|
||||
style['font_variant_caps'],
|
||||
style['font_variant_numeric'],
|
||||
style['font_variant_alternates'],
|
||||
style['font_variant_east_asian'],
|
||||
style['font_feature_settings'],
|
||||
style['font_variation_settings'],
|
||||
style['font_language_override'],
|
||||
style['lang'],
|
||||
))
|
||||
if include_size:
|
||||
key += str(style['font_size']) + str(style['line_height'])
|
||||
return key
|
||||
|
||||
|
||||
def register_computer(name):
|
||||
"""Decorator registering a property ``name`` for a function."""
|
||||
name = name.replace('-', '_')
|
||||
|
||||
def decorator(function):
|
||||
"""Register the property ``name`` for ``function``."""
|
||||
COMPUTER_FUNCTIONS[name] = function
|
||||
return function
|
||||
return decorator
|
||||
|
||||
|
||||
def compute_attr(style, values):
|
||||
# TODO: use real token parsing instead of casting with Python types
|
||||
func_name, value = values
|
||||
assert func_name == 'attr()'
|
||||
attr_name, type_or_unit, fallback = value
|
||||
try:
|
||||
attr_value = style.element.get(attr_name, fallback)
|
||||
if type_or_unit == 'string':
|
||||
pass # Keep the string
|
||||
elif type_or_unit == 'url':
|
||||
if attr_value.startswith('#'):
|
||||
attr_value = ('internal', unquote(attr_value[1:]))
|
||||
else:
|
||||
attr_value = (
|
||||
'external', safe_urljoin(style.base_url, attr_value))
|
||||
elif type_or_unit == 'color':
|
||||
attr_value = parse_color(attr_value.strip())
|
||||
elif type_or_unit == 'integer':
|
||||
attr_value = int(attr_value.strip())
|
||||
elif type_or_unit == 'number':
|
||||
attr_value = float(attr_value.strip())
|
||||
elif type_or_unit == '%':
|
||||
attr_value = Dimension(float(attr_value.strip()), '%')
|
||||
type_or_unit = 'length'
|
||||
elif type_or_unit in LENGTH_UNITS:
|
||||
attr_value = Dimension(float(attr_value.strip()), type_or_unit)
|
||||
type_or_unit = 'length'
|
||||
elif type_or_unit in ANGLE_TO_RADIANS:
|
||||
attr_value = Dimension(float(attr_value.strip()), type_or_unit)
|
||||
type_or_unit = 'angle'
|
||||
except Exception:
|
||||
return
|
||||
return (type_or_unit, attr_value)
|
||||
|
||||
|
||||
@register_computer('background-image')
|
||||
def background_image(style, name, values):
|
||||
"""Compute lenghts in gradient background-image."""
|
||||
for type_, value in values:
|
||||
if type_ in ('linear-gradient', 'radial-gradient'):
|
||||
value.stop_positions = tuple(
|
||||
length(style, name, pos) if pos is not None else None
|
||||
for pos in value.stop_positions)
|
||||
if type_ == 'radial-gradient':
|
||||
value.center, = compute_position(
|
||||
style, name, (value.center,))
|
||||
if value.size_type == 'explicit':
|
||||
value.size = length_or_percentage_tuple(
|
||||
style, name, value.size)
|
||||
return values
|
||||
|
||||
|
||||
@register_computer('background-position')
|
||||
@register_computer('object-position')
|
||||
def compute_position(style, name, values):
|
||||
"""Compute lengths in background-position."""
|
||||
return tuple(
|
||||
(origin_x, length(style, name, pos_x),
|
||||
origin_y, length(style, name, pos_y))
|
||||
for origin_x, pos_x, origin_y, pos_y in values)
|
||||
|
||||
|
||||
@register_computer('transform-origin')
|
||||
def length_or_percentage_tuple(style, name, values):
|
||||
"""Compute the lists of lengths that can be percentages."""
|
||||
return tuple(length(style, name, value) for value in values)
|
||||
|
||||
|
||||
@register_computer('border-spacing')
|
||||
@register_computer('size')
|
||||
@register_computer('clip')
|
||||
def length_tuple(style, name, values):
|
||||
"""Compute the properties with a list of lengths."""
|
||||
return tuple(
|
||||
length(style, name, value, pixels_only=True) for value in values)
|
||||
|
||||
|
||||
@register_computer('break-after')
|
||||
@register_computer('break-before')
|
||||
def break_before_after(style, name, value):
|
||||
"""Compute the ``break-before`` and ``break-after`` properties."""
|
||||
return 'page' if value == 'always' else value
|
||||
|
||||
|
||||
@register_computer('top')
|
||||
@register_computer('right')
|
||||
@register_computer('left')
|
||||
@register_computer('bottom')
|
||||
@register_computer('margin-top')
|
||||
@register_computer('margin-right')
|
||||
@register_computer('margin-bottom')
|
||||
@register_computer('margin-left')
|
||||
@register_computer('height')
|
||||
@register_computer('width')
|
||||
@register_computer('min-width')
|
||||
@register_computer('min-height')
|
||||
@register_computer('max-width')
|
||||
@register_computer('max-height')
|
||||
@register_computer('padding-top')
|
||||
@register_computer('padding-right')
|
||||
@register_computer('padding-bottom')
|
||||
@register_computer('padding-left')
|
||||
@register_computer('text-indent')
|
||||
@register_computer('hyphenate-limit-zone')
|
||||
@register_computer('flex-basis')
|
||||
def length(style, name, value, font_size=None, pixels_only=False):
|
||||
"""Compute a length ``value``."""
|
||||
if value in ('auto', 'content'):
|
||||
return value
|
||||
if value.value == 0:
|
||||
return 0 if pixels_only else ZERO_PIXELS
|
||||
|
||||
unit = value.unit
|
||||
if unit == 'px':
|
||||
return value.value if pixels_only else value
|
||||
elif unit in LENGTHS_TO_PIXELS:
|
||||
# Convert absolute lengths to pixels
|
||||
result = value.value * LENGTHS_TO_PIXELS[unit]
|
||||
elif unit in ('em', 'ex', 'ch', 'rem'):
|
||||
if font_size is None:
|
||||
font_size = style['font_size']
|
||||
if unit == 'ex':
|
||||
# TODO: use context to use @font-face fonts
|
||||
ratio = character_ratio(style, 'x')
|
||||
result = value.value * font_size * ratio
|
||||
elif unit == 'ch':
|
||||
ratio = character_ratio(style, '0')
|
||||
result = value.value * font_size * ratio
|
||||
elif unit == 'em':
|
||||
result = value.value * font_size
|
||||
elif unit == 'rem':
|
||||
result = value.value * style.root_style['font_size']
|
||||
else:
|
||||
# A percentage or 'auto': no conversion needed.
|
||||
return value
|
||||
|
||||
return result if pixels_only else Dimension(result, 'px')
|
||||
|
||||
|
||||
@register_computer('bleed-left')
|
||||
@register_computer('bleed-right')
|
||||
@register_computer('bleed-top')
|
||||
@register_computer('bleed-bottom')
|
||||
def bleed(style, name, value):
|
||||
if value == 'auto':
|
||||
return Dimension(8 if 'crop' in style['marks'] else 0, 'px')
|
||||
else:
|
||||
return length(style, name, value)
|
||||
|
||||
|
||||
@register_computer('letter-spacing')
|
||||
def pixel_length(style, name, value):
|
||||
if value == 'normal':
|
||||
return value
|
||||
else:
|
||||
return length(style, name, value, pixels_only=True)
|
||||
|
||||
|
||||
@register_computer('background-size')
|
||||
def background_size(style, name, values):
|
||||
"""Compute the ``background-size`` properties."""
|
||||
return tuple(
|
||||
value if value in ('contain', 'cover') else
|
||||
length_or_percentage_tuple(style, name, value)
|
||||
for value in values)
|
||||
|
||||
|
||||
@register_computer('image-orientation')
|
||||
def image_orientation(style, name, values):
|
||||
"""Compute the ``image-orientation`` properties."""
|
||||
if values in ('none', 'from-image'):
|
||||
return values
|
||||
angle, flip = values
|
||||
return (int(round(angle / pi * 2)) % 4 * 90, flip)
|
||||
|
||||
|
||||
@register_computer('border-top-width')
|
||||
@register_computer('border-right-width')
|
||||
@register_computer('border-left-width')
|
||||
@register_computer('border-bottom-width')
|
||||
@register_computer('column-rule-width')
|
||||
@register_computer('outline-width')
|
||||
def border_width(style, name, value):
|
||||
"""Compute the ``border-*-width`` properties."""
|
||||
border_style = style[name.replace('width', 'style')]
|
||||
if border_style in ('none', 'hidden'):
|
||||
return 0
|
||||
|
||||
if value in BORDER_WIDTH_KEYWORDS:
|
||||
return BORDER_WIDTH_KEYWORDS[value]
|
||||
|
||||
if isinstance(value, int):
|
||||
# The initial value can get here, but length() would fail as
|
||||
# it does not have a 'unit' attribute.
|
||||
return value
|
||||
|
||||
return length(style, name, value, pixels_only=True)
|
||||
|
||||
|
||||
@register_computer('border-image-slice')
|
||||
def border_image_slice(style, name, values):
|
||||
"""Compute the ``border-image-slice`` property."""
|
||||
computed_values = []
|
||||
fill = None
|
||||
for value in values:
|
||||
if value == 'fill':
|
||||
fill = value
|
||||
else:
|
||||
number, unit = value
|
||||
if unit is None:
|
||||
computed_values.append(number)
|
||||
else:
|
||||
computed_values.append(Dimension(number, '%'))
|
||||
if len(computed_values) == 1:
|
||||
computed_values *= 4
|
||||
elif len(computed_values) == 2:
|
||||
computed_values *= 2
|
||||
elif len(computed_values) == 3:
|
||||
computed_values.append(computed_values[1])
|
||||
return (*computed_values, fill)
|
||||
|
||||
|
||||
@register_computer('border-image-width')
|
||||
def border_image_width(style, name, values):
|
||||
"""Compute the ``border-image-width`` property."""
|
||||
computed_values = []
|
||||
for value in values:
|
||||
if value == 'auto':
|
||||
computed_values.append(value)
|
||||
else:
|
||||
number, unit = value
|
||||
computed_values.append(number if unit is None else value)
|
||||
if len(computed_values) == 1:
|
||||
computed_values *= 4
|
||||
elif len(computed_values) == 2:
|
||||
computed_values *= 2
|
||||
elif len(computed_values) == 3:
|
||||
computed_values.append(computed_values[1])
|
||||
return tuple(computed_values)
|
||||
|
||||
|
||||
@register_computer('border-image-outset')
|
||||
def border_image_outset(style, name, values):
|
||||
"""Compute the ``border-image-outset`` property."""
|
||||
computed_values = [
|
||||
value if isinstance(value, (int, float)) else length(style, name, value)
|
||||
for value in values]
|
||||
if len(computed_values) == 1:
|
||||
computed_values *= 4
|
||||
elif len(computed_values) == 2:
|
||||
computed_values *= 2
|
||||
elif len(computed_values) == 3:
|
||||
computed_values.append(computed_values[1])
|
||||
return tuple(computed_values)
|
||||
|
||||
|
||||
@register_computer('border-image-repeat')
|
||||
def border_image_repeat(style, name, values):
|
||||
"""Compute the ``border-image-repeat`` property."""
|
||||
return (values * 2) if len(values) == 1 else values
|
||||
|
||||
|
||||
@register_computer('column-width')
|
||||
def column_width(style, name, value):
|
||||
"""Compute the ``column-width`` property."""
|
||||
return length(style, name, value, pixels_only=True)
|
||||
|
||||
|
||||
@register_computer('border-top-left-radius')
|
||||
@register_computer('border-top-right-radius')
|
||||
@register_computer('border-bottom-left-radius')
|
||||
@register_computer('border-bottom-right-radius')
|
||||
def border_radius(style, name, values):
|
||||
"""Compute the ``border-*-radius`` properties."""
|
||||
return tuple(length(style, name, value) for value in values)
|
||||
|
||||
|
||||
@register_computer('column-gap')
|
||||
@register_computer('row-gap')
|
||||
def gap(style, name, value):
|
||||
"""Compute the ``*-gap`` properties."""
|
||||
if value == 'normal':
|
||||
return value
|
||||
return length(style, name, value)
|
||||
|
||||
|
||||
def _content_list(style, values):
|
||||
computed_values = []
|
||||
for value in values:
|
||||
if value[0] in ('string', 'content', 'url', 'quote', 'leader()'):
|
||||
computed_value = value
|
||||
elif value[0] == 'attr()':
|
||||
assert value[1][1] == 'string'
|
||||
computed_value = compute_attr(style, value)
|
||||
elif value[0] in (
|
||||
'counter()', 'counters()', 'content()', 'element()',
|
||||
'string()'):
|
||||
# Other values need layout context, their computed value cannot be
|
||||
# better than their specified value yet.
|
||||
# See build.compute_content_list.
|
||||
computed_value = value
|
||||
elif value[0] in (
|
||||
'target-counter()', 'target-counters()', 'target-text()'):
|
||||
anchor_token = value[1][0]
|
||||
if anchor_token[0] == 'attr()':
|
||||
attr = compute_attr(style, anchor_token)
|
||||
if attr is None:
|
||||
computed_value = None
|
||||
else:
|
||||
computed_value = (value[0], ((attr,) + value[1][1:]))
|
||||
else:
|
||||
computed_value = value
|
||||
if computed_value is None:
|
||||
LOGGER.warning('Unable to compute %r value for content: %r' % (
|
||||
style.element, ', '.join(str(item) for item in value)))
|
||||
else:
|
||||
computed_values.append(computed_value)
|
||||
|
||||
return tuple(computed_values)
|
||||
|
||||
|
||||
@register_computer('bookmark-label')
|
||||
def bookmark_label(style, name, values):
|
||||
"""Compute the ``bookmark-label`` property."""
|
||||
return _content_list(style, values)
|
||||
|
||||
|
||||
@register_computer('string-set')
|
||||
def string_set(style, name, values):
|
||||
"""Compute the ``string-set`` property."""
|
||||
# Spec asks for strings after custom keywords, but we allow content-lists
|
||||
return tuple(
|
||||
(string_set[0], _content_list(style, string_set[1]))
|
||||
for string_set in values)
|
||||
|
||||
|
||||
@register_computer('content')
|
||||
def content(style, name, values):
|
||||
"""Compute the ``content`` property."""
|
||||
if len(values) == 1:
|
||||
value, = values
|
||||
if value == 'normal':
|
||||
return 'inhibit' if style.pseudo_type else 'contents'
|
||||
elif value == 'none':
|
||||
return 'inhibit'
|
||||
return _content_list(style, values)
|
||||
|
||||
|
||||
@register_computer('display')
|
||||
def display(style, name, value):
|
||||
"""Compute the ``display`` property."""
|
||||
# See https://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo.
|
||||
float_ = style.specified['float']
|
||||
position = style.specified['position']
|
||||
if position in ('absolute', 'fixed') or float_ != 'none' or (
|
||||
style.is_root_element):
|
||||
if value == ('inline-table',):
|
||||
return ('block', 'table')
|
||||
elif len(value) == 1 and value[0].startswith('table-'):
|
||||
return ('block', 'flow')
|
||||
elif value[0] == 'inline':
|
||||
if 'list-item' in value:
|
||||
return ('block', 'flow', 'list-item')
|
||||
else:
|
||||
return ('block', 'flow')
|
||||
return value
|
||||
|
||||
|
||||
@register_computer('float')
|
||||
def compute_float(style, name, value):
|
||||
"""Compute the ``float`` property."""
|
||||
# See https://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo.
|
||||
position = style.specified['position']
|
||||
if position in ('absolute', 'fixed') or position[0] == 'running()':
|
||||
return 'none'
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
@register_computer('font-size')
|
||||
def font_size(style, name, value):
|
||||
"""Compute the ``font-size`` property."""
|
||||
if value in FONT_SIZE_KEYWORDS:
|
||||
return FONT_SIZE_KEYWORDS[value]
|
||||
|
||||
keyword_values = list(FONT_SIZE_KEYWORDS.values())
|
||||
if style.parent_style is None:
|
||||
parent_font_size = INITIAL_VALUES['font_size']
|
||||
else:
|
||||
parent_font_size = style.parent_style['font_size']
|
||||
|
||||
if value == 'larger':
|
||||
for i, keyword_value in enumerate(keyword_values):
|
||||
if keyword_value > parent_font_size:
|
||||
return keyword_values[i]
|
||||
else:
|
||||
return parent_font_size * 1.2
|
||||
elif value == 'smaller':
|
||||
for i, keyword_value in enumerate(keyword_values[::-1]):
|
||||
if keyword_value < parent_font_size:
|
||||
return keyword_values[-i - 1]
|
||||
else:
|
||||
return parent_font_size * 0.8
|
||||
elif value.unit == '%':
|
||||
return value.value * parent_font_size / 100
|
||||
else:
|
||||
return length(
|
||||
style, name, value, pixels_only=True,
|
||||
font_size=parent_font_size)
|
||||
|
||||
|
||||
@register_computer('font-weight')
|
||||
def font_weight(style, name, value):
|
||||
"""Compute the ``font-weight`` property."""
|
||||
if value == 'normal':
|
||||
return 400
|
||||
elif value == 'bold':
|
||||
return 700
|
||||
elif value in ('bolder', 'lighter'):
|
||||
if style.parent_style is None:
|
||||
parent_value = INITIAL_VALUES['font_weight']
|
||||
else:
|
||||
parent_value = style.parent_style['font_weight']
|
||||
return FONT_WEIGHT_RELATIVE[value][parent_value]
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
def _compute_track_breadth(style, name, value):
|
||||
"""Compute track breadth."""
|
||||
if value in ('auto', 'min-content', 'max-content'):
|
||||
return value
|
||||
elif isinstance(value, Dimension):
|
||||
if value.unit == 'fr':
|
||||
return value
|
||||
else:
|
||||
return length(style, name, value)
|
||||
|
||||
|
||||
def _track_size(style, name, values):
|
||||
"""Compute track size."""
|
||||
return_values = []
|
||||
for i, value in enumerate(values):
|
||||
if i % 2 == 0:
|
||||
# line name
|
||||
return_values.append(value)
|
||||
else:
|
||||
# track section
|
||||
track_breadth = _compute_track_breadth(style, name, value)
|
||||
if track_breadth:
|
||||
return_values.append(track_breadth)
|
||||
elif value[0] == 'minmax()':
|
||||
return_values.append((
|
||||
'minmax()',
|
||||
_compute_track_breadth(style, name, value[1]),
|
||||
_compute_track_breadth(style, name, value[2])))
|
||||
elif value[0] == 'fit-content()':
|
||||
return_values.append((
|
||||
'fit-content()', length(style, name, value[1])))
|
||||
elif value[0] == 'repeat()':
|
||||
return_values.append((
|
||||
'repeat()', value[1], _track_size(style, name, value[2])))
|
||||
return tuple(return_values)
|
||||
|
||||
|
||||
@register_computer('grid-template-columns')
|
||||
@register_computer('grid-template-rows')
|
||||
def grid_template(style, name, values):
|
||||
"""Compute the ``grid-template-*`` properties."""
|
||||
if values == 'none' or values[0] == 'subgrid':
|
||||
return values
|
||||
else:
|
||||
return _track_size(style, name, values)
|
||||
|
||||
|
||||
@register_computer('grid-auto-columns')
|
||||
@register_computer('grid-auto-rows')
|
||||
def grid_auto(style, name, values):
|
||||
"""Compute the ``grid-auto-*`` properties."""
|
||||
return_values = []
|
||||
for value in values:
|
||||
track_breadth = _compute_track_breadth(style, name, value)
|
||||
if track_breadth:
|
||||
return_values.append(track_breadth)
|
||||
elif value[0] == 'minmax()':
|
||||
return_values.append((
|
||||
'minmax()', grid_auto(style, name, [value[1]])[0],
|
||||
grid_auto(style, name, [value[2]])[0]))
|
||||
elif value[0] == 'fit-content()':
|
||||
return_values.append((
|
||||
'fit-content()', grid_auto(style, name, [value[1]])[0]))
|
||||
return tuple(return_values)
|
||||
|
||||
|
||||
@register_computer('line-height')
|
||||
def line_height(style, name, value):
|
||||
"""Compute the ``line-height`` property."""
|
||||
if value == 'normal':
|
||||
return value
|
||||
elif not value.unit:
|
||||
return ('NUMBER', value.value)
|
||||
elif value.unit == '%':
|
||||
factor = value.value / 100
|
||||
font_size_value = style['font_size']
|
||||
pixels = factor * font_size_value
|
||||
else:
|
||||
pixels = length(style, name, value, pixels_only=True)
|
||||
return ('PIXELS', pixels)
|
||||
|
||||
|
||||
@register_computer('anchor')
|
||||
def anchor(style, name, values):
|
||||
"""Compute the ``anchor`` property."""
|
||||
if values != 'none':
|
||||
_, key = values
|
||||
anchor_name = style.element.get(key) or None
|
||||
return anchor_name
|
||||
|
||||
|
||||
@register_computer('link')
|
||||
def link(style, name, values):
|
||||
"""Compute the ``link`` property."""
|
||||
if values == 'none':
|
||||
return None
|
||||
else:
|
||||
type_, value = values
|
||||
if type_ == 'attr()':
|
||||
return get_link_attribute(style.element, value, style.base_url)
|
||||
else:
|
||||
return values
|
||||
|
||||
|
||||
@register_computer('lang')
|
||||
def lang(style, name, values):
|
||||
"""Compute the ``lang`` property."""
|
||||
if values == 'none':
|
||||
return None
|
||||
else:
|
||||
name, key = values
|
||||
if name == 'attr()':
|
||||
return style.element.get(key) or None
|
||||
elif name == 'string':
|
||||
return key
|
||||
|
||||
|
||||
@register_computer('tab-size')
|
||||
def tab_size(style, name, value):
|
||||
"""Compute the ``tab-size`` property."""
|
||||
return value if isinstance(value, int) else length(style, name, value)
|
||||
|
||||
|
||||
@register_computer('transform')
|
||||
def transform(style, name, value):
|
||||
"""Compute the ``transform`` property."""
|
||||
result = []
|
||||
for function, args in value:
|
||||
if function == 'translate':
|
||||
args = length_or_percentage_tuple(style, name, args)
|
||||
result.append((function, args))
|
||||
return tuple(result)
|
||||
|
||||
|
||||
@register_computer('vertical-align')
|
||||
def vertical_align(style, name, value):
|
||||
"""Compute the ``vertical-align`` property."""
|
||||
# Use +/- half an em for super and sub, same as Pango.
|
||||
# (See the SUPERSUB_RISE constant in pango-markup.c)
|
||||
if value in (
|
||||
'baseline', 'middle', 'text-top', 'text-bottom', 'top', 'bottom'):
|
||||
return value
|
||||
elif value == 'super':
|
||||
return style['font_size'] * 0.5
|
||||
elif value == 'sub':
|
||||
return style['font_size'] * -0.5
|
||||
elif value.unit == '%':
|
||||
height, _ = strut_layout(style)
|
||||
return height * value.value / 100
|
||||
else:
|
||||
return length(style, name, value, pixels_only=True)
|
||||
|
||||
|
||||
@register_computer('word-spacing')
|
||||
def word_spacing(style, name, value):
|
||||
"""Compute the ``word-spacing`` property."""
|
||||
if value == 'normal':
|
||||
return 0
|
||||
else:
|
||||
return length(style, name, value, pixels_only=True)
|
||||
|
||||
|
||||
def strut_layout(style, context=None):
|
||||
"""Return a tuple of the used value of ``line-height`` and the baseline.
|
||||
|
||||
The baseline is given from the top edge of line height.
|
||||
|
||||
"""
|
||||
if style['font_size'] == 0:
|
||||
return 0, 0
|
||||
|
||||
if context:
|
||||
key = _font_style_cache_key(style, include_size=True)
|
||||
if key in context.strut_layouts:
|
||||
return context.strut_layouts[key]
|
||||
|
||||
layout = Layout(context, style)
|
||||
layout.set_text(' ')
|
||||
line, _ = layout.get_first_line()
|
||||
_, _, _, _, text_height, baseline = first_line_metrics(
|
||||
line, '', layout, resume_at=None, space_collapse=False, style=style)
|
||||
if style['line_height'] == 'normal':
|
||||
result = text_height, baseline
|
||||
if context:
|
||||
context.strut_layouts[key] = result
|
||||
return result
|
||||
type_, line_height = style['line_height']
|
||||
if type_ == 'NUMBER':
|
||||
line_height *= style['font_size']
|
||||
result = line_height, baseline + (line_height - text_height) / 2
|
||||
if context:
|
||||
context.strut_layouts[key] = result
|
||||
return result
|
||||
|
||||
|
||||
def character_ratio(style, character):
|
||||
"""Return the ratio of 1ex/font_size or 1ch/font_size."""
|
||||
# TODO: use context to use @font-face fonts
|
||||
|
||||
assert character in ('x', '0')
|
||||
|
||||
cache = style.cache[f'ratio_{"ex" if character == "x" else "ch"}']
|
||||
cache_key = _font_style_cache_key(style)
|
||||
if cache_key in cache:
|
||||
return cache[cache_key]
|
||||
|
||||
# Avoid recursion for letter-spacing and word-spacing properties
|
||||
style = style.copy()
|
||||
style['letter_spacing'] = 'normal'
|
||||
style['word_spacing'] = 0
|
||||
# Random big value
|
||||
style['font_size'] = 1000
|
||||
|
||||
layout = Layout(context=None, style=style)
|
||||
layout.set_text(character)
|
||||
line, _ = layout.get_first_line()
|
||||
|
||||
ink_extents = ffi.new('PangoRectangle *')
|
||||
logical_extents = ffi.new('PangoRectangle *')
|
||||
pango.pango_layout_line_get_extents(line, ink_extents, logical_extents)
|
||||
if character == 'x':
|
||||
measure = -units_to_double(ink_extents.y)
|
||||
else:
|
||||
measure = units_to_double(logical_extents.width)
|
||||
ffi.release(ink_extents)
|
||||
ffi.release(logical_extents)
|
||||
|
||||
# Zero means some kind of failure, fallback is 0.5.
|
||||
# We round to try keeping exact values that were altered by Pango.
|
||||
ratio = round(measure / style['font_size'], 5) or 0.5
|
||||
cache[cache_key] = ratio
|
||||
return ratio
|
||||
296
app/.venv/Lib/site-packages/weasyprint/css/counters.py
Normal file
296
app/.venv/Lib/site-packages/weasyprint/css/counters.py
Normal file
@@ -0,0 +1,296 @@
|
||||
"""Implement counter styles.
|
||||
|
||||
These are defined in CSS Counter Styles Level 3:
|
||||
https://www.w3.org/TR/css-counter-styles-3/#counter-style-system
|
||||
|
||||
"""
|
||||
|
||||
from math import inf
|
||||
|
||||
from .utils import remove_whitespace
|
||||
|
||||
|
||||
def symbol(string_or_url):
|
||||
"""Create a string from a symbol."""
|
||||
# TODO: this function should handle images too, and return something else
|
||||
# than strings.
|
||||
type_, value = string_or_url
|
||||
if type_ == 'string':
|
||||
return value
|
||||
return ''
|
||||
|
||||
|
||||
def parse_counter_style_name(tokens, counter_style):
|
||||
tokens = remove_whitespace(tokens)
|
||||
if len(tokens) == 1:
|
||||
token, = tokens
|
||||
if token.type == 'ident':
|
||||
if token.lower_value in ('decimal', 'disc'):
|
||||
if token.lower_value not in counter_style:
|
||||
return token.value
|
||||
elif token.lower_value != 'none':
|
||||
return token.value
|
||||
|
||||
|
||||
class CounterStyle(dict):
|
||||
"""Counter styles dictionary.
|
||||
|
||||
Keep a list of counter styles defined by ``@counter-style`` rules, indexed
|
||||
by their names.
|
||||
|
||||
See https://www.w3.org/TR/css-counter-styles-3/.
|
||||
|
||||
"""
|
||||
def resolve_counter(self, counter_name, previous_types=None):
|
||||
if counter_name[0] in ('symbols()', 'string'):
|
||||
counter_type, arguments = counter_name
|
||||
if counter_type == 'string':
|
||||
system = (None, 'cyclic', None)
|
||||
symbols = (('string', arguments),)
|
||||
suffix = ('string', '')
|
||||
elif counter_type == 'symbols()':
|
||||
system = (
|
||||
None, arguments[0], 1 if arguments[0] == 'fixed' else None)
|
||||
symbols = tuple(
|
||||
('string', argument) for argument in arguments[1:])
|
||||
suffix = ('string', ' ')
|
||||
return {
|
||||
'system': system,
|
||||
'negative': (('string', '-'), ('string', '')),
|
||||
'prefix': ('string', ''),
|
||||
'suffix': suffix,
|
||||
'range': 'auto',
|
||||
'pad': (0, ''),
|
||||
'fallback': 'decimal',
|
||||
'symbols': symbols,
|
||||
'additive_symbols': (),
|
||||
}
|
||||
elif counter_name in self:
|
||||
# Avoid circular fallbacks
|
||||
if previous_types is None:
|
||||
previous_types = []
|
||||
elif counter_name in previous_types:
|
||||
return
|
||||
previous_types.append(counter_name)
|
||||
|
||||
counter = self[counter_name].copy()
|
||||
if counter['system']:
|
||||
extends, system, _ = counter['system']
|
||||
else:
|
||||
extends, system = None, 'symbolic'
|
||||
|
||||
# Handle extends
|
||||
while extends:
|
||||
if system in self:
|
||||
extended_counter = self[system]
|
||||
counter['system'] = extended_counter['system']
|
||||
previous_types.append(system)
|
||||
if counter['system']:
|
||||
extends, system, _ = counter['system']
|
||||
else:
|
||||
extends, system = None, 'symbolic'
|
||||
if extends and system in previous_types:
|
||||
extends, system = 'extends', 'decimal'
|
||||
continue
|
||||
for name, value in extended_counter.items():
|
||||
if counter[name] is None and value is not None:
|
||||
counter[name] = value
|
||||
else:
|
||||
return counter
|
||||
|
||||
return counter
|
||||
|
||||
def render_value(self, counter_value, counter_name=None, counter=None,
|
||||
previous_types=None):
|
||||
"""Generate the counter representation.
|
||||
|
||||
See https://www.w3.org/TR/css-counter-styles-3/#generate-a-counter
|
||||
|
||||
"""
|
||||
assert counter or counter_name
|
||||
counter = counter or self.resolve_counter(counter_name, previous_types)
|
||||
if counter is None:
|
||||
if 'decimal' in self:
|
||||
return self.render_value(counter_value, 'decimal')
|
||||
else:
|
||||
# Could happen if the UA stylesheet is not used
|
||||
return ''
|
||||
|
||||
if counter['system']:
|
||||
extends, system, fixed_number = counter['system']
|
||||
else:
|
||||
extends, system, fixed_number = None, 'symbolic', None
|
||||
|
||||
# Avoid circular fallbacks
|
||||
if previous_types is None:
|
||||
previous_types = []
|
||||
elif system in previous_types:
|
||||
return self.render_value(counter_value, 'decimal')
|
||||
previous_types.append(counter_name)
|
||||
|
||||
# Handle extends
|
||||
while extends:
|
||||
if system in self:
|
||||
extended_counter = self[system]
|
||||
counter['system'] = extended_counter['system']
|
||||
if counter['system']:
|
||||
extends, system, fixed_number = counter['system']
|
||||
else:
|
||||
extends, system, fixed_number = None, 'symbolic', None
|
||||
if system in previous_types:
|
||||
return self.render_value(counter_value, 'decimal')
|
||||
previous_types.append(system)
|
||||
for name, value in extended_counter.items():
|
||||
if counter[name] is None and value is not None:
|
||||
counter[name] = value
|
||||
else:
|
||||
return self.render_value(counter_value, 'decimal')
|
||||
|
||||
# Step 2
|
||||
if counter['range'] in ('auto', None):
|
||||
min_range, max_range = -inf, inf
|
||||
if system in ('alphabetic', 'symbolic'):
|
||||
min_range = 1
|
||||
elif system == 'additive':
|
||||
min_range = 0
|
||||
counter_ranges = ((min_range, max_range),)
|
||||
else:
|
||||
counter_ranges = counter['range']
|
||||
for min_range, max_range in counter_ranges:
|
||||
if min_range <= counter_value <= max_range:
|
||||
break
|
||||
else:
|
||||
return self.render_value(
|
||||
counter_value, counter['fallback'] or 'decimal',
|
||||
previous_types=previous_types)
|
||||
|
||||
# Step 3
|
||||
initial = None
|
||||
is_negative = counter_value < 0
|
||||
if is_negative:
|
||||
negative_prefix, negative_suffix = (
|
||||
symbol(character) for character
|
||||
in counter['negative'] or (('string', '-'), ('string', '')))
|
||||
use_negative = (
|
||||
system in
|
||||
('symbolic', 'alphabetic', 'numeric', 'additive'))
|
||||
if use_negative:
|
||||
counter_value = abs(counter_value)
|
||||
|
||||
# TODO: instead of using the decimal fallback when we have the wrong
|
||||
# number of symbols, we should discard the whole counter. The problem
|
||||
# only happens when extending from another style, it is easily refused
|
||||
# during validation otherwise.
|
||||
|
||||
if system == 'cyclic':
|
||||
length = len(counter['symbols'])
|
||||
if length < 1:
|
||||
return self.render_value(counter_value, 'decimal')
|
||||
index = (counter_value - 1) % length
|
||||
initial = symbol(counter['symbols'][index])
|
||||
|
||||
elif system == 'fixed':
|
||||
length = len(counter['symbols'])
|
||||
if length < 1:
|
||||
return self.render_value(counter_value, 'decimal')
|
||||
index = counter_value - fixed_number
|
||||
if 0 <= index < length:
|
||||
initial = symbol(counter['symbols'][index])
|
||||
else:
|
||||
return self.render_value(
|
||||
counter_value, counter['fallback'] or 'decimal',
|
||||
previous_types=previous_types)
|
||||
|
||||
elif system == 'symbolic':
|
||||
length = len(counter['symbols'])
|
||||
if length < 1:
|
||||
return self.render_value(counter_value, 'decimal')
|
||||
index = (counter_value - 1) % length
|
||||
repeat = (counter_value - 1) // length + 1
|
||||
initial = symbol(counter['symbols'][index]) * repeat
|
||||
|
||||
elif system == 'alphabetic':
|
||||
length = len(counter['symbols'])
|
||||
if length < 2:
|
||||
return self.render_value(counter_value, 'decimal')
|
||||
reversed_parts = []
|
||||
while counter_value != 0:
|
||||
counter_value -= 1
|
||||
reversed_parts.append(symbol(
|
||||
counter['symbols'][counter_value % length]))
|
||||
counter_value //= length
|
||||
initial = ''.join(reversed(reversed_parts))
|
||||
|
||||
elif system == 'numeric':
|
||||
if counter_value == 0:
|
||||
initial = symbol(counter['symbols'][0])
|
||||
else:
|
||||
reversed_parts = []
|
||||
length = len(counter['symbols'])
|
||||
if length < 2:
|
||||
return self.render_value(counter_value, 'decimal')
|
||||
counter_value = abs(counter_value)
|
||||
while counter_value != 0:
|
||||
reversed_parts.append(symbol(
|
||||
counter['symbols'][counter_value % length]))
|
||||
counter_value //= length
|
||||
initial = ''.join(reversed(reversed_parts))
|
||||
|
||||
elif system == 'additive':
|
||||
if counter_value == 0:
|
||||
for weight, symbol_string in counter['additive_symbols']:
|
||||
if weight == 0:
|
||||
initial = symbol(symbol_string)
|
||||
else:
|
||||
parts = []
|
||||
if len(counter['additive_symbols']) < 1:
|
||||
return self.render_value(counter_value, 'decimal')
|
||||
for weight, symbol_string in counter['additive_symbols']:
|
||||
repetitions = counter_value // weight
|
||||
parts.extend([symbol(symbol_string)] * repetitions)
|
||||
counter_value -= weight * repetitions
|
||||
if counter_value == 0:
|
||||
initial = ''.join(parts)
|
||||
break
|
||||
if initial is None:
|
||||
return self.render_value(
|
||||
counter_value, counter['fallback'] or 'decimal',
|
||||
previous_types=previous_types)
|
||||
|
||||
assert initial is not None
|
||||
|
||||
# Step 4
|
||||
pad = counter['pad'] or (0, '')
|
||||
pad_difference = pad[0] - len(initial)
|
||||
if is_negative and use_negative:
|
||||
pad_difference -= len(negative_prefix) + len(negative_suffix)
|
||||
if pad_difference > 0:
|
||||
initial = pad_difference * symbol(pad[1]) + initial
|
||||
|
||||
# Step 5
|
||||
if is_negative and use_negative:
|
||||
initial = negative_prefix + initial + negative_suffix
|
||||
|
||||
# Step 6
|
||||
return initial
|
||||
|
||||
def render_marker(self, counter_name, counter_value):
|
||||
"""Generate the content of a ::marker pseudo-element."""
|
||||
counter = self.resolve_counter(counter_name)
|
||||
if counter is None:
|
||||
if 'decimal' in self:
|
||||
return self.render_marker('decimal', counter_value)
|
||||
else:
|
||||
# Could happen if the UA stylesheet is not used
|
||||
return ''
|
||||
|
||||
prefix = symbol(counter['prefix'] or ('string', ''))
|
||||
suffix = symbol(counter['suffix'] or ('string', '. '))
|
||||
|
||||
value = self.render_value(counter_value, counter_name=counter_name)
|
||||
assert value is not None
|
||||
return prefix + value + suffix
|
||||
|
||||
def copy(self):
|
||||
# Values are dicts but they are never modified, no need to deepcopy
|
||||
return CounterStyle(super().copy())
|
||||
192
app/.venv/Lib/site-packages/weasyprint/css/html5_ph.css
Normal file
192
app/.venv/Lib/site-packages/weasyprint/css/html5_ph.css
Normal file
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
|
||||
Presentational hints stylsheet for HTML.
|
||||
|
||||
This stylesheet contains all the presentational hints rules that can be
|
||||
expressed as CSS.
|
||||
|
||||
See https://www.w3.org/TR/html5/rendering.html#rendering
|
||||
|
||||
TODO: Attribute values are not case-insensitive, but they should be. We can add
|
||||
a "i" flag when CSS Selectors Level 4 is supported.
|
||||
|
||||
*/
|
||||
|
||||
pre[wrap] { white-space: pre-wrap; }
|
||||
|
||||
br[clear=left] { clear: left; }
|
||||
br[clear=right] { clear: right; }
|
||||
br[clear=all], br[clear=both] { clear: both; }
|
||||
|
||||
ol[type="1"], li[type="1"] { list-style-type: decimal; }
|
||||
ol[type=a], li[type=a] { list-style-type: lower-alpha; }
|
||||
ol[type=A], li[type=A] { list-style-type: upper-alpha; }
|
||||
ol[type=i], li[type=i] { list-style-type: lower-roman; }
|
||||
ol[type=I], li[type=I] { list-style-type: upper-roman; }
|
||||
ul[type=disc], li[type=disc] { list-style-type: disc; }
|
||||
ul[type=circle], li[type=circle] { list-style-type: circle; }
|
||||
ul[type=square], li[type=square] { list-style-type: square; }
|
||||
|
||||
table[align=left] { float: left; }
|
||||
table[align=right] { float: right; }
|
||||
table[align=center] { margin-left: auto; margin-right: auto; }
|
||||
thead[align=absmiddle], tbody[align=absmiddle], tfoot[align=absmiddle],
|
||||
tr[align=absmiddle], td[align=absmiddle], th[align=absmiddle] {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
caption[align=bottom] { caption-side: bottom; }
|
||||
p[align=left], h1[align=left], h2[align=left], h3[align=left],
|
||||
h4[align=left], h5[align=left], h6[align=left] {
|
||||
text-align: left;
|
||||
}
|
||||
p[align=right], h1[align=right], h2[align=right], h3[align=right],
|
||||
h4[align=right], h5[align=right], h6[align=right] {
|
||||
text-align: right;
|
||||
}
|
||||
p[align=center], h1[align=center], h2[align=center], h3[align=center],
|
||||
h4[align=center], h5[align=center], h6[align=center] {
|
||||
text-align: center;
|
||||
}
|
||||
p[align=justify], h1[align=justify], h2[align=justify], h3[align=justify],
|
||||
h4[align=justify], h5[align=justify], h6[align=justify] {
|
||||
text-align: justify;
|
||||
}
|
||||
thead[valign=top], tbody[valign=top], tfoot[valign=top],
|
||||
tr[valign=top], td[valign=top], th[valign=top] {
|
||||
vertical-align: top;
|
||||
}
|
||||
thead[valign=middle], tbody[valign=middle], tfoot[valign=middle],
|
||||
tr[valign=middle], td[valign=middle], th[valign=middle] {
|
||||
vertical-align: middle;
|
||||
}
|
||||
thead[valign=bottom], tbody[valign=bottom], tfoot[valign=bottom],
|
||||
tr[valign=bottom], td[valign=bottom], th[valign=bottom] {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
thead[valign=baseline], tbody[valign=baseline], tfoot[valign=baseline],
|
||||
tr[valign=baseline], td[valign=baseline], th[valign=baseline] {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
td[nowrap], th[nowrap] { white-space: nowrap; }
|
||||
|
||||
table[rules=none], table[rules=groups], table[rules=rows],
|
||||
table[rules=cols], table[rules=all] {
|
||||
border-style: hidden;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table[border]:not([border="0"]) { border-style: outset; }
|
||||
table[frame=void] { border-style: hidden; }
|
||||
table[frame=above] { border-style: outset hidden hidden hidden; }
|
||||
table[frame=below] { border-style: hidden hidden outset hidden; }
|
||||
table[frame=hsides] { border-style: outset hidden outset hidden; }
|
||||
table[frame=lhs] { border-style: hidden hidden hidden outset; }
|
||||
table[frame=rhs] { border-style: hidden outset hidden hidden; }
|
||||
table[frame=vsides] { border-style: hidden outset; }
|
||||
table[frame=box], table[frame=border] { border-style: outset; }
|
||||
|
||||
table[border]:not([border="0"]) > tr > td, table[border]:not([border="0"]) > tr > th,
|
||||
table[border]:not([border="0"]) > thead > tr > td, table[border]:not([border="0"]) > thead > tr > th,
|
||||
table[border]:not([border="0"]) > tbody > tr > td, table[border]:not([border="0"]) > tbody > tr > th,
|
||||
table[border]:not([border="0"]) > tfoot > tr > td, table[border]:not([border="0"]) > tfoot > tr > th {
|
||||
border-width: 1px;
|
||||
border-style: inset;
|
||||
}
|
||||
table[rules=none] > tr > td, table[rules=none] > tr > th,
|
||||
table[rules=none] > thead > tr > td, table[rules=none] > thead > tr > th,
|
||||
table[rules=none] > tbody > tr > td, table[rules=none] > tbody > tr > th,
|
||||
table[rules=none] > tfoot > tr > td, table[rules=none] > tfoot > tr > th,
|
||||
table[rules=groups] > tr > td, table[rules=groups] > tr > th,
|
||||
table[rules=groups] > thead > tr > td, table[rules=groups] > thead > tr > th,
|
||||
table[rules=groups] > tbody > tr > td, table[rules=groups] > tbody > tr > th,
|
||||
table[rules=groups] > tfoot > tr > td, table[rules=groups] > tfoot > tr > th,
|
||||
table[rules=rows] > tr > td, table[rules=rows] > tr > th,
|
||||
table[rules=rows] > thead > tr > td, table[rules=rows] > thead > tr > th,
|
||||
table[rules=rows] > tbody > tr > td, table[rules=rows] > tbody > tr > th,
|
||||
table[rules=rows] > tfoot > tr > td, table[rules=rows] > tfoot > tr > th {
|
||||
border-width: 1px;
|
||||
border-style: none;
|
||||
}
|
||||
table[rules=cols] > tr > td, table[rules=cols] > tr > th,
|
||||
table[rules=cols] > thead > tr > td, table[rules=cols] > thead > tr > th,
|
||||
table[rules=cols] > tbody > tr > td, table[rules=cols] > tbody > tr > th,
|
||||
table[rules=cols] > tfoot > tr > td, table[rules=cols] > tfoot > tr > th {
|
||||
border-width: 1px;
|
||||
border-style: none solid;
|
||||
}
|
||||
table[rules=all] > tr > td, table[rules=all] > tr > th,
|
||||
table[rules=all] > thead > tr > td, table[rules=all] > thead > tr > th,
|
||||
table[rules=all] > tbody > tr > td, table[rules=all] > tbody > tr > th,
|
||||
table[rules=all] > tfoot > tr > td, table[rules=all] > tfoot > tr > th {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
table[rules=groups] > colgroup {
|
||||
border-left-width: 1px;
|
||||
border-left-style: solid;
|
||||
border-right-width: 1px;
|
||||
border-right-style: solid;
|
||||
}
|
||||
table[rules=groups] > thead,
|
||||
table[rules=groups] > tbody,
|
||||
table[rules=groups] > tfoot {
|
||||
border-top-width: 1px;
|
||||
border-top-style: solid;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
|
||||
table[rules=rows] > tr, table[rules=rows] > thead > tr,
|
||||
table[rules=rows] > tbody > tr, table[rules=rows] > tfoot > tr {
|
||||
border-top-width: 1px;
|
||||
border-top-style: solid;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
|
||||
hr[align=left] { margin-left: 0; margin-right: auto; }
|
||||
hr[align=right] { margin-left: auto; margin-right: 0; }
|
||||
hr[align=center] { margin-left: auto; margin-right: auto; }
|
||||
hr[color], hr[noshade] { border-style: solid; }
|
||||
|
||||
iframe[frameborder="0"], iframe[frameborder=no] { border: none; }
|
||||
|
||||
applet[align=left], embed[align=left], iframe[align=left],
|
||||
img[align=left], input[type=image][align=left], object[align=left] {
|
||||
float: left;
|
||||
}
|
||||
|
||||
applet[align=right], embed[align=right], iframe[align=right],
|
||||
img[align=right], input[type=image][align=right], object[align=right] {
|
||||
float: right;
|
||||
}
|
||||
|
||||
applet[align=top], embed[align=top], iframe[align=top],
|
||||
img[align=top], input[type=image][align=top], object[align=top] {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
applet[align=baseline], embed[align=baseline], iframe[align=baseline],
|
||||
img[align=baseline], input[type=image][align=baseline], object[align=baseline] {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
applet[align=texttop], embed[align=texttop], iframe[align=texttop],
|
||||
img[align=texttop], input[type=image][align=texttop], object[align=texttop] {
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
applet[align=absmiddle], embed[align=absmiddle], iframe[align=absmiddle],
|
||||
img[align=absmiddle], input[type=image][align=absmiddle], object[align=absmiddle],
|
||||
applet[align=abscenter], embed[align=abscenter], iframe[align=abscenter],
|
||||
img[align=abscenter], input[type=image][align=abscenter], object[align=abscenter] {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
applet[align=bottom], embed[align=bottom], iframe[align=bottom],
|
||||
img[align=bottom], input[type=image][align=bottom],
|
||||
object[align=bottom] {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
761
app/.venv/Lib/site-packages/weasyprint/css/html5_ua.css
Normal file
761
app/.venv/Lib/site-packages/weasyprint/css/html5_ua.css
Normal file
@@ -0,0 +1,761 @@
|
||||
/*
|
||||
|
||||
User agent stylsheet for HTML.
|
||||
|
||||
Contributed by Peter Moulder.
|
||||
Based on suggested styles in the HTML5 specification, CSS 2.1, and
|
||||
what various web browsers use.
|
||||
|
||||
*/
|
||||
|
||||
/* https://www.w3.org/TR/html5/Overview#scroll-to-the-fragment-identifier */
|
||||
*[id] { -weasy-anchor: attr(id); }
|
||||
a[name] { -weasy-anchor: attr(name); }
|
||||
|
||||
*[dir] { /* unicode-bidi: embed; */ }
|
||||
*[hidden] { display: none; }
|
||||
*[dir=ltr] { direction: ltr; }
|
||||
*[dir=rtl] { direction: rtl; }
|
||||
/* :dir(ltr) { direction: ltr; } */
|
||||
/* :dir(rtl) { direction: rtl; } */
|
||||
*[lang] { -weasy-lang: attr(lang); }
|
||||
:link { color: #0000EE; text-decoration: underline; }
|
||||
a[href] { -weasy-link: attr(href); }
|
||||
:visited { color: #551A8B; text-decoration: underline; }
|
||||
a:link[rel~=help] { cursor: help; }
|
||||
a:visited[rel~=help] { cursor: help; }
|
||||
abbr[title] { text-decoration: dotted underline; }
|
||||
acronym[title] { text-decoration: dotted underline; }
|
||||
address { display: block; font-style: italic; /* unicode-bidi: isolate; */ }
|
||||
area { display: none; }
|
||||
area:link[rel~=help] { cursor: help; }
|
||||
area:visited[rel~=help] { cursor: help; }
|
||||
article { display: block; /* unicode-bidi: isolate; */ }
|
||||
aside { display: block; /* unicode-bidi: isolate; */ }
|
||||
b { font-weight: bold; }
|
||||
base { display: none; }
|
||||
basefont { display: none; }
|
||||
bdi { /* unicode-bidi: isolate; */ }
|
||||
bdi[dir] { /* unicode-bidi: isolate; */ }
|
||||
bdo { /* unicode-bidi: bidi-override; */ }
|
||||
bdo[dir] { /* unicode-bidi: bidi-override; */ }
|
||||
big { font-size: larger; }
|
||||
blink { text-decoration: blink; }
|
||||
blockquote { display: block; margin: 1em 40px; /* unicode-bidi: isolate; */ }
|
||||
body { display: block; margin: 8px; }
|
||||
br::before { content: '\A'; white-space: pre-line; }
|
||||
wbr::before { content: '\200B'; }
|
||||
caption { display: table-caption; /* unicode-bidi: isolate; */ }
|
||||
center { display: block; text-align: center; /* unicode-bidi: isolate; */ }
|
||||
cite { font-style: italic; }
|
||||
code { font-family: monospace; }
|
||||
col { display: table-column; /* unicode-bidi: isolate; */ }
|
||||
col[hidden] { display: table-column; /* unicode-bidi: isolate; */ visibility: collapse; }
|
||||
colgroup { display: table-column-group; /* unicode-bidi: isolate; */ }
|
||||
colgroup[hidden] { display: table-column-group; /* unicode-bidi: isolate; */ visibility: collapse; }
|
||||
command { display: none; }
|
||||
datalist { display: none; }
|
||||
|
||||
dd { display: block; margin-left: 40px; /* unicode-bidi: isolate; */ }
|
||||
|
||||
*[dir=ltr] dd { margin-left: 0; margin-right: 40px; }
|
||||
*[dir=rtl] dd { margin-left: 40px; margin-right: 0; }
|
||||
*[dir] *[dir=ltr] dd { margin-left: 0; margin-right: 40px; }
|
||||
*[dir] *[dir=rtl] dd { margin-left: 40px; margin-right: 0; }
|
||||
*[dir] *[dir] *[dir=ltr] dd { margin-left: 0; margin-right: 40px; }
|
||||
*[dir] *[dir] *[dir=rtl] dd { margin-left: 40px; margin-right: 0; }
|
||||
dd[dir=ltr][dir][dir] { margin-left: 0; margin-right: 40px; }
|
||||
dd[dir=rtl][dir][dir] { margin-left: 40px; margin-right: 0; }
|
||||
|
||||
details { display: block; /* unicode-bidi: isolate; */ }
|
||||
del { text-decoration: line-through; }
|
||||
dfn { font-style: italic; }
|
||||
|
||||
dir { display: block; list-style-type: disc; margin-bottom: 1em; margin-top: 1em; padding-left: 40px; /* unicode-bidi: isolate; */ }
|
||||
|
||||
*[dir=rtl] dir { padding-left: 0; padding-right: 40px; }
|
||||
*[dir=ltr] dir { padding-left: 40px; padding-right: 0; }
|
||||
*[dir] *[dir=rtl] dir { padding-left: 0; padding-right: 40px; }
|
||||
*[dir] *[dir=ltr] dir { padding-left: 40px; padding-right: 0; }
|
||||
*[dir] *[dir] *[dir=rtl] dir { padding-left: 0; padding-right: 40px; }
|
||||
*[dir] *[dir] *[dir=ltr] dir { padding-left: 40px; padding-right: 0; }
|
||||
dir[dir=rtl][dir][dir] { padding-left: 0; padding-right: 40px; }
|
||||
dir[dir=ltr][dir][dir] { padding-left: 40px; padding-right: 0; }
|
||||
|
||||
dir dir { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
dl dir { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
menu dir { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
ol dir { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
ul dir { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
|
||||
div { display: block; /* unicode-bidi: isolate; */ }
|
||||
|
||||
dl { display: block; margin-bottom: 1em; margin-top: 1em; /* unicode-bidi: isolate; */ }
|
||||
|
||||
dir dl { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
dl dl { margin-bottom: 0; margin-top: 0; }
|
||||
ol dl { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
ul dl { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
|
||||
dir dir dl { list-style-type: square; }
|
||||
dir menu dl { list-style-type: square; }
|
||||
dir ol dl { list-style-type: square; }
|
||||
dir ul dl { list-style-type: square; }
|
||||
menu dir dl { list-style-type: square; }
|
||||
menu dl { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
menu menu dl { list-style-type: square; }
|
||||
menu ol dl { list-style-type: square; }
|
||||
menu ul dl { list-style-type: square; }
|
||||
ol dir dl { list-style-type: square; }
|
||||
ol menu dl { list-style-type: square; }
|
||||
ol ol dl { list-style-type: square; }
|
||||
ol ul dl { list-style-type: square; }
|
||||
ul dir dl { list-style-type: square; }
|
||||
ul menu dl { list-style-type: square; }
|
||||
ul ol dl { list-style-type: square; }
|
||||
ul ul dl { list-style-type: square; }
|
||||
|
||||
ol, ul { counter-reset: list-item }
|
||||
|
||||
|
||||
dt { display: block; /* unicode-bidi: isolate; */ }
|
||||
em { font-style: italic; }
|
||||
fieldset { display: block; border-style: groove; border-width: 2px; margin-left: 2px; margin-right: 2px; padding: .35em .625em .75em .625em; }
|
||||
figcaption { display: block; /* unicode-bidi: isolate; */ }
|
||||
figure { display: block; margin: 1em 40px; /* unicode-bidi: isolate; */ }
|
||||
footer { display: block; /* unicode-bidi: isolate; */ }
|
||||
|
||||
form { display: block; /* unicode-bidi: isolate; */ }
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
border: 1px solid black;
|
||||
display: inline-block;
|
||||
font-size: 0.85em;
|
||||
height: 1.2em;
|
||||
padding: 0.2em;
|
||||
white-space: pre;
|
||||
width: 20em;
|
||||
}
|
||||
input[type="button"],
|
||||
input[type="reset"],
|
||||
input[type="submit"],
|
||||
button {
|
||||
background: lightgrey;
|
||||
border-radius: 0.25em;
|
||||
text-align: center;
|
||||
}
|
||||
input[type="button"][value],
|
||||
input[type="reset"][value],
|
||||
input[type="submit"][value],
|
||||
button[value] {
|
||||
max-width: 100%;
|
||||
width: auto;
|
||||
}
|
||||
input[type="submit"]:not([value])::before {
|
||||
content: "Submit";
|
||||
}
|
||||
input[type="reset"]:not([value])::before {
|
||||
content: "Reset";
|
||||
}
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
height: 1.2em;
|
||||
width: 1.2em;
|
||||
}
|
||||
input[type="checkbox"][checked]:before,
|
||||
input[type="radio"][checked]:before {
|
||||
background: black;
|
||||
content: "";
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
input[type="radio"][checked]:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
input[type="hidden"] {
|
||||
display: none;
|
||||
}
|
||||
input[type="radio"] {
|
||||
border-radius: 50%;
|
||||
margin: 0.2em 0.2em 0 0.4em;
|
||||
}
|
||||
input[value]::before {
|
||||
content: attr(value);
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
input::before,
|
||||
input[value=""]::before {
|
||||
content: " ";
|
||||
}
|
||||
select {
|
||||
background: lightgrey;
|
||||
border-radius: 0.25em 0.25em;
|
||||
position: relative;
|
||||
white-space: normal;
|
||||
}
|
||||
select[multiple] {
|
||||
height: 3.6em;
|
||||
}
|
||||
select:not([multiple])::before {
|
||||
content: "˅";
|
||||
position: absolute;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
width: 1.5em;
|
||||
}
|
||||
select option {
|
||||
padding-right: 1.5em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
select:not([multiple]) option {
|
||||
display: none;
|
||||
}
|
||||
select[multiple] option,
|
||||
select:not(:has(option[selected])) option:first-of-type,
|
||||
select option[selected]:not(option[selected] ~ option[selected]) {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
textarea {
|
||||
height: 3em;
|
||||
margin: 0.1em 0;
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
padding: 0.2em;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
textarea:empty {
|
||||
height: 3em;
|
||||
}
|
||||
|
||||
frame { display: block; }
|
||||
frameset { display: block; }
|
||||
|
||||
h1 { display: block; font-size: 2em; font-weight: bold; hyphens: manual; margin-bottom: .67em; margin-top: .67em; page-break-after: avoid; page-break-inside: avoid; /* unicode-bidi: isolate; */ bookmark-level: 1; bookmark-label: content(text); }
|
||||
section h1 { font-size: 1.50em; margin-bottom: .83em; margin-top: .83em; }
|
||||
section section h1 { font-size: 1.17em; margin-bottom: 1.00em; margin-top: 1.00em; }
|
||||
section section section h1 { font-size: 1.00em; margin-bottom: 1.33em; margin-top: 1.33em; }
|
||||
section section section section h1 { font-size: .83em; margin-bottom: 1.67em; margin-top: 1.67em; }
|
||||
section section section section section h1 { font-size: .67em; margin-bottom: 2.33em; margin-top: 2.33em; }
|
||||
h2 { display: block; font-size: 1.50em; font-weight: bold; hyphens: manual; margin-bottom: .83em; margin-top: .83em; page-break-after: avoid; page-break-inside: avoid; /* unicode-bidi: isolate; */ bookmark-level: 2; bookmark-label: content(text); }
|
||||
h3 { display: block; font-size: 1.17em; font-weight: bold; hyphens: manual; margin-bottom: 1.00em; margin-top: 1.00em; page-break-after: avoid; page-break-inside: avoid; /* unicode-bidi: isolate; */ bookmark-level: 3; bookmark-label: content(text); }
|
||||
h4 { display: block; font-size: 1.00em; font-weight: bold; hyphens: manual; margin-bottom: 1.33em; margin-top: 1.33em; page-break-after: avoid; page-break-inside: avoid; /* unicode-bidi: isolate; */ bookmark-level: 4; bookmark-label: content(text); }
|
||||
h5 { display: block; font-size: .83em; font-weight: bold; hyphens: manual; margin-bottom: 1.67em; margin-top: 1.67em; page-break-after: avoid; /* unicode-bidi: isolate; */ bookmark-level: 5; bookmark-label: content(text); }
|
||||
h6 { display: block; font-size: .67em; font-weight: bold; hyphens: manual; margin-bottom: 2.33em; margin-top: 2.33em; page-break-after: avoid; /* unicode-bidi: isolate; */ bookmark-level: 6; bookmark-label: content(text); }
|
||||
|
||||
head { display: none; }
|
||||
header { display: block; /* unicode-bidi: isolate; */ }
|
||||
hgroup { display: block; /* unicode-bidi: isolate; */ }
|
||||
|
||||
hr { border-style: inset; border-width: 1px; color: gray; display: block; margin-bottom: .5em; margin-left: auto; margin-right: auto; margin-top: .5em; /* unicode-bidi: isolate; */ }
|
||||
html { display: block; }
|
||||
i { font-style: italic; }
|
||||
*[dir=auto] { /* unicode-bidi: isolate; */ }
|
||||
bdo[dir=auto] { /* unicode-bidi: bidi-override isolate; */ }
|
||||
input[type=hidden] { display: none; }
|
||||
menu[type=context] { display: none; }
|
||||
pre[dir=auto] { /* unicode-bidi: plaintext; */ }
|
||||
table[frame=above] { border-color: black; }
|
||||
table[frame=below] { border-color: black; }
|
||||
table[frame=border] { border-color: black; }
|
||||
table[frame=box] { border-color: black; }
|
||||
table[frame=hsides] { border-color: black; }
|
||||
table[frame=lhs] { border-color: black; }
|
||||
table[frame=rhs] { border-color: black; }
|
||||
table[frame=void] { border-color: black; }
|
||||
table[frame=vsides] { border-color: black; }
|
||||
table[rules=all] { border-color: black; }
|
||||
table[rules=cols] { border-color: black; }
|
||||
table[rules=groups] { border-color: black; }
|
||||
table[rules=none] { border-color: black; }
|
||||
table[rules=rows] { border-color: black; }
|
||||
textarea[dir=auto] { /* unicode-bidi: plaintext; */ }
|
||||
iframe { border: 2px inset; }
|
||||
iframe[seamless] { border: none; }
|
||||
input { display: inline-block; text-indent: 0; }
|
||||
ins { text-decoration: underline; }
|
||||
kbd { font-family: monospace; }
|
||||
keygen { display: inline-block; text-indent: 0; }
|
||||
legend { display: block; /* unicode-bidi: isolate; */ }
|
||||
li { display: list-item; /* unicode-bidi: isolate; */ }
|
||||
link { display: none; }
|
||||
listing { display: block; font-family: monospace; margin-bottom: 1em; margin-top: 1em; /* unicode-bidi: isolate; */ white-space: pre; }
|
||||
mark { background: yellow; color: black; }
|
||||
main { display: block; /* unicode-bidi: isolate; */ }
|
||||
|
||||
menu { display: block; list-style-type: disc; margin-bottom: 1em; margin-top: 1em; padding-left: 40px; /* unicode-bidi: isolate; */ }
|
||||
|
||||
*[dir=rtl] menu { padding-left: 0; padding-right: 40px; }
|
||||
*[dir=ltr] menu { padding-left: 40px; padding-right: 0; }
|
||||
*[dir] *[dir=rtl] menu { padding-left: 0; padding-right: 40px; }
|
||||
*[dir] *[dir=ltr] menu { padding-left: 40px; padding-right: 0; }
|
||||
*[dir] *[dir] *[dir=rtl] menu { padding-left: 0; padding-right: 40px; }
|
||||
*[dir] *[dir] *[dir=ltr] menu { padding-left: 40px; padding-right: 0; }
|
||||
menu[dir=rtl][dir][dir] { padding-left: 0; padding-right: 40px; }
|
||||
menu[dir=ltr][dir][dir] { padding-left: 40px; padding-right: 0; }
|
||||
|
||||
dir menu { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
dl menu { margin-bottom: 0; margin-top: 0; }
|
||||
menu menu { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
|
||||
dir dir menu { list-style-type: square; }
|
||||
dir menu menu { list-style-type: square; }
|
||||
dir ol menu { list-style-type: square; }
|
||||
dir ul menu { list-style-type: square; }
|
||||
menu dir menu { list-style-type: square; }
|
||||
menu menu menu { list-style-type: square; }
|
||||
menu ol menu { list-style-type: square; }
|
||||
menu ul menu { list-style-type: square; }
|
||||
|
||||
ol menu { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
ol dir menu { list-style-type: square; }
|
||||
ol menu menu { list-style-type: square; }
|
||||
ol ol menu { list-style-type: square; }
|
||||
ol ul menu { list-style-type: square; }
|
||||
ul dir menu { list-style-type: square; }
|
||||
ul menu menu { list-style-type: square; }
|
||||
ul menu { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
ul ol menu { list-style-type: square; }
|
||||
ul ul menu { list-style-type: square; }
|
||||
meta { display: none; }
|
||||
nav { display: block; /* unicode-bidi: isolate; */ }
|
||||
nobr { white-space: nowrap; }
|
||||
noembed { display: none; }
|
||||
|
||||
/* The HTML5 spec suggests display:none for the old (now forbidden) noframes element,
|
||||
* but Morp doesn't currently handle frames, so we might as well render it.
|
||||
*/
|
||||
/*noframes { display: none; }*/
|
||||
noframes { display: block; }
|
||||
|
||||
ol { page-break-before: avoid; }
|
||||
ol { display: block; list-style-type: decimal; margin-bottom: 1em; margin-top: 1em; padding-left: 40px; /* unicode-bidi: isolate; */ }
|
||||
|
||||
*[dir=ltr] ol { padding-left: 0; padding-right: 40px; }
|
||||
*[dir=rtl] ol { padding-left: 40px; padding-right: 0; }
|
||||
*[dir] *[dir=ltr] ol { padding-left: 0; padding-right: 40px; }
|
||||
*[dir] *[dir=rtl] ol { padding-left: 40px; padding-right: 0; }
|
||||
*[dir] *[dir] *[dir=ltr] ol { padding-left: 0; padding-right: 40px; }
|
||||
*[dir] *[dir] *[dir=rtl] ol { padding-left: 40px; padding-right: 0; }
|
||||
ol[dir=ltr][dir][dir] { padding-left: 0; padding-right: 40px; }
|
||||
ol[dir=rtl][dir][dir] { padding-left: 40px; padding-right: 0; }
|
||||
|
||||
dir ol { margin-bottom: 0; margin-top: 0; }
|
||||
dl ol { margin-bottom: 0; margin-top: 0; }
|
||||
menu ol { margin-bottom: 0; margin-top: 0; }
|
||||
ol ol { margin-bottom: 0; margin-top: 0; }
|
||||
ul ol { margin-bottom: 0; margin-top: 0; }
|
||||
|
||||
optgroup { text-indent: 0; }
|
||||
output { /* unicode-bidi: isolate; */ }
|
||||
output[dir] { /* unicode-bidi: isolate; */ }
|
||||
p { display: block; margin-bottom: 1em; margin-top: 1em; /* unicode-bidi: isolate; */ }
|
||||
param { display: none; }
|
||||
plaintext { display: block; font-family: monospace; margin-bottom: 1em; margin-top: 1em; /* unicode-bidi: isolate; */ white-space: pre; }
|
||||
pre { display: block; font-family: monospace; margin-bottom: 1em; margin-top: 1em; /* unicode-bidi: isolate; */ white-space: pre; }
|
||||
q::after { content: close-quote; }
|
||||
q::before { content: open-quote; }
|
||||
rp { display: none; }
|
||||
/* rt { display: ruby-text; } */
|
||||
/* ruby { display: ruby; } */
|
||||
s { text-decoration: line-through; }
|
||||
samp { font-family: monospace; }
|
||||
script { display: none; }
|
||||
section { display: block; /* unicode-bidi: isolate; */ }
|
||||
small { font-size: smaller; }
|
||||
source { display: none; }
|
||||
strike { text-decoration: line-through; }
|
||||
strong { font-weight: bolder; }
|
||||
style { display: none; }
|
||||
sub { font-size: smaller; line-height: normal; vertical-align: sub; }
|
||||
summary { display: block; /* unicode-bidi: isolate; */ }
|
||||
sup { font-size: smaller; line-height: normal; vertical-align: super; }
|
||||
img, svg { overflow: hidden; }
|
||||
|
||||
table { border-collapse: separate; border-color: gray; border-spacing: 2px; display: table; text-indent: 0; /* unicode-bidi: isolate; */ }
|
||||
|
||||
/* The html5 spec doesn't mention the following, though the CSS 2.1 spec does
|
||||
* hint at its use, and a couple of UAs do have this. I haven't looked into
|
||||
* why the HTML5 spec doesn't include this rule.
|
||||
*/
|
||||
table { box-sizing: border-box; }
|
||||
|
||||
tbody { border-color: inherit; display: table-row-group; /* unicode-bidi: isolate; */ vertical-align: middle; }
|
||||
tbody[hidden] { display: table-row-group; /* unicode-bidi: isolate; */ visibility: collapse; }
|
||||
|
||||
td { border-color: gray; display: table-cell; padding: 1px; /* unicode-bidi: isolate; */ vertical-align: inherit; }
|
||||
td[hidden] { display: table-cell; /* unicode-bidi: isolate; */ visibility: collapse; }
|
||||
|
||||
textarea { display: inline-block; text-indent: 0; white-space: pre-wrap; }
|
||||
|
||||
tfoot { border-color: inherit; display: table-footer-group; /* unicode-bidi: isolate; */ vertical-align: middle; }
|
||||
tfoot[hidden] { display: table-footer-group; /* unicode-bidi: isolate; */ visibility: collapse; }
|
||||
|
||||
table[rules=none] > tr > td, table[rules=none] > tr > th, table[rules=groups] > tr > td, table[rules=groups] > tr > th, table[rules=rows] > tr > td, table[rules=rows] > tr > th, table[rules=cols] > tr > td, table[rules=cols] > tr > th, table[rules=all] > tr > td, table[rules=all] > tr > th, table[rules=none] > thead > tr > td, table[rules=none] > thead > tr > th, table[rules=groups] > thead > tr > td, table[rules=groups] > thead > tr > th, table[rules=rows] > thead > tr > td, table[rules=rows] > thead > tr > th, table[rules=cols] > thead > tr > td, table[rules=cols] > thead > tr > th, table[rules=all] > thead > tr > td, table[rules=all] > thead > tr > th, table[rules=none] > tbody > tr > td, table[rules=none] > tbody > tr > th, table[rules=groups] > tbody > tr > td, table[rules=groups] > tbody > tr > th, table[rules=rows] > tbody > tr > td, table[rules=rows] > tbody > tr > th, table[rules=cols] > tbody > tr > td, table[rules=cols] > tbody > tr > th, table[rules=all] > tbody > tr > td, table[rules=all] > tbody > tr > th, table[rules=none] > tfoot > tr > td, table[rules=none] > tfoot > tr > th, table[rules=groups] > tfoot > tr > td, table[rules=groups] > tfoot > tr > th, table[rules=rows] > tfoot > tr > td, table[rules=rows] > tfoot > tr > th, table[rules=cols] > tfoot > tr > td, table[rules=cols] > tfoot > tr > th, table[rules=all] > tfoot > tr > td, table[rules=all] > tfoot > tr > th { border-color: black; }
|
||||
th { border-color: gray; display: table-cell; font-weight: bold; padding: 1px; /* unicode-bidi: isolate; */ vertical-align: inherit; }
|
||||
|
||||
th[hidden] { display: table-cell; /* unicode-bidi: isolate; */ visibility: collapse; }
|
||||
thead { border-color: inherit; display: table-header-group; /* unicode-bidi: isolate; */ vertical-align: middle; }
|
||||
thead[hidden] { display: table-header-group; /* unicode-bidi: isolate; */ visibility: collapse; }
|
||||
table > tr { vertical-align: middle; }
|
||||
tr { border-color: inherit; display: table-row; /* unicode-bidi: isolate; */ vertical-align: inherit; }
|
||||
tr[hidden] { display: table-row; /* unicode-bidi: isolate; */ visibility: collapse; }
|
||||
|
||||
template { display: none; }
|
||||
title { display: none; }
|
||||
track { display: none; }
|
||||
tt { font-family: monospace; }
|
||||
u { text-decoration: underline; }
|
||||
|
||||
::marker { /* unicode-bidi: isolate; */ font-variant-numeric: tabular-nums; }
|
||||
ul { display: block; list-style-type: disc; margin-bottom: 1em; margin-top: 1em; padding-left: 40px; /* unicode-bidi: isolate; */ }
|
||||
|
||||
*[dir=ltr] ul { padding-left: 40px; padding-right: 0; }
|
||||
*[dir=rtl] ul { padding-left: 0; padding-right: 40px; }
|
||||
*[dir] *[dir=ltr] ul { padding-left: 40px; padding-right: 0; }
|
||||
*[dir] *[dir=rtl] ul { padding-left: 0; padding-right: 40px; }
|
||||
*[dir] *[dir] *[dir=ltr] ul { padding-left: 40px; padding-right: 0; }
|
||||
*[dir] *[dir] *[dir=rtl] ul { padding-left: 0; padding-right: 40px; }
|
||||
ul[dir=ltr][dir][dir] { padding-left: 40px; padding-right: 0; }
|
||||
ul[dir=rtl][dir][dir] { padding-left: 0; padding-right: 40px; }
|
||||
|
||||
/* This isn't in the HTML5 spec's suggested styling, and should probably be a
|
||||
* mere hint rather than a demand. It usually is the right thing, though.
|
||||
*/
|
||||
ul { display: block; page-break-before: avoid; }
|
||||
|
||||
dir ul { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
dl ul { margin-bottom: 0; margin-top: 0; }
|
||||
menu ul { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
ol ul { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
ul ul { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
|
||||
dir dir ul { list-style-type: square; }
|
||||
dir menu ul { list-style-type: square; }
|
||||
dir ol ul { list-style-type: square; }
|
||||
dir ul ul { list-style-type: square; }
|
||||
menu dir ul { list-style-type: square; }
|
||||
menu menu ul { list-style-type: square; }
|
||||
menu ol ul { list-style-type: square; }
|
||||
menu ul ul { list-style-type: square; }
|
||||
ol dir ul { list-style-type: square; }
|
||||
ol menu ul { list-style-type: square; }
|
||||
ol ol ul { list-style-type: square; }
|
||||
ol ul ul { list-style-type: square; }
|
||||
ul dir ul { list-style-type: square; }
|
||||
ul menu ul { list-style-type: square; }
|
||||
ul ol ul { list-style-type: square; }
|
||||
ul ul ul { list-style-type: square; }
|
||||
|
||||
var { font-style: italic; }
|
||||
video { object-fit: contain; }
|
||||
xmp { display: block; font-family: monospace; margin-bottom: 1em; margin-top: 1em; /* unicode-bidi: isolate; */ white-space: pre; }
|
||||
|
||||
::footnote-call { content: counter(footnote); vertical-align: super; font-size: smaller; line-height: inherit; }
|
||||
::footnote-marker { content: counter(footnote) '. '; }
|
||||
|
||||
@page {
|
||||
/* `size: auto` (the initial) is A4 portrait */
|
||||
margin: 75px;
|
||||
@footnote { margin-top: 1em }
|
||||
@top-left-corner { text-align: right; vertical-align: middle }
|
||||
@top-left { text-align: left; vertical-align: middle }
|
||||
@top-center { text-align: center; vertical-align: middle }
|
||||
@top-right { text-align: right; vertical-align: middle }
|
||||
@top-right-corner { text-align: left; vertical-align: middle }
|
||||
@left-top { text-align: center; vertical-align: top }
|
||||
@left-middle { text-align: center; vertical-align: middle }
|
||||
@left-bottom { text-align: center; vertical-align: bottom }
|
||||
@right-top { text-align: center; vertical-align: top }
|
||||
@right-middle { text-align: center; vertical-align: middle }
|
||||
@right-bottom { text-align: center; vertical-align: bottom }
|
||||
@bottom-left-corner { text-align: right; vertical-align: middle }
|
||||
@bottom-left { text-align: left; vertical-align: middle }
|
||||
@bottom-center { text-align: center; vertical-align: middle }
|
||||
@bottom-right { text-align: right; vertical-align: middle }
|
||||
@bottom-right-corner { text-align: left; vertical-align: middle }
|
||||
}
|
||||
|
||||
|
||||
/* Counters: https://www.w3.org/TR/css-counter-styles-3/#predefined-counters */
|
||||
|
||||
@counter-style disc {
|
||||
system: cyclic;
|
||||
symbols: •;
|
||||
suffix: " ";
|
||||
}
|
||||
|
||||
@counter-style circle {
|
||||
system: cyclic;
|
||||
symbols: ◦;
|
||||
suffix: " ";
|
||||
}
|
||||
|
||||
@counter-style square {
|
||||
system: cyclic;
|
||||
symbols: ▪;
|
||||
suffix: " ";
|
||||
}
|
||||
|
||||
@counter-style disclosure-open {
|
||||
system: cyclic;
|
||||
symbols: ▾;
|
||||
suffix: " ";
|
||||
}
|
||||
|
||||
@counter-style disclosure-closed {
|
||||
system: cyclic;
|
||||
/* TODO: handle rtl */
|
||||
symbols: ▸;
|
||||
suffix: " ";
|
||||
}
|
||||
|
||||
@counter-style decimal {
|
||||
system: numeric;
|
||||
symbols: '0' '1' '2' '3' '4' '5' '6' '7' '8' '9';
|
||||
}
|
||||
|
||||
|
||||
@counter-style decimal-leading-zero {
|
||||
system: extends decimal;
|
||||
pad: 2 '0';
|
||||
}
|
||||
|
||||
@counter-style arabic-indic {
|
||||
system: numeric;
|
||||
symbols: ٠ ١ ٢ ٣ ٤ ٥ ٦ ٧ ٨ ٩;
|
||||
}
|
||||
|
||||
@counter-style armenian {
|
||||
system: additive;
|
||||
range: 1 9999;
|
||||
additive-symbols: 9000 Ք, 8000 Փ, 7000 Ւ, 6000 Ց, 5000 Ր, 4000 Տ, 3000 Վ, 2000 Ս, 1000 Ռ, 900 Ջ, 800 Պ, 700 Չ, 600 Ո, 500 Շ, 400 Ն, 300 Յ, 200 Մ, 100 Ճ, 90 Ղ, 80 Ձ, 70 Հ, 60 Կ, 50 Ծ, 40 Խ, 30 Լ, 20 Ի, 10 Ժ, 9 Թ, 8 Ը, 7 Է, 6 Զ, 5 Ե, 4 Դ, 3 Գ, 2 Բ, 1 Ա;
|
||||
}
|
||||
|
||||
@counter-style upper-armenian {
|
||||
system: extends armenian;
|
||||
}
|
||||
|
||||
@counter-style lower-armenian {
|
||||
system: additive;
|
||||
range: 1 9999;
|
||||
additive-symbols: 9000 ք, 8000 փ, 7000 ւ, 6000 ց, 5000 ր, 4000 տ, 3000 վ, 2000 ս, 1000 ռ, 900 ջ, 800 պ, 700 չ, 600 ո, 500 շ, 400 ն, 300 յ, 200 մ, 100 ճ, 90 ղ, 80 ձ, 70 հ, 60 կ, 50 ծ, 40 խ, 30 լ, 20 ի, 10 ժ, 9 թ, 8 ը, 7 է, 6 զ, 5 ե, 4 դ, 3 գ, 2 բ, 1 ա;
|
||||
}
|
||||
|
||||
@counter-style bengali {
|
||||
system: numeric;
|
||||
symbols: ০ ১ ২ ৩ ৪ ৫ ৬ ৭ ৮ ৯;
|
||||
}
|
||||
|
||||
@counter-style cambodian {
|
||||
system: numeric;
|
||||
symbols: ០ ១ ២ ៣ ៤ ៥ ៦ ៧ ៨ ៩;
|
||||
}
|
||||
|
||||
@counter-style khmer {
|
||||
system: extends cambodian;
|
||||
}
|
||||
|
||||
@counter-style cjk-decimal {
|
||||
system: numeric;
|
||||
range: 0 infinite;
|
||||
symbols: 〇 一 二 三 四 五 六 七 八 九;
|
||||
suffix: "、";
|
||||
}
|
||||
|
||||
@counter-style devanagari {
|
||||
system: numeric;
|
||||
symbols: ० १ २ ३ ४ ५ ६ ७ ८ ९;
|
||||
}
|
||||
|
||||
@counter-style georgian {
|
||||
system: additive;
|
||||
range: 1 19999;
|
||||
additive-symbols: 10000 ჵ, 9000 ჰ, 8000 ჯ, 7000 ჴ, 6000 ხ, 5000 ჭ, 4000 წ, 3000 ძ, 2000 ც, 1000 ჩ, 900 შ, 800 ყ, 700 ღ, 600 ქ, 500 ფ, 400 ჳ, 300 ტ, 200 ს, 100 რ, 90 ჟ, 80 პ, 70 ო, 60 ჲ, 50 ნ, 40 მ, 30 ლ, 20 კ, 10 ი, 9 თ, 8 ჱ, 7 ზ, 6 ვ, 5 ე, 4 დ, 3 გ, 2 ბ, 1 ა;
|
||||
}
|
||||
|
||||
@counter-style gujarati {
|
||||
system: numeric;
|
||||
symbols: ૦ ૧ ૨ ૩ ૪ ૫ ૬ ૭ ૮ ૯;
|
||||
}
|
||||
|
||||
@counter-style gurmukhi {
|
||||
system: numeric;
|
||||
symbols: ੦ ੧ ੨ ੩ ੪ ੫ ੬ ੭ ੮ ੯;
|
||||
}
|
||||
|
||||
@counter-style hebrew {
|
||||
system: additive;
|
||||
range: 1 10999;
|
||||
additive-symbols: 10000 י׳, 9000 ט׳, 8000 ח׳, 7000 ז׳, 6000 ו׳, 5000 ה׳, 4000 ד׳, 3000 ג׳, 2000 ב׳, 1000 א׳, 400 ת, 300 ש, 200 ר, 100 ק, 90 צ, 80 פ, 70 ע, 60 ס, 50 נ, 40 מ, 30 ל, 20 כ, 19 יט, 18 יח, 17 יז, 16 טז, 15 טו, 10 י, 9 ט, 8 ח, 7 ז, 6 ו, 5 ה, 4 ד, 3 ג, 2 ב, 1 א;
|
||||
}
|
||||
|
||||
@counter-style kannada {
|
||||
system: numeric;
|
||||
symbols: ೦ ೧ ೨ ೩ ೪ ೫ ೬ ೭ ೮ ೯;
|
||||
}
|
||||
|
||||
@counter-style lao {
|
||||
system: numeric;
|
||||
symbols: ໐ ໑ ໒ ໓ ໔ ໕ ໖ ໗ ໘ ໙;
|
||||
}
|
||||
|
||||
@counter-style malayalam {
|
||||
system: numeric;
|
||||
symbols: ൦ ൧ ൨ ൩ ൪ ൫ ൬ ൭ ൮ ൯;
|
||||
}
|
||||
|
||||
@counter-style mongolian {
|
||||
system: numeric;
|
||||
symbols: ᠐ ᠑ ᠒ ᠓ ᠔ ᠕ ᠖ ᠗ ᠘ ᠙;
|
||||
}
|
||||
|
||||
@counter-style myanmar {
|
||||
system: numeric;
|
||||
symbols: ၀ ၁ ၂ ၃ ၄ ၅ ၆ ၇ ၈ ၉;
|
||||
}
|
||||
|
||||
@counter-style oriya {
|
||||
system: numeric;
|
||||
symbols: ୦ ୧ ୨ ୩ ୪ ୫ ୬ ୭ ୮ ୯;
|
||||
}
|
||||
|
||||
@counter-style persian {
|
||||
system: numeric;
|
||||
symbols: ۰ ۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹;
|
||||
}
|
||||
|
||||
@counter-style lower-roman {
|
||||
system: additive;
|
||||
range: 1 3999;
|
||||
additive-symbols: 1000 m, 900 cm, 500 d, 400 cd, 100 c, 90 xc, 50 l, 40 xl, 10 x, 9 ix, 5 v, 4 iv, 1 i;
|
||||
}
|
||||
|
||||
@counter-style upper-roman {
|
||||
system: additive;
|
||||
range: 1 3999;
|
||||
additive-symbols: 1000 M, 900 CM, 500 D, 400 CD, 100 C, 90 XC, 50 L, 40 XL, 10 X, 9 IX, 5 V, 4 IV, 1 I;
|
||||
}
|
||||
|
||||
@counter-style tamil {
|
||||
system: numeric;
|
||||
symbols: ௦ ௧ ௨ ௩ ௪ ௫ ௬ ௭ ௮ ௯;
|
||||
}
|
||||
|
||||
@counter-style telugu {
|
||||
system: numeric;
|
||||
symbols: ౦ ౧ ౨ ౩ ౪ ౫ ౬ ౭ ౮ ౯;
|
||||
}
|
||||
|
||||
@counter-style thai {
|
||||
system: numeric;
|
||||
symbols: ๐ ๑ ๒ ๓ ๔ ๕ ๖ ๗ ๘ ๙;
|
||||
}
|
||||
|
||||
@counter-style tibetan {
|
||||
system: numeric;
|
||||
symbols: ༠ ༡ ༢ ༣ ༤ ༥ ༦ ༧ ༨ ༩;
|
||||
}
|
||||
@counter-style lower-alpha {
|
||||
system: alphabetic;
|
||||
symbols: a b c d e f g h i j k l m n o p q r s t u v w x y z;
|
||||
}
|
||||
|
||||
@counter-style lower-latin {
|
||||
system: extends lower-alpha;
|
||||
}
|
||||
|
||||
@counter-style upper-alpha {
|
||||
system: alphabetic;
|
||||
symbols: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z;
|
||||
}
|
||||
|
||||
@counter-style upper-latin {
|
||||
system: extends upper-alpha;
|
||||
}
|
||||
|
||||
@counter-style cjk-earthly-branch {
|
||||
system: alphabetic;
|
||||
symbols: 子 丑 寅 卯 辰 巳 午 未 申 酉 戌 亥;
|
||||
suffix: "、";
|
||||
}
|
||||
|
||||
@counter-style cjk-heavenly-stem {
|
||||
system: alphabetic;
|
||||
symbols: 甲 乙 丙 丁 戊 己 庚 辛 壬 癸;
|
||||
suffix: "、";
|
||||
}
|
||||
|
||||
@counter-style lower-greek {
|
||||
system: alphabetic;
|
||||
symbols: α β γ δ ε ζ η θ ι κ λ μ ν ξ ο π ρ σ τ υ φ χ ψ ω;
|
||||
}
|
||||
|
||||
@counter-style hiragana {
|
||||
system: alphabetic;
|
||||
symbols: あ い う え お か き く け こ さ し す せ そ た ち つ て と な に ぬ ね の は ひ ふ へ ほ ま み む め も や ゆ よ ら り る れ ろ わ ゐ ゑ を ん;
|
||||
suffix: "、";
|
||||
}
|
||||
|
||||
@counter-style hiragana-iroha {
|
||||
system: alphabetic;
|
||||
symbols: い ろ は に ほ へ と ち り ぬ る を わ か よ た れ そ つ ね な ら む う ゐ の お く や ま け ふ こ え て あ さ き ゆ め み し ゑ ひ も せ す;
|
||||
suffix: "、";
|
||||
}
|
||||
|
||||
@counter-style katakana {
|
||||
system: alphabetic;
|
||||
symbols: ア イ ウ エ オ カ キ ク ケ コ サ シ ス セ ソ タ チ ツ テ ト ナ ニ ヌ ネ ノ ハ ヒ フ ヘ ホ マ ミ ム メ モ ヤ ユ ヨ ラ リ ル レ ロ ワ ヰ ヱ ヲ ン;
|
||||
suffix: "、";
|
||||
}
|
||||
|
||||
@counter-style katakana-iroha {
|
||||
system: alphabetic;
|
||||
symbols: イ ロ ハ ニ ホ ヘ ト チ リ ヌ ル ヲ ワ カ ヨ タ レ ソ ツ ネ ナ ラ ム ウ ヰ ノ オ ク ヤ マ ケ フ コ エ テ ア サ キ ユ メ ミ シ ヱ ヒ モ セ ス;
|
||||
suffix: "、";
|
||||
}
|
||||
|
||||
@counter-style japanese-informal {
|
||||
system: additive;
|
||||
range: -9999 9999;
|
||||
additive-symbols: 9000 九千, 8000 八千, 7000 七千, 6000 六千, 5000 五千, 4000 四千, 3000 三千, 2000 二千, 1000 千, 900 九百, 800 八百, 700 七百, 600 六百, 500 五百, 400 四百, 300 三百, 200 二百, 100 百, 90 九十, 80 八十, 70 七十, 60 六十, 50 五十, 40 四十, 30 三十, 20 二十, 10 十, 9 九, 8 八, 7 七, 6 六, 5 五, 4 四, 3 三, 2 二, 1 一, 0 〇;
|
||||
suffix: 、;
|
||||
negative: マイナス;
|
||||
fallback: cjk-decimal;
|
||||
}
|
||||
|
||||
@counter-style japanese-formal {
|
||||
system: additive;
|
||||
range: -9999 9999;
|
||||
additive-symbols: 9000 九阡, 8000 八阡, 7000 七阡, 6000 六阡, 5000 伍阡, 4000 四阡, 3000 参阡, 2000 弐阡, 1000 壱阡, 900 九百, 800 八百, 700 七百, 600 六百, 500 伍百, 400 四百, 300 参百, 200 弐百, 100 壱百, 90 九拾, 80 八拾, 70 七拾, 60 六拾, 50 伍拾, 40 四拾, 30 参拾, 20 弐拾, 10 壱拾, 9 九, 8 八, 7 七, 6 六, 5 伍, 4 四, 3 参, 2 弐, 1 壱, 0 零;
|
||||
suffix: 、;
|
||||
negative: マイナス;
|
||||
fallback: cjk-decimal;
|
||||
}
|
||||
|
||||
@counter-style korean-hangul-formal {
|
||||
system: additive;
|
||||
range: -9999 9999;
|
||||
additive-symbols: 9000 구천, 8000 팔천, 7000 칠천, 6000 육천, 5000 오천, 4000 사천, 3000 삼천, 2000 이천, 1000 일천, 900 구백, 800 팔백, 700 칠백, 600 육백, 500 오백, 400 사백, 300 삼백, 200 이백, 100 일백, 90 구십, 80 팔십, 70 칠십, 60 육십, 50 오십, 40 사십, 30 삼십, 20 이십, 10 일십, 9 구, 8 팔, 7 칠, 6 육, 5 오, 4 사, 3 삼, 2 이, 1 일, 0 영;
|
||||
suffix: ', ';
|
||||
negative: "마이너스 ";
|
||||
}
|
||||
|
||||
@counter-style korean-hanja-informal {
|
||||
system: additive;
|
||||
range: -9999 9999;
|
||||
additive-symbols: 9000 九千, 8000 八千, 7000 七千, 6000 六千, 5000 五千, 4000 四千, 3000 三千, 2000 二千, 1000 千, 900 九百, 800 八百, 700 七百, 600 六百, 500 五百, 400 四百, 300 三百, 200 二百, 100 百, 90 九十, 80 八十, 70 七十, 60 六十, 50 五十, 40 四十, 30 三十, 20 二十, 10 十, 9 九, 8 八, 7 七, 6 六, 5 五, 4 四, 3 三, 2 二, 1 一, 0 零;
|
||||
suffix: ', ';
|
||||
negative: "마이너스 ";
|
||||
}
|
||||
|
||||
@counter-style korean-hanja-formal {
|
||||
system: additive;
|
||||
range: -9999 9999;
|
||||
additive-symbols: 9000 九仟, 8000 八仟, 7000 七仟, 6000 六仟, 5000 五仟, 4000 四仟, 3000 參仟, 2000 貳仟, 1000 壹仟, 900 九百, 800 八百, 700 七百, 600 六百, 500 五百, 400 四百, 300 參百, 200 貳百, 100 壹百, 90 九拾, 80 八拾, 70 七拾, 60 六拾, 50 五拾, 40 四拾, 30 參拾, 20 貳拾, 10 壹拾, 9 九, 8 八, 7 七, 6 六, 5 五, 4 四, 3 參, 2 貳, 1 壹, 0 零;
|
||||
suffix: ', ';
|
||||
negative: "마이너스 ";
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/* Default stylesheet for PDF forms */
|
||||
|
||||
button, input, select, textarea {
|
||||
appearance: auto;
|
||||
}
|
||||
39
app/.venv/Lib/site-packages/weasyprint/css/media_queries.py
Normal file
39
app/.venv/Lib/site-packages/weasyprint/css/media_queries.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""Handle media queries.
|
||||
|
||||
https://www.w3.org/TR/mediaqueries-4/
|
||||
|
||||
"""
|
||||
|
||||
import tinycss2
|
||||
|
||||
from ..logger import LOGGER
|
||||
from .utils import remove_whitespace, split_on_comma
|
||||
|
||||
|
||||
def evaluate_media_query(query_list, device_media_type):
|
||||
"""Return the boolean evaluation of `query_list` for the given
|
||||
`device_media_type`.
|
||||
|
||||
:attr query_list: a cssutilts.stlysheets.MediaList
|
||||
:attr device_media_type: a media type string (for now)
|
||||
|
||||
"""
|
||||
# TODO: actual support for media queries, not just media types
|
||||
return 'all' in query_list or device_media_type in query_list
|
||||
|
||||
|
||||
def parse_media_query(tokens):
|
||||
tokens = remove_whitespace(tokens)
|
||||
if not tokens:
|
||||
return ['all']
|
||||
else:
|
||||
media = []
|
||||
for part in split_on_comma(tokens):
|
||||
types = [token.type for token in part]
|
||||
if types == ['ident']:
|
||||
media.append(part[0].lower_value)
|
||||
else:
|
||||
LOGGER.warning(
|
||||
'Expected a media type, got %r', tinycss2.serialize(part))
|
||||
return
|
||||
return media
|
||||
359
app/.venv/Lib/site-packages/weasyprint/css/properties.py
Normal file
359
app/.venv/Lib/site-packages/weasyprint/css/properties.py
Normal file
@@ -0,0 +1,359 @@
|
||||
"""Various data about known CSS properties."""
|
||||
|
||||
import collections
|
||||
from math import inf
|
||||
|
||||
from tinycss2.color3 import parse_color
|
||||
|
||||
Dimension = collections.namedtuple('Dimension', ['value', 'unit'])
|
||||
|
||||
ZERO_PIXELS = Dimension(0, 'px')
|
||||
|
||||
INITIAL_VALUES = {
|
||||
# CSS 2.1: https://www.w3.org/TR/CSS21/propidx.html
|
||||
'bottom': 'auto',
|
||||
'caption_side': 'top',
|
||||
'clear': 'none',
|
||||
'clip': (), # computed value for 'auto'
|
||||
'color': parse_color('black'), # chosen by the user agent
|
||||
'direction': 'ltr',
|
||||
'display': ('inline', 'flow'),
|
||||
'empty_cells': 'show',
|
||||
'float': 'none',
|
||||
'left': 'auto',
|
||||
'line_height': 'normal',
|
||||
'margin_top': ZERO_PIXELS,
|
||||
'margin_right': ZERO_PIXELS,
|
||||
'margin_bottom': ZERO_PIXELS,
|
||||
'margin_left': ZERO_PIXELS,
|
||||
'padding_top': ZERO_PIXELS,
|
||||
'padding_right': ZERO_PIXELS,
|
||||
'padding_bottom': ZERO_PIXELS,
|
||||
'padding_left': ZERO_PIXELS,
|
||||
'position': 'static',
|
||||
'right': 'auto',
|
||||
'table_layout': 'auto',
|
||||
'top': 'auto',
|
||||
'unicode_bidi': 'normal',
|
||||
'vertical_align': 'baseline',
|
||||
'visibility': 'visible',
|
||||
'z_index': 'auto',
|
||||
|
||||
# Backgrounds and Borders 3 (CR): https://www.w3.org/TR/css-backgrounds-3/
|
||||
'background_attachment': ('scroll',),
|
||||
'background_clip': ('border-box',),
|
||||
'background_color': parse_color('transparent'),
|
||||
'background_image': (('none', None),),
|
||||
'background_origin': ('padding-box',),
|
||||
'background_position': (('left', Dimension(0, '%'),
|
||||
'top', Dimension(0, '%')),),
|
||||
'background_repeat': (('repeat', 'repeat'),),
|
||||
'background_size': (('auto', 'auto'),),
|
||||
'border_bottom_color': 'currentColor',
|
||||
'border_bottom_left_radius': (ZERO_PIXELS, ZERO_PIXELS),
|
||||
'border_bottom_right_radius': (ZERO_PIXELS, ZERO_PIXELS),
|
||||
'border_bottom_style': 'none',
|
||||
'border_bottom_width': 3,
|
||||
'border_collapse': 'separate',
|
||||
'border_left_color': 'currentColor',
|
||||
'border_left_style': 'none',
|
||||
'border_left_width': 3,
|
||||
'border_right_color': 'currentColor',
|
||||
'border_right_style': 'none',
|
||||
'border_right_width': 3,
|
||||
'border_spacing': (0, 0),
|
||||
'border_top_color': 'currentColor',
|
||||
'border_top_left_radius': (ZERO_PIXELS, ZERO_PIXELS),
|
||||
'border_top_right_radius': (ZERO_PIXELS, ZERO_PIXELS),
|
||||
'border_top_style': 'none',
|
||||
'border_top_width': 3, # computed value for 'medium'
|
||||
'border_image_source': ('none', None),
|
||||
'border_image_slice': (
|
||||
Dimension(100, '%'), Dimension(100, '%'),
|
||||
Dimension(100, '%'), Dimension(100, '%'),
|
||||
None),
|
||||
'border_image_width': (1, 1, 1, 1),
|
||||
'border_image_outset': (
|
||||
Dimension(0, None), Dimension(0, None),
|
||||
Dimension(0, None), Dimension(0, None)),
|
||||
'border_image_repeat': ('stretch', 'stretch'),
|
||||
|
||||
|
||||
# Color 3 (REC): https://www.w3.org/TR/css-color-3/
|
||||
'opacity': 1,
|
||||
|
||||
# Multi-column Layout (WD): https://www.w3.org/TR/css-multicol-1/
|
||||
'column_width': 'auto',
|
||||
'column_count': 'auto',
|
||||
'column_rule_color': 'currentColor',
|
||||
'column_rule_style': 'none',
|
||||
'column_rule_width': 'medium',
|
||||
'column_fill': 'balance',
|
||||
'column_span': 'none',
|
||||
|
||||
# Fonts 3 (REC): https://www.w3.org/TR/css-fonts-3/
|
||||
'font_family': ('serif',), # depends on user agent
|
||||
'font_feature_settings': 'normal',
|
||||
'font_kerning': 'auto',
|
||||
'font_language_override': 'normal',
|
||||
'font_size': 16, # actually medium, but we define medium from this
|
||||
'font_stretch': 'normal',
|
||||
'font_style': 'normal',
|
||||
'font_variant': 'normal',
|
||||
'font_variant_alternates': 'normal',
|
||||
'font_variant_caps': 'normal',
|
||||
'font_variant_east_asian': 'normal',
|
||||
'font_variant_ligatures': 'normal',
|
||||
'font_variant_numeric': 'normal',
|
||||
'font_variant_position': 'normal',
|
||||
'font_weight': 400,
|
||||
|
||||
# Fonts 4 (WD): https://www.w3.org/TR/css-fonts-4/
|
||||
'font_variation_settings': 'normal',
|
||||
|
||||
# Fragmentation 3/4 (CR/WD): https://www.w3.org/TR/css-break-4/
|
||||
'box_decoration_break': 'slice',
|
||||
'break_after': 'auto',
|
||||
'break_before': 'auto',
|
||||
'break_inside': 'auto',
|
||||
'margin_break': 'auto',
|
||||
'orphans': 2,
|
||||
'widows': 2,
|
||||
|
||||
# Generated Content 3 (WD): https://www.w3.org/TR/css-content-3/
|
||||
'bookmark_label': (('content', 'text'),),
|
||||
'bookmark_level': 'none',
|
||||
'bookmark_state': 'open',
|
||||
'content': 'normal',
|
||||
'footnote_display': 'block',
|
||||
'footnote_policy': 'auto',
|
||||
'quotes': 'auto',
|
||||
'string_set': 'none',
|
||||
|
||||
# Images 3/4 (CR/WD): https://www.w3.org/TR/css-images-4/
|
||||
'image_resolution': 1, # dppx
|
||||
'image_rendering': 'auto',
|
||||
'image_orientation': 'from-image',
|
||||
'object_fit': 'fill',
|
||||
'object_position': (('left', Dimension(50, '%'),
|
||||
'top', Dimension(50, '%')),),
|
||||
|
||||
# Paged Media 3 (WD): https://www.w3.org/TR/css-page-3/
|
||||
'size': None, # set to A4 in computed_values
|
||||
'page': 'auto',
|
||||
'bleed_left': 'auto',
|
||||
'bleed_right': 'auto',
|
||||
'bleed_top': 'auto',
|
||||
'bleed_bottom': 'auto',
|
||||
'marks': (), # computed value for 'none'
|
||||
|
||||
# Text 3/4 (WD/WD): https://www.w3.org/TR/css-text-4/
|
||||
'hyphenate_character': '‐', # computed value chosen by the user agent
|
||||
'hyphenate_limit_chars': (5, 2, 2),
|
||||
'hyphenate_limit_zone': ZERO_PIXELS,
|
||||
'hyphens': 'manual',
|
||||
'letter_spacing': 'normal',
|
||||
'tab_size': 8,
|
||||
'text_align_all': 'start',
|
||||
'text_align_last': 'auto',
|
||||
'text_indent': ZERO_PIXELS,
|
||||
'text_transform': 'none',
|
||||
'white_space': 'normal',
|
||||
'word_break': 'normal',
|
||||
'word_spacing': 0, # computed value for 'normal'
|
||||
|
||||
# Transforms 1 (CR): https://www.w3.org/TR/css-transforms-1/
|
||||
'transform_origin': (Dimension(50, '%'), Dimension(50, '%')),
|
||||
'transform': (), # computed value for 'none'
|
||||
|
||||
# User Interface 3/4 (REC/WD): https://www.w3.org/TR/css-ui-4/
|
||||
'appearance': 'none',
|
||||
'outline_color': 'currentColor', # invert is not supported
|
||||
'outline_style': 'none',
|
||||
'outline_width': 3, # computed value for 'medium'
|
||||
|
||||
# Sizing 3 (WD): https://www.w3.org/TR/css-sizing-3/
|
||||
'box_sizing': 'content-box',
|
||||
'height': 'auto',
|
||||
'max_height': Dimension(inf, 'px'), # parsed value for 'none'
|
||||
'max_width': Dimension(inf, 'px'),
|
||||
'min_height': 'auto',
|
||||
'min_width': 'auto',
|
||||
'width': 'auto',
|
||||
|
||||
# Flexible Box Layout Module 1 (CR): https://www.w3.org/TR/css-flexbox-1/
|
||||
'flex_basis': 'auto',
|
||||
'flex_direction': 'row',
|
||||
'flex_grow': 0,
|
||||
'flex_shrink': 1,
|
||||
'flex_wrap': 'nowrap',
|
||||
|
||||
# Grid Layout Module Level 2 (CR): https://www.w3.org/TR/css-grid-2/
|
||||
'grid_auto_columns': ('auto',),
|
||||
'grid_auto_flow': ('row',),
|
||||
'grid_auto_rows': ('auto',),
|
||||
'grid_template_areas': 'none',
|
||||
'grid_template_columns': 'none',
|
||||
'grid_template_rows': 'none',
|
||||
'grid_row_start': 'auto',
|
||||
'grid_column_start': 'auto',
|
||||
'grid_row_end': 'auto',
|
||||
'grid_column_end': 'auto',
|
||||
|
||||
# CSS Box Alignment Module Level 3 (WD): https://www.w3.org/TR/css-align-3/
|
||||
'align_content': ('normal',),
|
||||
'align_items': ('normal',),
|
||||
'align_self': ('auto',),
|
||||
'justify_content': ('normal',),
|
||||
'justify_items': ('normal',),
|
||||
'justify_self': ('auto',),
|
||||
'order': 0,
|
||||
'column_gap': 'normal',
|
||||
'row_gap': 'normal',
|
||||
|
||||
# Text Decoration Module 3 (CR): https://www.w3.org/TR/css-text-decor-3/
|
||||
'text_decoration_line': 'none',
|
||||
'text_decoration_color': 'currentColor',
|
||||
'text_decoration_style': 'solid',
|
||||
|
||||
# Overflow Module 3/4 (WD): https://www.w3.org/TR/css-overflow-4/
|
||||
'block_ellipsis': 'none',
|
||||
'continue': 'auto',
|
||||
'max_lines': 'none',
|
||||
'overflow': 'visible',
|
||||
'overflow_wrap': 'normal',
|
||||
'text_overflow': 'clip',
|
||||
|
||||
# Lists Module 3 (WD): https://drafts.csswg.org/css-lists-3/
|
||||
# Means 'none', but allow `display: list-item` to increment the
|
||||
# list-item counter. If we ever have a way for authors to query
|
||||
# computed values (JavaScript?), this value should serialize to 'none'.
|
||||
'counter_increment': 'auto',
|
||||
'counter_reset': (), # parsed value for 'none'
|
||||
'counter_set': (), # parsed value for 'none'
|
||||
'list_style_image': ('none', None),
|
||||
'list_style_position': 'outside',
|
||||
'list_style_type': 'disc',
|
||||
|
||||
# Proprietary
|
||||
'anchor': None, # computed value of 'none'
|
||||
'link': None, # computed value of 'none'
|
||||
'lang': None, # computed value of 'none'
|
||||
}
|
||||
|
||||
|
||||
KNOWN_PROPERTIES = set(name.replace('_', '-') for name in INITIAL_VALUES)
|
||||
|
||||
# Do not list shorthand properties here as we handle them before inheritance.
|
||||
#
|
||||
# Values inherited but not applicable to print are not included.
|
||||
#
|
||||
# text_decoration is not a really inherited, see
|
||||
# https://www.w3.org/TR/CSS2/text.html#propdef-text-decoration
|
||||
#
|
||||
# link: click events normally bubble up to link ancestors
|
||||
# See https://lists.w3.org/Archives/Public/www-style/2012Jun/0315.html
|
||||
INHERITED = {
|
||||
'block_ellipsis',
|
||||
'border_collapse',
|
||||
'border_spacing',
|
||||
'caption_side',
|
||||
'color',
|
||||
'direction',
|
||||
'empty_cells',
|
||||
'font_family',
|
||||
'font_feature_settings',
|
||||
'font_kerning',
|
||||
'font_language_override',
|
||||
'font_size',
|
||||
'font_style',
|
||||
'font_stretch',
|
||||
'font_variant',
|
||||
'font_variant_alternates',
|
||||
'font_variant_caps',
|
||||
'font_variant_east_asian',
|
||||
'font_variant_ligatures',
|
||||
'font_variant_numeric',
|
||||
'font_variant_position',
|
||||
'font_variation_settings',
|
||||
'font_weight',
|
||||
'hyphens',
|
||||
'hyphenate_character',
|
||||
'hyphenate_limit_chars',
|
||||
'hyphenate_limit_zone',
|
||||
'image_rendering',
|
||||
'image_resolution',
|
||||
'lang',
|
||||
'letter_spacing',
|
||||
'line_height',
|
||||
'link',
|
||||
'list_style_image',
|
||||
'list_style_position',
|
||||
'list_style_type',
|
||||
'orphans',
|
||||
'overflow_wrap',
|
||||
'quotes',
|
||||
'tab_size',
|
||||
'text_align_all',
|
||||
'text_align_last',
|
||||
'text_indent',
|
||||
'text_transform',
|
||||
'visibility',
|
||||
'white_space',
|
||||
'widows',
|
||||
'word_break',
|
||||
'word_spacing',
|
||||
}
|
||||
|
||||
|
||||
# https://www.w3.org/TR/CSS21/tables.html#model
|
||||
# See also https://lists.w3.org/Archives/Public/www-style/2012Jun/0066.html
|
||||
# Only non-inherited properties need to be included here.
|
||||
TABLE_WRAPPER_BOX_PROPERTIES = {
|
||||
'bottom',
|
||||
'break_after',
|
||||
'break_before',
|
||||
'clear',
|
||||
'counter_increment',
|
||||
'counter_reset',
|
||||
'counter_set',
|
||||
'float',
|
||||
'left',
|
||||
'margin_top',
|
||||
'margin_bottom',
|
||||
'margin_left',
|
||||
'margin_right',
|
||||
'opacity',
|
||||
'overflow',
|
||||
'position',
|
||||
'right',
|
||||
'top',
|
||||
'transform',
|
||||
'transform_origin',
|
||||
'vertical_align',
|
||||
'z_index',
|
||||
}
|
||||
|
||||
|
||||
# Properties that have an initial value that is not always the same when
|
||||
# computed.
|
||||
INITIAL_NOT_COMPUTED = {
|
||||
'display',
|
||||
'column_gap',
|
||||
'bleed_top',
|
||||
'bleed_left',
|
||||
'bleed_bottom',
|
||||
'bleed_right',
|
||||
'outline_width',
|
||||
'outline_color',
|
||||
'column_rule_width',
|
||||
'column_rule_color',
|
||||
'border_top_width',
|
||||
'border_left_width',
|
||||
'border_bottom_width',
|
||||
'border_right_width',
|
||||
'border_top_color',
|
||||
'border_left_color',
|
||||
'border_bottom_color',
|
||||
'border_right_color',
|
||||
}
|
||||
225
app/.venv/Lib/site-packages/weasyprint/css/targets.py
Normal file
225
app/.venv/Lib/site-packages/weasyprint/css/targets.py
Normal file
@@ -0,0 +1,225 @@
|
||||
"""Handle target-counter, target-counters and target-text.
|
||||
|
||||
The TargetCollector is a structure providing required targets' counter_values
|
||||
and stuff needed to build pending targets later, when the layout of all
|
||||
targeted anchors has been done.
|
||||
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
from ..logger import LOGGER
|
||||
|
||||
|
||||
class TargetLookupItem:
|
||||
"""Item controlling pending targets and page based target counters.
|
||||
|
||||
Collected in the TargetCollector's ``target_lookup_items``.
|
||||
|
||||
"""
|
||||
def __init__(self, state='pending'):
|
||||
self.state = state
|
||||
|
||||
# Required by target-counter and target-counters to access the
|
||||
# target's .cached_counter_values.
|
||||
# Needed for target-text via extract_text().
|
||||
self.target_box = None
|
||||
|
||||
# Functions that have to been called to check pending targets.
|
||||
# Keys are (source_box, css_token).
|
||||
self.parse_again_functions = {}
|
||||
|
||||
# Anchor position during pagination (page_number - 1)
|
||||
self.page_maker_index = None
|
||||
|
||||
# target_box's page_counters during pagination
|
||||
self.cached_page_counter_values = {}
|
||||
|
||||
|
||||
class CounterLookupItem:
|
||||
"""Item controlling page based counters.
|
||||
|
||||
Collected in the TargetCollector's ``counter_lookup_items``.
|
||||
|
||||
"""
|
||||
def __init__(self, parse_again, missing_counters, missing_target_counters):
|
||||
# Function that have to been called to check pending counter.
|
||||
self.parse_again = parse_again
|
||||
|
||||
# Missing counters and target counters
|
||||
self.missing_counters = missing_counters
|
||||
self.missing_target_counters = missing_target_counters
|
||||
|
||||
# Box position during pagination (page_number - 1)
|
||||
self.page_maker_index = None
|
||||
|
||||
# Marker for remake_page
|
||||
self.pending = False
|
||||
|
||||
# Targeting box's page_counters during pagination
|
||||
self.cached_page_counter_values = {}
|
||||
|
||||
|
||||
def anchor_name_from_token(anchor_token):
|
||||
"""Get anchor name from string or uri token."""
|
||||
if anchor_token[0] == 'string' and anchor_token[1].startswith('#'):
|
||||
return anchor_token[1][1:]
|
||||
elif anchor_token[0] == 'url' and anchor_token[1][0] == 'internal':
|
||||
return anchor_token[1][1]
|
||||
|
||||
|
||||
class TargetCollector:
|
||||
"""Collector of HTML targets used by CSS content with ``target-*``."""
|
||||
|
||||
def __init__(self):
|
||||
# Lookup items for targets and page counters
|
||||
self.target_lookup_items = {}
|
||||
self.counter_lookup_items = {}
|
||||
|
||||
# When collecting is True, compute_content_list() collects missing
|
||||
# page counters in CounterLookupItems. Otherwise, it mixes in the
|
||||
# TargetLookupItem's cached_page_counter_values.
|
||||
# Is switched to False in check_pending_targets().
|
||||
self.collecting = True
|
||||
|
||||
# had_pending_targets is set to True when a target is needed but has
|
||||
# not been seen yet. check_pending_targets then uses this information
|
||||
# to call the needed parse_again functions.
|
||||
self.had_pending_targets = False
|
||||
|
||||
def collect_anchor(self, anchor_name):
|
||||
"""Create a TargetLookupItem for the given `anchor_name``."""
|
||||
if isinstance(anchor_name, str):
|
||||
if self.target_lookup_items.get(anchor_name) is not None:
|
||||
LOGGER.warning('Anchor defined twice: %r', anchor_name)
|
||||
else:
|
||||
self.target_lookup_items.setdefault(
|
||||
anchor_name, TargetLookupItem())
|
||||
|
||||
def lookup_target(self, anchor_token, source_box, css_token, parse_again):
|
||||
"""Get a TargetLookupItem corresponding to ``anchor_token``.
|
||||
|
||||
If it is already filled by a previous anchor-element, the status is
|
||||
'up-to-date'. Otherwise, it is 'pending', we must parse the whole
|
||||
tree again.
|
||||
|
||||
"""
|
||||
anchor_name = anchor_name_from_token(anchor_token)
|
||||
item = self.target_lookup_items.get(
|
||||
anchor_name, TargetLookupItem('undefined'))
|
||||
|
||||
if item.state == 'pending':
|
||||
self.had_pending_targets = True
|
||||
item.parse_again_functions.setdefault(
|
||||
(source_box, css_token), parse_again)
|
||||
|
||||
if item.state == 'undefined':
|
||||
LOGGER.error(
|
||||
'Content discarded: target points to undefined anchor %r',
|
||||
anchor_token)
|
||||
|
||||
return item
|
||||
|
||||
def store_target(self, anchor_name, target_counter_values, target_box):
|
||||
"""Store a target called ``anchor_name``.
|
||||
|
||||
If there is a pending TargetLookupItem, it is updated. Only previously
|
||||
collected anchors are stored.
|
||||
|
||||
"""
|
||||
item = self.target_lookup_items.get(anchor_name)
|
||||
if item and item.state == 'pending':
|
||||
item.state = 'up-to-date'
|
||||
item.target_box = target_box
|
||||
# Store the counter_values in the target_box like
|
||||
# compute_content_list does.
|
||||
if target_box.cached_counter_values is None:
|
||||
target_box.cached_counter_values = {
|
||||
key: value.copy() for key, value
|
||||
in target_counter_values.items()}
|
||||
|
||||
def collect_missing_counters(self, parent_box, css_token,
|
||||
parse_again_function, missing_counters,
|
||||
missing_target_counters):
|
||||
"""Collect missing (probably page-based) counters during formatting.
|
||||
|
||||
The ``missing_counters`` are re-used during pagination.
|
||||
|
||||
The ``missing_link`` attribute added to the parent_box is required to
|
||||
connect the paginated boxes to their originating ``parent_box``.
|
||||
|
||||
"""
|
||||
# No counter collection during pagination
|
||||
if not self.collecting:
|
||||
return
|
||||
|
||||
# No need to add empty miss-lists
|
||||
if missing_counters or missing_target_counters:
|
||||
if parent_box.missing_link is None:
|
||||
parent_box.missing_link = parent_box
|
||||
counter_lookup_item = CounterLookupItem(
|
||||
parse_again_function, missing_counters,
|
||||
missing_target_counters)
|
||||
self.counter_lookup_items.setdefault(
|
||||
(parent_box, css_token), counter_lookup_item)
|
||||
|
||||
def check_pending_targets(self):
|
||||
"""Check pending targets if needed."""
|
||||
if self.had_pending_targets:
|
||||
for item in self.target_lookup_items.values():
|
||||
for function in item.parse_again_functions.values():
|
||||
function()
|
||||
self.had_pending_targets = False
|
||||
# Ready for pagination
|
||||
self.collecting = False
|
||||
|
||||
def cache_target_page_counters(self, anchor_name, page_counter_values,
|
||||
page_maker_index, page_maker):
|
||||
"""Store target's current ``page_maker_index`` and page counter values.
|
||||
|
||||
Eventually update associated targeting boxes.
|
||||
|
||||
"""
|
||||
# Only store page counters when paginating
|
||||
if self.collecting:
|
||||
return
|
||||
|
||||
item = self.target_lookup_items.get(anchor_name)
|
||||
if item and item.state == 'up-to-date':
|
||||
item.page_maker_index = page_maker_index
|
||||
if item.cached_page_counter_values != page_counter_values:
|
||||
item.cached_page_counter_values = copy.deepcopy(
|
||||
page_counter_values)
|
||||
|
||||
# Spread the news: update boxes affected by a change in the
|
||||
# anchor's page counter values.
|
||||
for (_, css_token), item in self.counter_lookup_items.items():
|
||||
# Only update items that need counters in their content
|
||||
if css_token != 'content':
|
||||
continue
|
||||
|
||||
# Don't update if item has no missing target counter
|
||||
missing_counters = item.missing_target_counters.get(
|
||||
anchor_name)
|
||||
if missing_counters is None:
|
||||
continue
|
||||
|
||||
# Pending marker for remake_page
|
||||
if (item.page_maker_index is None or
|
||||
item.page_maker_index >= len(page_maker)):
|
||||
item.pending = True
|
||||
continue
|
||||
|
||||
# TODO: Is the item at all interested in the new
|
||||
# page_counter_values? It probably is and this check is a
|
||||
# brake.
|
||||
for counter_name in missing_counters:
|
||||
counter_value = page_counter_values.get(counter_name)
|
||||
if counter_value is not None:
|
||||
remake_state = (
|
||||
page_maker[item.page_maker_index][-1])
|
||||
remake_state['content_changed'] = True
|
||||
item.parse_again(item.cached_page_counter_values)
|
||||
break
|
||||
# Hint: the box's own cached page counters trigger a
|
||||
# separate 'content_changed'.
|
||||
40
app/.venv/Lib/site-packages/weasyprint/css/tests_ua.css
Normal file
40
app/.venv/Lib/site-packages/weasyprint/css/tests_ua.css
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
Simplified user-agent stylesheet for HTML5 in tests.
|
||||
*/
|
||||
@page { background: white; bleed: 0; @footnote { margin: 0 } }
|
||||
html, body, div, h1, h2, h3, h4, ol, p, ul, hr, pre, section, article
|
||||
{ display: block }
|
||||
body { orphans: 1; widows: 1 }
|
||||
li { display: list-item }
|
||||
head { display: none }
|
||||
pre { white-space: pre }
|
||||
br:before { content: '\A'; white-space: pre-line }
|
||||
ol { list-style-type: decimal }
|
||||
ol, ul { counter-reset: list-item }
|
||||
|
||||
table, x-table { display: table;
|
||||
box-sizing: border-box }
|
||||
tr, x-tr { display: table-row }
|
||||
thead, x-thead { display: table-header-group }
|
||||
tbody, x-tbody { display: table-row-group }
|
||||
tfoot, x-tfoot { display: table-footer-group }
|
||||
col, x-col { display: table-column }
|
||||
colgroup, x-colgroup { display: table-column-group }
|
||||
td, th, x-td, x-th { display: table-cell }
|
||||
caption, x-caption { display: table-caption }
|
||||
|
||||
*[lang] { -weasy-lang: attr(lang) }
|
||||
a[href] { -weasy-link: attr(href) }
|
||||
a[name] { -weasy-anchor: attr(name) }
|
||||
*[id] { -weasy-anchor: attr(id) }
|
||||
h1 { bookmark-level: 1; bookmark-label: content(text) }
|
||||
h2 { bookmark-level: 2; bookmark-label: content(text) }
|
||||
h3 { bookmark-level: 3; bookmark-label: content(text) }
|
||||
h4 { bookmark-level: 4; bookmark-label: content(text) }
|
||||
h5 { bookmark-level: 5; bookmark-label: content(text) }
|
||||
h6 { bookmark-level: 6; bookmark-label: content(text) }
|
||||
|
||||
::marker { unicode-bidi: isolate; font-variant-numeric: tabular-nums }
|
||||
|
||||
::footnote-call { content: counter(footnote) }
|
||||
::footnote-marker { content: counter(footnote) '.' }
|
||||
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)
|
||||
@@ -0,0 +1,246 @@
|
||||
"""Validate properties, expanders and descriptors."""
|
||||
|
||||
from cssselect2 import SelectorError, compile_selector_list
|
||||
from tinycss2 import parse_blocks_contents, serialize
|
||||
from tinycss2.ast import FunctionBlock, IdentToken, LiteralToken, WhitespaceToken
|
||||
|
||||
from ... import LOGGER
|
||||
from ..utils import InvalidValues, remove_whitespace
|
||||
from .expanders import EXPANDERS
|
||||
from .properties import PREFIX, PROPRIETARY, UNSTABLE, validate_non_shorthand
|
||||
|
||||
# Not applicable to the print media
|
||||
NOT_PRINT_MEDIA = {
|
||||
# Aural media
|
||||
'azimuth',
|
||||
'cue',
|
||||
'cue-after',
|
||||
'cue-before',
|
||||
'elevation',
|
||||
'pause',
|
||||
'pause-after',
|
||||
'pause-before',
|
||||
'pitch-range',
|
||||
'pitch',
|
||||
'play-during',
|
||||
'richness',
|
||||
'speak-header',
|
||||
'speak-numeral',
|
||||
'speak-punctuation',
|
||||
'speak',
|
||||
'speech-rate',
|
||||
'stress',
|
||||
'voice-family',
|
||||
'volume',
|
||||
# Animations, transitions, timelines
|
||||
'animation',
|
||||
'animation-composition',
|
||||
'animation-delay',
|
||||
'animation-direction',
|
||||
'animation-duration',
|
||||
'animation-fill-mode',
|
||||
'animation-iteration-count',
|
||||
'animation-name',
|
||||
'animation-play-state',
|
||||
'animation-range',
|
||||
'animation-range-end',
|
||||
'animation-range-start',
|
||||
'animation-timeline',
|
||||
'animation-timing-function',
|
||||
'timeline-scope',
|
||||
'transition',
|
||||
'transition-delay',
|
||||
'transition-duration',
|
||||
'transition-property',
|
||||
'transition-timing-function',
|
||||
'view-timeline',
|
||||
'view-timeline-axis',
|
||||
'view-timeline-inset',
|
||||
'view-timeline-name',
|
||||
'view-transition-name',
|
||||
'will-change',
|
||||
# Dynamic and interactive
|
||||
'caret',
|
||||
'caret-color',
|
||||
'caret-shape',
|
||||
'cursor',
|
||||
'field-sizing',
|
||||
'pointer-event',
|
||||
'resize',
|
||||
'touch-action',
|
||||
# Browser viewport scrolling
|
||||
'overscroll-behavior',
|
||||
'overscroll-behavior-block',
|
||||
'overscroll-behavior-inline',
|
||||
'overscroll-behavior-x',
|
||||
'overscroll-behavior-y',
|
||||
'scroll-behavior',
|
||||
'scroll-margin',
|
||||
'scroll-margin-block',
|
||||
'scroll-margin-block-end',
|
||||
'scroll-margin-block-start',
|
||||
'scroll-margin-bottom',
|
||||
'scroll-margin-inline',
|
||||
'scroll-margin-inline-end',
|
||||
'scroll-margin-inline-start',
|
||||
'scroll-margin-left',
|
||||
'scroll-margin-right',
|
||||
'scroll-margin-top',
|
||||
'scroll-padding',
|
||||
'scroll-padding-block',
|
||||
'scroll-padding-block-end',
|
||||
'scroll-padding-block-start',
|
||||
'scroll-padding-bottom',
|
||||
'scroll-padding-inline',
|
||||
'scroll-padding-inline-end',
|
||||
'scroll-padding-inline-start',
|
||||
'scroll-padding-left',
|
||||
'scroll-padding-right',
|
||||
'scroll-padding-top',
|
||||
'scroll-snap-align',
|
||||
'scroll-snap-stop',
|
||||
'scroll-snap-type',
|
||||
'scroll-timeline',
|
||||
'scroll-timeline-axis',
|
||||
'scroll-timeline-name',
|
||||
'scrollbar-color',
|
||||
'scrollbar-gutter',
|
||||
'scrollbar-width',
|
||||
}
|
||||
NESTING_SELECTOR = LiteralToken(1, 1, '&')
|
||||
ROOT_TOKEN = LiteralToken(1, 1, ':'), IdentToken(1, 1, 'root')
|
||||
|
||||
|
||||
def preprocess_declarations(base_url, declarations, prelude=None):
|
||||
"""Expand shorthand properties, filter unsupported properties and values.
|
||||
|
||||
Log a warning for every ignored declaration.
|
||||
|
||||
Return a iterable of ``(name, value, important)`` tuples.
|
||||
|
||||
"""
|
||||
# Compile list of selectors.
|
||||
if prelude is not None:
|
||||
try:
|
||||
if NESTING_SELECTOR in prelude:
|
||||
# Handle & selector in non-nested rule. MDN explains that & is
|
||||
# then equivalent to :scope, and :scope is equivalent to :root
|
||||
# as we don’t support :scope yet.
|
||||
original_prelude, prelude = prelude, []
|
||||
for token in original_prelude:
|
||||
if token == NESTING_SELECTOR:
|
||||
prelude.extend(ROOT_TOKEN)
|
||||
else:
|
||||
prelude.append(token)
|
||||
selectors = compile_selector_list(prelude)
|
||||
except SelectorError:
|
||||
raise SelectorError(f"'{serialize(prelude)}'")
|
||||
|
||||
# Yield declarations.
|
||||
is_token = LiteralToken(1, 1, ':'), FunctionBlock(1, 1, 'is', prelude)
|
||||
for declaration in declarations:
|
||||
if declaration.type == 'error':
|
||||
LOGGER.warning(
|
||||
'Error: %s at %d:%d.',
|
||||
declaration.message,
|
||||
declaration.source_line, declaration.source_column)
|
||||
|
||||
if declaration.type == 'qualified-rule':
|
||||
# Nested rule.
|
||||
if prelude is None:
|
||||
continue
|
||||
declaration_prelude = []
|
||||
token_groups = [[]]
|
||||
for token in declaration.prelude:
|
||||
if token == ',':
|
||||
token_groups.append([])
|
||||
else:
|
||||
token_groups[-1].append(token)
|
||||
for token_group in token_groups:
|
||||
if NESTING_SELECTOR in token_group:
|
||||
# Replace & selector by parent.
|
||||
for token in declaration.prelude:
|
||||
if token == NESTING_SELECTOR:
|
||||
declaration_prelude.extend(is_token)
|
||||
else:
|
||||
declaration_prelude.append(token)
|
||||
else:
|
||||
# No & selector, prepend parent.
|
||||
is_token = (
|
||||
LiteralToken(1, 1, ':'),
|
||||
FunctionBlock(1, 1, 'is', prelude))
|
||||
declaration_prelude.extend([
|
||||
*is_token, WhitespaceToken(1, 1, ' '),
|
||||
*token_group])
|
||||
declaration_prelude.append(LiteralToken(1, 1, ','))
|
||||
yield from preprocess_declarations(
|
||||
base_url, parse_blocks_contents(declaration.content),
|
||||
declaration_prelude[:-1])
|
||||
|
||||
if declaration.type != 'declaration':
|
||||
continue
|
||||
|
||||
name = declaration.name
|
||||
if not name.startswith('--'):
|
||||
name = declaration.lower_name
|
||||
|
||||
def validation_error(level, reason):
|
||||
getattr(LOGGER, level)(
|
||||
'Ignored `%s:%s` at %d:%d, %s.',
|
||||
declaration.name, serialize(declaration.value),
|
||||
declaration.source_line, declaration.source_column, reason)
|
||||
|
||||
if name in NOT_PRINT_MEDIA:
|
||||
validation_error(
|
||||
'debug', 'the property does not apply for the print media')
|
||||
continue
|
||||
|
||||
if name.startswith(PREFIX):
|
||||
unprefixed_name = name[len(PREFIX):]
|
||||
if unprefixed_name in PROPRIETARY:
|
||||
name = unprefixed_name
|
||||
elif unprefixed_name in UNSTABLE:
|
||||
LOGGER.warning(
|
||||
'Deprecated `%s:%s` at %d:%d, '
|
||||
'prefixes on unstable attributes are deprecated, '
|
||||
'use %r instead.',
|
||||
declaration.name, serialize(declaration.value),
|
||||
declaration.source_line, declaration.source_column,
|
||||
unprefixed_name)
|
||||
name = unprefixed_name
|
||||
else:
|
||||
LOGGER.warning(
|
||||
'Ignored `%s:%s` at %d:%d, '
|
||||
'prefix on this attribute is not supported, '
|
||||
'use %r instead.',
|
||||
declaration.name, serialize(declaration.value),
|
||||
declaration.source_line, declaration.source_column,
|
||||
unprefixed_name)
|
||||
continue
|
||||
|
||||
if name.startswith('-') and not name.startswith('--'):
|
||||
validation_error('debug', 'prefixed selectors are ignored')
|
||||
continue
|
||||
|
||||
validator = EXPANDERS.get(name, validate_non_shorthand)
|
||||
tokens = remove_whitespace(declaration.value)
|
||||
try:
|
||||
# Having no tokens is allowed by grammar but refused by all
|
||||
# properties and expanders.
|
||||
if not tokens:
|
||||
raise InvalidValues('no value')
|
||||
# Use list() to consume generators now and catch any error.
|
||||
result = list(validator(tokens, name, base_url))
|
||||
except InvalidValues as exc:
|
||||
validation_error(
|
||||
'warning',
|
||||
exc.args[0] if exc.args and exc.args[0] else 'invalid value')
|
||||
continue
|
||||
|
||||
important = declaration.important
|
||||
for long_name, value in result:
|
||||
if prelude is not None:
|
||||
declaration = (long_name.replace('-', '_'), value, important)
|
||||
yield selectors, declaration
|
||||
else:
|
||||
yield long_name.replace('-', '_'), value, important
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,347 @@
|
||||
"""Validate descriptors used for some at-rules."""
|
||||
|
||||
from math import inf
|
||||
|
||||
import tinycss2
|
||||
|
||||
from ...logger import LOGGER
|
||||
from . import properties
|
||||
|
||||
from ..utils import ( # isort:skip
|
||||
InvalidValues, comma_separated_list, get_custom_ident, get_keyword,
|
||||
get_single_keyword, get_url, remove_whitespace, single_keyword,
|
||||
single_token, split_on_comma)
|
||||
|
||||
DESCRIPTORS = {
|
||||
'font-face': {},
|
||||
'counter-style': {},
|
||||
}
|
||||
NOT_PRINT_MEDIA = (
|
||||
'font-display',
|
||||
)
|
||||
|
||||
class NoneFakeToken:
|
||||
type = 'ident'
|
||||
lower_value = 'none'
|
||||
|
||||
|
||||
class NormalFakeToken:
|
||||
type = 'ident'
|
||||
lower_value = 'normal'
|
||||
|
||||
|
||||
def preprocess_descriptors(rule, 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:
|
||||
if descriptor.type != 'declaration' or descriptor.important:
|
||||
continue
|
||||
tokens = remove_whitespace(descriptor.value)
|
||||
try:
|
||||
if descriptor.name in NOT_PRINT_MEDIA:
|
||||
continue
|
||||
elif descriptor.name not in DESCRIPTORS[rule]:
|
||||
raise InvalidValues('descriptor not supported')
|
||||
|
||||
function = DESCRIPTORS[rule][descriptor.name]
|
||||
if function.wants_base_url:
|
||||
value = function(tokens, base_url)
|
||||
else:
|
||||
value = function(tokens)
|
||||
if value is None:
|
||||
raise InvalidValues
|
||||
result = ((descriptor.name, value),)
|
||||
except InvalidValues as exc:
|
||||
LOGGER.warning(
|
||||
'Ignored `%s:%s` at %d:%d, %s.',
|
||||
descriptor.name, tinycss2.serialize(descriptor.value),
|
||||
descriptor.source_line, descriptor.source_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
|
||||
|
||||
|
||||
def descriptor(rule, 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[rule], name
|
||||
|
||||
function.wants_base_url = wants_base_url
|
||||
DESCRIPTORS[rule][name] = function
|
||||
return function
|
||||
return decorator
|
||||
|
||||
|
||||
def expand_font_variant(tokens):
|
||||
keyword = get_single_keyword(tokens)
|
||||
if keyword in ('normal', 'none'):
|
||||
for suffix in (
|
||||
'-alternates', '-caps', '-east-asian', '-numeric',
|
||||
'-position'):
|
||||
yield suffix, [NormalFakeToken]
|
||||
token = NormalFakeToken if keyword == 'normal' else NoneFakeToken
|
||||
yield '-ligatures', [token]
|
||||
else:
|
||||
features = {
|
||||
'alternates': [],
|
||||
'caps': [],
|
||||
'east-asian': [],
|
||||
'ligatures': [],
|
||||
'numeric': [],
|
||||
'position': []}
|
||||
for token in tokens:
|
||||
keyword = get_keyword(token)
|
||||
if keyword == 'normal':
|
||||
# We don't allow 'normal', only the specific values
|
||||
raise InvalidValues
|
||||
for feature in features:
|
||||
function_name = f'font_variant_{feature.replace("-", "_")}'
|
||||
if getattr(properties, function_name)([token]):
|
||||
features[feature].append(token)
|
||||
break
|
||||
else:
|
||||
raise InvalidValues
|
||||
for feature, tokens in features.items():
|
||||
if tokens:
|
||||
yield (f'-{feature}', tokens)
|
||||
|
||||
|
||||
@descriptor('font-face')
|
||||
def font_family(tokens, allow_spaces=False):
|
||||
"""``font-family`` descriptor validation."""
|
||||
allowed_types = ['ident']
|
||||
if allow_spaces:
|
||||
allowed_types.append('whitespace')
|
||||
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('font-face', wants_base_url=True)
|
||||
@comma_separated_list
|
||||
def src(tokens, base_url):
|
||||
"""``src`` descriptor validation."""
|
||||
if len(tokens) in (1, 2):
|
||||
tokens, token = tokens[:-1], tokens[-1]
|
||||
if token.type == 'function' and token.lower_name == 'format':
|
||||
tokens, token = tokens[:-1], tokens[-1]
|
||||
if token.type == 'function' and token.lower_name == 'local':
|
||||
return 'local', font_family(token.arguments, allow_spaces=True)
|
||||
url = get_url(token, base_url)
|
||||
if url is not None and url[0] == 'url':
|
||||
return url[1]
|
||||
|
||||
|
||||
@descriptor('font-face')
|
||||
@single_keyword
|
||||
def font_style(keyword):
|
||||
"""``font-style`` descriptor validation."""
|
||||
return keyword in ('normal', 'italic', 'oblique')
|
||||
|
||||
|
||||
@descriptor('font-face')
|
||||
@single_token
|
||||
def font_weight(token):
|
||||
"""``font-weight`` descriptor validation."""
|
||||
keyword = get_keyword(token)
|
||||
if keyword in ('normal', 'bold'):
|
||||
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
|
||||
|
||||
|
||||
@descriptor('font-face')
|
||||
@single_keyword
|
||||
def font_stretch(keyword):
|
||||
"""``font-stretch`` descriptor validation."""
|
||||
return keyword in (
|
||||
'ultra-condensed', 'extra-condensed', 'condensed', 'semi-condensed',
|
||||
'normal',
|
||||
'semi-expanded', 'expanded', 'extra-expanded', 'ultra-expanded')
|
||||
|
||||
|
||||
@descriptor('font-face')
|
||||
def font_feature_settings(tokens):
|
||||
"""``font-feature-settings`` descriptor validation."""
|
||||
return properties.font_feature_settings(tokens)
|
||||
|
||||
|
||||
@descriptor('font-face')
|
||||
def font_variant(tokens):
|
||||
"""``font-variant`` descriptor validation."""
|
||||
if len(tokens) == 1:
|
||||
keyword = get_keyword(tokens[0])
|
||||
if keyword in ('normal', 'none', 'inherit'):
|
||||
return []
|
||||
values = []
|
||||
for name, sub_tokens in expand_font_variant(tokens):
|
||||
try:
|
||||
values.append(properties.validate_non_shorthand(
|
||||
sub_tokens, f'font-variant{name}', required=True))
|
||||
except InvalidValues:
|
||||
return None
|
||||
return values
|
||||
|
||||
|
||||
@descriptor('counter-style')
|
||||
def system(tokens):
|
||||
"""``system`` descriptor validation."""
|
||||
if len(tokens) > 2:
|
||||
return
|
||||
|
||||
keyword = get_keyword(tokens[0])
|
||||
|
||||
if keyword == 'extends':
|
||||
if len(tokens) == 2:
|
||||
second_keyword = get_keyword(tokens[1])
|
||||
if second_keyword:
|
||||
return (keyword, second_keyword, None)
|
||||
elif keyword == 'fixed':
|
||||
if len(tokens) == 1:
|
||||
return (None, 'fixed', 1)
|
||||
elif tokens[1].type == 'number' and tokens[1].is_integer:
|
||||
return (None, 'fixed', tokens[1].int_value)
|
||||
elif len(tokens) == 1 and keyword in (
|
||||
'cyclic', 'numeric', 'alphabetic', 'symbolic', 'additive'):
|
||||
return (None, keyword, None)
|
||||
|
||||
|
||||
@descriptor('counter-style', wants_base_url=True)
|
||||
def negative(tokens, base_url):
|
||||
"""``negative`` descriptor validation."""
|
||||
if len(tokens) > 2:
|
||||
return
|
||||
|
||||
values = []
|
||||
tokens = list(tokens)
|
||||
while tokens:
|
||||
token = tokens.pop(0)
|
||||
if token.type in ('string', 'ident'):
|
||||
values.append(('string', token.value))
|
||||
continue
|
||||
url = get_url(token, base_url)
|
||||
if url is not None and url[0] == 'url':
|
||||
values.append(('url', url[1]))
|
||||
|
||||
if len(values) == 1:
|
||||
values.append(('string', ''))
|
||||
|
||||
if len(values) == 2:
|
||||
return values
|
||||
|
||||
|
||||
@descriptor('counter-style', 'prefix', wants_base_url=True)
|
||||
@descriptor('counter-style', 'suffix', wants_base_url=True)
|
||||
def prefix_suffix(tokens, base_url):
|
||||
"""``prefix`` and ``suffix`` descriptors validation."""
|
||||
if len(tokens) != 1:
|
||||
return
|
||||
|
||||
token, = tokens
|
||||
if token.type in ('string', 'ident'):
|
||||
return ('string', token.value)
|
||||
url = get_url(token, base_url)
|
||||
if url is not None and url[0] == 'url':
|
||||
return ('url', url[1])
|
||||
|
||||
|
||||
@descriptor('counter-style')
|
||||
@comma_separated_list
|
||||
def range(tokens):
|
||||
"""``range`` descriptor validation."""
|
||||
if len(tokens) == 1:
|
||||
keyword = get_single_keyword(tokens)
|
||||
if keyword == 'auto':
|
||||
return 'auto'
|
||||
elif len(tokens) == 2:
|
||||
values = []
|
||||
for i, token in enumerate(tokens):
|
||||
if token.type == 'ident' and token.value == 'infinite':
|
||||
values.append(inf if i else -inf)
|
||||
elif token.type == 'number' and token.is_integer:
|
||||
values.append(token.int_value)
|
||||
if len(values) == 2 and values[0] <= values[1]:
|
||||
return tuple(values)
|
||||
|
||||
|
||||
@descriptor('counter-style', wants_base_url=True)
|
||||
def pad(tokens, base_url):
|
||||
"""``pad`` descriptor validation."""
|
||||
if len(tokens) == 2:
|
||||
values = [None, None]
|
||||
for token in tokens:
|
||||
if token.type == 'number':
|
||||
if token.is_integer and token.value >= 0 and values[0] is None:
|
||||
values[0] = token.int_value
|
||||
elif token.type in ('string', 'ident'):
|
||||
values[1] = ('string', token.value)
|
||||
url = get_url(token, base_url)
|
||||
if url is not None and url[0] == 'url':
|
||||
values[1] = ('url', url[1])
|
||||
|
||||
if None not in values:
|
||||
return tuple(values)
|
||||
|
||||
|
||||
@descriptor('counter-style')
|
||||
@single_token
|
||||
def fallback(token):
|
||||
"""``fallback`` descriptor validation."""
|
||||
ident = get_custom_ident(token)
|
||||
if ident != 'none':
|
||||
return ident
|
||||
|
||||
|
||||
@descriptor('counter-style', wants_base_url=True)
|
||||
def symbols(tokens, base_url):
|
||||
"""``symbols`` descriptor validation."""
|
||||
values = []
|
||||
for token in tokens:
|
||||
if token.type in ('string', 'ident'):
|
||||
values.append(('string', token.value))
|
||||
continue
|
||||
url = get_url(token, base_url)
|
||||
if url is not None and url[0] == 'url':
|
||||
values.append(('url', url[1]))
|
||||
continue
|
||||
return
|
||||
return tuple(values)
|
||||
|
||||
|
||||
@descriptor('counter-style', wants_base_url=True)
|
||||
def additive_symbols(tokens, base_url):
|
||||
"""``additive-symbols`` descriptor validation."""
|
||||
results = []
|
||||
for part in split_on_comma(tokens):
|
||||
result = pad(remove_whitespace(part), base_url)
|
||||
if result is None:
|
||||
return
|
||||
if results and results[-1][0] <= result[0]:
|
||||
return
|
||||
results.append(result)
|
||||
return tuple(results)
|
||||
1038
app/.venv/Lib/site-packages/weasyprint/css/validation/expanders.py
Normal file
1038
app/.venv/Lib/site-packages/weasyprint/css/validation/expanders.py
Normal file
File diff suppressed because it is too large
Load Diff
2103
app/.venv/Lib/site-packages/weasyprint/css/validation/properties.py
Normal file
2103
app/.venv/Lib/site-packages/weasyprint/css/validation/properties.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user