mirror of
https://github.com/Kozea/WeasyPrint.git
synced 2024-10-05 08:27:22 +03:00
2ee4516786
Fix #700.
291 lines
8.6 KiB
Python
291 lines
8.6 KiB
Python
"""
|
||
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.
|
||
# We’ll 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 counter’s
|
||
prefix and suffix.
|
||
"""
|
||
style = STYLES[counter_style]
|
||
return style['prefix'] + format(value, counter_style) + style['suffix']
|