1
1
mirror of https://github.com/Kozea/WeasyPrint.git synced 2024-10-05 08:27:22 +03:00
WeasyPrint/weasyprint/css/counters.py
Guillaume Ayoub b40ad461be Remove repeated copyright assignments in files
Everything is replaced by a more explicit message in the COPYRIGHT file.

Inspired by rust-lang/rust#43498 and rust-lang/rust#57108.
2020-01-02 14:10:09 +01:00

302 lines
11 KiB
Python

"""
weasyprint.css.counters
-----------------------
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 copy import deepcopy
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.
.. versionadded:: 0.52
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 = -float('inf'), float('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):
return CounterStyle(deepcopy(self))