debian-weasyprint/weasyprint/formatting_structure/counters.py

291 lines
8.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
weasyprint.formatting_structure.counters
----------------------------------------
Implement the various counter types and list-style-type values.
These are defined in the same terms as CSS 3 Lists:
http://dev.w3.org/csswg/css3-lists/#predefined-counters
:copyright: Copyright 2011-2019 Simon Sapin and contributors, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
import functools
__all__ = ['format', 'format_list_marker']
# Initial values for counter style descriptors.
INITIAL_VALUES = dict(
negative=('-', ''),
prefix='',
suffix='. ',
range=(float('-inf'), float('inf')),
fallback='decimal',
# type and symbols ommited here.
)
# Maps counter-style names to a dict of descriptors.
STYLES = {
# Included here for format_list_marker().
# format() special-cases decimal and does not use this.
'decimal': INITIAL_VALUES,
}
# Maps counter types to a function implementing it.
# The functions take three arguments: the values of the `symbols`
# (or `additive-symbols` for the additive type) and `negative` descriptors,
# and the integer value being formatted.
# They return the representation as a string or None. None means that
# the value can not represented and the fallback should be used.
FORMATTERS = {}
def register_style(name, type='symbolic', **descriptors):
"""Register a counter style."""
if type == 'override':
# TODO: when @counter-style rules are supported, change override
# to bind when a value is generated, not when the @rule is parsed.
style = dict(STYLES[descriptors.pop('override')])
else:
style = dict(INITIAL_VALUES, formatter=functools.partial(
FORMATTERS[type],
descriptors.pop('symbols'),
descriptors.pop('negative', INITIAL_VALUES['negative'])))
style.update(descriptors)
STYLES[name] = style
def register_formatter(function):
"""Register a counter type/algorithm."""
FORMATTERS[function.__name__.replace('_', '-')] = function
return function
@register_formatter
def repeating(symbols, _negative, value):
"""Implement the algorithm for `type: repeating`."""
return symbols[(value - 1) % len(symbols)]
@register_formatter
def numeric(symbols, negative, value):
"""Implement the algorithm for `type: numeric`."""
if value == 0:
return symbols[0]
is_negative = value < 0
if is_negative:
value = abs(value)
prefix, suffix = negative
reversed_parts = [suffix]
else:
reversed_parts = []
length = len(symbols)
value = abs(value)
while value != 0:
reversed_parts.append(symbols[value % length])
value //= length
if is_negative:
reversed_parts.append(prefix)
return ''.join(reversed(reversed_parts))
@register_formatter
def alphabetic(symbols, _negative, value):
"""Implement the algorithm for `type: alphabetic`."""
if value <= 0:
return None
length = len(symbols)
reversed_parts = []
while value != 0:
value -= 1
reversed_parts.append(symbols[value % length])
value //= length
return ''.join(reversed(reversed_parts))
@register_formatter
def symbolic(symbols, _negative, value):
"""Implement the algorithm for `type: symbolic`."""
if value <= 0:
return None
length = len(symbols)
return symbols[value % length] * ((value - 1) // length)
@register_formatter
def non_repeating(symbols, _negative, value):
"""Implement the algorithm for `type: non-repeating`."""
first_symbol_value, symbols = symbols
value -= first_symbol_value
if 0 <= value < len(symbols):
return symbols[value]
@register_formatter
def additive(symbols, negative, value):
"""Implement the algorithm for `type: additive`."""
if value == 0:
for weight, symbol in symbols:
if weight == 0:
return symbol
is_negative = value < 0
if is_negative:
value = abs(value)
prefix, suffix = negative
parts = [prefix]
else:
parts = []
for weight, symbol in symbols:
repetitions = value // weight
parts.extend([symbol] * repetitions)
value -= weight * repetitions
if value == 0:
if is_negative:
parts.append(suffix)
return ''.join(parts)
return None # Failed to find a representation for this value
# 'decimal' behaves the same as this, but defining it this way is silly.
# Well special-case it and just use str().
# register_style(
# 'decimal',
# type='numeric',
# symbols='0 1 2 3 4 5 6 7 8 9'.split(),
# )
register_style(
'decimal-leading-zero',
type='non-repeating',
symbols=(-9, '''-09 -08 -07 -06 -05 -04 -03 -02 -01
00 01 02 03 04 05 06 07 08 09'''.split()),
)
register_style(
'lower-roman',
type='additive',
range=(1, 4999),
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')],
)
register_style(
'upper-roman',
type='additive',
range=(1, 4999),
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')],
)
register_style(
'georgian',
type='additive',
range=(1, 19999),
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, '')],
)
register_style(
'armenian',
type='additive',
range=(1, 9999),
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, 'Ա')],
)
register_style(
'lower-alpha',
type='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'.split(),
)
register_style(
'upper-alpha',
type='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'.split(),
)
register_style(
'lower-greek',
type='alphabetic',
symbols='α β γ δ ε ζ η θ ι κ λ μ ν ξ ο π ρ σ τ υ φ χ ψ ω'.split()
)
register_style(
'disc',
type='repeating',
symbols=[''], # U+2022, BULLET
suffix=' ',
)
register_style(
'circle',
type='repeating',
symbols=[''], # U+25E6 WHITE BULLET
suffix=' ',
)
register_style(
'square',
type='repeating',
# CSS Lists 3 suggests U+25FE BLACK MEDIUM SMALL SQUARE
# But I think this one looks better.
symbols=[''], # U+25AA BLACK SMALL SQUARE
suffix=' ',
)
register_style(
'lower-latin',
type='override',
override='lower-alpha',
)
register_style(
'upper-latin',
type='override',
override='upper-alpha',
)
def format(value, counter_style):
"""
Return a representation of ``value`` formatted by ``counter_style``
or one of its fallback.
The representation includes negative signs, but not the prefix and suffix.
"""
if counter_style == 'none':
return ''
failed_styles = set() # avoid fallback loops
while True:
if counter_style == 'decimal' or counter_style in failed_styles:
return str(value)
style = STYLES[counter_style]
low, high = style['range']
if low <= value <= high:
representation = style['formatter'](value)
if representation is not None:
return representation
failed_styles.add(counter_style)
counter_style = style['fallback']
def format_list_marker(value, counter_style):
"""
Return a representation of ``value`` formatted for a list marker.
This is the same as :func:`format()`, but includes the counters
prefix and suffix.
"""
style = STYLES[counter_style]
return style['prefix'] + format(value, counter_style) + style['suffix']