1"""
2    weasyprint.formatting_structure.counters
3    ----------------------------------------
4
5    Implement the various counter types and list-style-type values.
6
7    These are defined in the same terms as CSS 3 Lists:
8    http://dev.w3.org/csswg/css3-lists/#predefined-counters
9
10    :copyright: Copyright 2011-2019 Simon Sapin and contributors, see AUTHORS.
11    :license: BSD, see LICENSE for details.
12
13"""
14
15import functools
16
17__all__ = ['format', 'format_list_marker']
18
19
20# Initial values for counter style descriptors.
21INITIAL_VALUES = dict(
22    negative=('-', ''),
23    prefix='',
24    suffix='. ',
25    range=(float('-inf'), float('inf')),
26    fallback='decimal',
27    # type and symbols ommited here.
28)
29
30# Maps counter-style names to a dict of descriptors.
31STYLES = {
32    # Included here for format_list_marker().
33    # format() special-cases decimal and does not use this.
34    'decimal': INITIAL_VALUES,
35}
36
37# Maps counter types to a function implementing it.
38# The functions take three arguments: the values of the `symbols`
39# (or `additive-symbols` for the additive type) and `negative` descriptors,
40# and the integer value being formatted.
41# They return the representation as a string or None. None means that
42# the value can not represented and the fallback should be used.
43FORMATTERS = {}
44
45
46def register_style(name, type='symbolic', **descriptors):
47    """Register a counter style."""
48    if type == 'override':
49        # TODO: when @counter-style rules are supported, change override
50        # to bind when a value is generated, not when the @rule is parsed.
51        style = dict(STYLES[descriptors.pop('override')])
52    else:
53        style = dict(INITIAL_VALUES, formatter=functools.partial(
54            FORMATTERS[type],
55            descriptors.pop('symbols'),
56            descriptors.pop('negative', INITIAL_VALUES['negative'])))
57    style.update(descriptors)
58    STYLES[name] = style
59
60
61def register_formatter(function):
62    """Register a counter type/algorithm."""
63    FORMATTERS[function.__name__.replace('_', '-')] = function
64    return function
65
66
67@register_formatter
68def repeating(symbols, _negative, value):
69    """Implement the algorithm for `type: repeating`."""
70    return symbols[(value - 1) % len(symbols)]
71
72
73@register_formatter
74def numeric(symbols, negative, value):
75    """Implement the algorithm for `type: numeric`."""
76    if value == 0:
77        return symbols[0]
78    is_negative = value < 0
79    if is_negative:
80        value = abs(value)
81        prefix, suffix = negative
82        reversed_parts = [suffix]
83    else:
84        reversed_parts = []
85    length = len(symbols)
86    value = abs(value)
87    while value != 0:
88        reversed_parts.append(symbols[value % length])
89        value //= length
90    if is_negative:
91        reversed_parts.append(prefix)
92    return ''.join(reversed(reversed_parts))
93
94
95@register_formatter
96def alphabetic(symbols, _negative, value):
97    """Implement the algorithm for `type: alphabetic`."""
98    if value <= 0:
99        return None
100    length = len(symbols)
101    reversed_parts = []
102    while value != 0:
103        value -= 1
104        reversed_parts.append(symbols[value % length])
105        value //= length
106    return ''.join(reversed(reversed_parts))
107
108
109@register_formatter
110def symbolic(symbols, _negative, value):
111    """Implement the algorithm for `type: symbolic`."""
112    if value <= 0:
113        return None
114    length = len(symbols)
115    return symbols[value % length] * ((value - 1) // length)
116
117
118@register_formatter
119def non_repeating(symbols, _negative, value):
120    """Implement the algorithm for `type: non-repeating`."""
121    first_symbol_value, symbols = symbols
122    value -= first_symbol_value
123    if 0 <= value < len(symbols):
124        return symbols[value]
125
126
127@register_formatter
128def additive(symbols, negative, value):
129    """Implement the algorithm for `type: additive`."""
130    if value == 0:
131        for weight, symbol in symbols:
132            if weight == 0:
133                return symbol
134    is_negative = value < 0
135    if is_negative:
136        value = abs(value)
137        prefix, suffix = negative
138        parts = [prefix]
139    else:
140        parts = []
141    for weight, symbol in symbols:
142        repetitions = value // weight
143        parts.extend([symbol] * repetitions)
144        value -= weight * repetitions
145        if value == 0:
146            if is_negative:
147                parts.append(suffix)
148            return ''.join(parts)
149    return None  # Failed to find a representation for this value
150
151
152# 'decimal' behaves the same as this, but defining it this way is silly.
153# We’ll special-case it and just use str().
154# register_style(
155#     'decimal',
156#     type='numeric',
157#     symbols='0 1 2 3 4 5 6 7 8 9'.split(),
158# )
159register_style(
160    'decimal-leading-zero',
161    type='non-repeating',
162    symbols=(-9, '''-09 -08 -07 -06 -05 -04 -03 -02 -01
163                    00 01 02 03 04 05 06 07 08 09'''.split()),
164)
165register_style(
166    'lower-roman',
167    type='additive',
168    range=(1, 4999),
169    symbols=[(1000, 'm'), (900, 'cm'), (500, 'd'), (400, 'cd'),
170             (100, 'c'), (90, 'xc'), (50, 'l'), (40, 'xl'),
171             (10, 'x'), (9, 'ix'), (5, 'v'), (4, 'iv'),
172             (1, 'i')],
173)
174register_style(
175    'upper-roman',
176    type='additive',
177    range=(1, 4999),
178    symbols=[(1000, 'M'), (900, 'CM'), (500, 'D'), (400, 'CD'),
179             (100, 'C'), (90, 'XC'), (50, 'L'), (40, 'XL'),
180             (10, 'X'), (9, 'IX'), (5, 'V'), (4, 'IV'),
181             (1, 'I')],
182)
183register_style(
184    'georgian',
185    type='additive',
186    range=(1, 19999),
187    symbols=[
188        (10000, 'ჵ'), (9000, 'ჰ'), (8000, 'ჯ'), (7000, 'ჴ'), (6000, 'ხ'),
189        (5000, 'ჭ'), (4000, 'წ'), (3000, 'ძ'), (2000, 'ც'), (1000, 'ჩ'),
190        (900, 'შ'), (800, 'ყ'), (700, 'ღ'), (600, 'ქ'),
191        (500, 'ფ'), (400, 'ჳ'), (300, 'ტ'), (200, 'ს'), (100, 'რ'),
192        (90, 'ჟ'), (80, 'პ'), (70, 'ო'), (60, 'ჲ'),
193        (50, 'ნ'), (40, 'მ'), (30, 'ლ'), (20, 'კ'), (10, 'ი'),
194        (9, 'თ'), (8, 'ჱ'), (7, 'ზ'), (6, 'ვ'),
195        (5, 'ე'), (4, 'დ'), (3, 'გ'), (2, 'ბ'), (1, 'ა')],
196)
197register_style(
198    'armenian',
199    type='additive',
200    range=(1, 9999),
201    symbols=[
202        (9000, 'Ք'), (8000, 'Փ'), (7000, 'Ւ'), (6000, 'Ց'),
203        (5000, 'Ր'), (4000, 'Տ'), (3000, 'Վ'), (2000, 'Ս'), (1000, 'Ռ'),
204        (900, 'Ջ'), (800, 'Պ'), (700, 'Չ'), (600, 'Ո'),
205        (500, 'Շ'), (400, 'Ն'), (300, 'Յ'), (200, 'Մ'), (100, 'Ճ'),
206        (90, 'Ղ'), (80, 'Ձ'), (70, 'Հ'), (60, 'Կ'),
207        (50, 'Ծ'), (40, 'Խ'), (30, 'Լ'), (20, 'Ի'), (10, 'Ժ'),
208        (9, 'Թ'), (8, 'Ը'), (7, 'Է'), (6, 'Զ'),
209        (5, 'Ե'), (4, 'Դ'), (3, 'Գ'), (2, 'Բ'), (1, 'Ա')],
210)
211register_style(
212    'lower-alpha',
213    type='alphabetic',
214    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(),
215)
216register_style(
217    'upper-alpha',
218    type='alphabetic',
219    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(),
220)
221register_style(
222    'lower-greek',
223    type='alphabetic',
224    symbols='α β γ δ ε ζ η θ ι κ λ μ ν ξ ο π ρ σ τ υ φ χ ψ ω'.split()
225)
226register_style(
227    'disc',
228    type='repeating',
229    symbols=['•'],  # U+2022, BULLET
230    suffix=' ',
231)
232register_style(
233    'circle',
234    type='repeating',
235    symbols=['◦'],  # U+25E6 WHITE BULLET
236    suffix=' ',
237)
238register_style(
239    'square',
240    type='repeating',
241    # CSS Lists 3 suggests U+25FE BLACK MEDIUM SMALL SQUARE
242    # But I think this one looks better.
243    symbols=['▪'],  # U+25AA BLACK SMALL SQUARE
244    suffix=' ',
245)
246register_style(
247    'lower-latin',
248    type='override',
249    override='lower-alpha',
250)
251register_style(
252    'upper-latin',
253    type='override',
254    override='upper-alpha',
255)
256
257
258def format(value, counter_style):
259    """
260    Return a representation of ``value`` formatted by ``counter_style``
261    or one of its fallback.
262
263    The representation includes negative signs, but not the prefix and suffix.
264
265    """
266    if counter_style == 'none':
267        return ''
268    failed_styles = set()  # avoid fallback loops
269    while True:
270        if counter_style == 'decimal' or counter_style in failed_styles:
271            return str(value)
272        style = STYLES[counter_style]
273        low, high = style['range']
274        if low <= value <= high:
275            representation = style['formatter'](value)
276            if representation is not None:
277                return representation
278        failed_styles.add(counter_style)
279        counter_style = style['fallback']
280
281
282def format_list_marker(value, counter_style):
283    """
284    Return a representation of ``value`` formatted for a list marker.
285
286    This is the same as :func:`format()`, but includes the counter’s
287    prefix and suffix.
288    """
289    style = STYLES[counter_style]
290    return style['prefix'] + format(value, counter_style) + style['suffix']
291