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