| 1 | # -*- coding: iso-8859-1 -*- |
|---|
| 2 | # |
|---|
| 3 | # Copyright (C) 2011 Mikael Relbe <mikael@relbe.se> |
|---|
| 4 | # All rights reserved. |
|---|
| 5 | # |
|---|
| 6 | # Copyright (C) 2006 Christian Boos <cboos@neuf.fr> |
|---|
| 7 | # All rights reserved. |
|---|
| 8 | # |
|---|
| 9 | # This software is licensed as described in the file COPYING, which |
|---|
| 10 | # you should have received as part of this distribution. The terms |
|---|
| 11 | # are also available at http://trac.edgewall.com/license.html. |
|---|
| 12 | # |
|---|
| 13 | # Author: Christian Boos <cboos@neuf.fr> |
|---|
| 14 | # Mikael Relbe <mikael@relbe.se> |
|---|
| 15 | |
|---|
| 16 | import re |
|---|
| 17 | import string |
|---|
| 18 | |
|---|
| 19 | from trac.util.html import Markup, html as tag |
|---|
| 20 | |
|---|
| 21 | from trac.util import arity |
|---|
| 22 | from trac.util.compat import sorted |
|---|
| 23 | from trac.util.html import TracHTMLSanitizer |
|---|
| 24 | if hasattr(TracHTMLSanitizer, 'sanitize_attrs'): |
|---|
| 25 | sanitizer = TracHTMLSanitizer() |
|---|
| 26 | from trac.util.html import Element |
|---|
| 27 | else: |
|---|
| 28 | sanitizer = None |
|---|
| 29 | from genshi.builder import Stream |
|---|
| 30 | from trac.wiki.api import WikiSystem |
|---|
| 31 | |
|---|
| 32 | |
|---|
| 33 | def prepare_regexp(d): |
|---|
| 34 | syms = d.keys() |
|---|
| 35 | syms.sort(lambda a, b: cmp(len(b), len(a))) |
|---|
| 36 | return "|".join([r'%s%s%s' |
|---|
| 37 | % (r'\b' if re.match(r'\w', s[0]) else '', |
|---|
| 38 | re.escape(s), |
|---|
| 39 | r'\b' if re.match(r'\w', s[-1]) else '') |
|---|
| 40 | for s in syms]) |
|---|
| 41 | |
|---|
| 42 | |
|---|
| 43 | def render_table(items, colspec, render_item, colspace=1): |
|---|
| 44 | try: |
|---|
| 45 | columns = max(int(colspec), 1) |
|---|
| 46 | except Exception: |
|---|
| 47 | columns = 3 |
|---|
| 48 | |
|---|
| 49 | nbsp = Markup(' ') |
|---|
| 50 | # hdr = [tag.th(nbsp, 'Display'), tag.th('Markup', nbsp)] |
|---|
| 51 | spacer_style = 'width:%dem;border:none' % colspace |
|---|
| 52 | # Find max width to make value cols equally wide |
|---|
| 53 | width = 0 |
|---|
| 54 | for i in items: |
|---|
| 55 | if (isinstance(i, str) or isinstance(i, unicode)) and len(i) > width: |
|---|
| 56 | width = len(i) |
|---|
| 57 | value_style = 'border:none' |
|---|
| 58 | #noinspection PyUnusedLocal |
|---|
| 59 | if width: |
|---|
| 60 | # empirical... |
|---|
| 61 | value_style = '%s;width:%dem' % (value_style, width*2/3) |
|---|
| 62 | |
|---|
| 63 | def render_def(s): |
|---|
| 64 | rendered = s and render_item(s) or None |
|---|
| 65 | if isinstance(s, str): |
|---|
| 66 | s = Markup(s.replace('&', '&')) |
|---|
| 67 | return [tag.td(rendered, nbsp, style='border:none'), |
|---|
| 68 | tag.td(tag.kbd(s), style=value_style)] |
|---|
| 69 | |
|---|
| 70 | return tag.table(#tag.tr((hdr + [tag.td(style=spacer_style)]) * |
|---|
| 71 | # (columns-1) + hdr), |
|---|
| 72 | [tag.tr([render_def(s) + [tag.td(style=spacer_style)] |
|---|
| 73 | for s in row[:-1]] + render_def(row[-1])) |
|---|
| 74 | for row in group_over(sorted(items), columns)], |
|---|
| 75 | class_='wiki', style='border:none') |
|---|
| 76 | |
|---|
| 77 | |
|---|
| 78 | def group_over(iterable, num, predicate=None): |
|---|
| 79 | """Combines the elements produced by the given iterable so that every `n` |
|---|
| 80 | items are returned as a tuple. |
|---|
| 81 | |
|---|
| 82 | >>> items = [1, 2, 3, 4] |
|---|
| 83 | >>> for item in group(items, 2): |
|---|
| 84 | ... print item |
|---|
| 85 | (1, 3) |
|---|
| 86 | (2, 4) |
|---|
| 87 | |
|---|
| 88 | The last tuple is padded with `None` values if its' length is smaller than |
|---|
| 89 | `num`. |
|---|
| 90 | |
|---|
| 91 | >>> items = [1, 2, 3, 4, 5] |
|---|
| 92 | >>> for item in group(items, 2): |
|---|
| 93 | ... print item |
|---|
| 94 | (1, 4) |
|---|
| 95 | (2, 5) |
|---|
| 96 | (3, None) |
|---|
| 97 | |
|---|
| 98 | The optional `predicate` parameter can be used to flag elements that should |
|---|
| 99 | not be packed together with other items. Only those elements where the |
|---|
| 100 | predicate function returns True are grouped with other elements, otherwise |
|---|
| 101 | they are returned as a tuple of length 1: |
|---|
| 102 | |
|---|
| 103 | >>> items = [1, 2, 3, 4, 5, 6, 7, 8, 9] |
|---|
| 104 | >>> for item in group(items, 2, lambda x: x != 4): |
|---|
| 105 | ... print item |
|---|
| 106 | (1, 3) |
|---|
| 107 | (2, None) |
|---|
| 108 | (4,) |
|---|
| 109 | (5, 8) |
|---|
| 110 | (6, 9) |
|---|
| 111 | (7, None) |
|---|
| 112 | """ |
|---|
| 113 | # This function was inspired by trac.util.presentation.group(). |
|---|
| 114 | num = max(num, 1) |
|---|
| 115 | # Split into sections at delimited by predicate |
|---|
| 116 | sections = [] # a list of lists of tuple (flush, [items]) |
|---|
| 117 | buf = [] |
|---|
| 118 | for item in iterable: |
|---|
| 119 | flush = predicate and not predicate(item) |
|---|
| 120 | if buf and flush: |
|---|
| 121 | sections.append((False, buf)) |
|---|
| 122 | buf = [] |
|---|
| 123 | if flush: |
|---|
| 124 | sections.append((True, [item])) |
|---|
| 125 | else: |
|---|
| 126 | buf.append(item) |
|---|
| 127 | if buf: |
|---|
| 128 | sections.append((False, buf)) |
|---|
| 129 | # Return section by section |
|---|
| 130 | buf = [] |
|---|
| 131 | for (flush, items) in sections: |
|---|
| 132 | if flush: |
|---|
| 133 | yield tuple(items) |
|---|
| 134 | else: |
|---|
| 135 | # iterate rows |
|---|
| 136 | rows = len(items) // num |
|---|
| 137 | if len(items) % num: |
|---|
| 138 | rows += 1 |
|---|
| 139 | for r in range(0, rows): |
|---|
| 140 | # build column |
|---|
| 141 | for c in range(0, num): |
|---|
| 142 | p = r + c * rows |
|---|
| 143 | if p < len(items): |
|---|
| 144 | buf.append(items[p]) |
|---|
| 145 | else: |
|---|
| 146 | buf.append(None) |
|---|
| 147 | yield tuple(buf) |
|---|
| 148 | del buf[:] |
|---|
| 149 | |
|---|
| 150 | |
|---|
| 151 | def reduce_names(names, keep=40): |
|---|
| 152 | """Reduce the number of names in an alphabetically balanced manner. |
|---|
| 153 | |
|---|
| 154 | The reduction is made on a letter-by-letter basis with the goal to keep a |
|---|
| 155 | balanced number of words with different letters on each position. Longer |
|---|
| 156 | words are removed before shorter, letters between lowest and highest |
|---|
| 157 | order are removed first. |
|---|
| 158 | |
|---|
| 159 | :param names: List of strings. |
|---|
| 160 | :param keep: Max number of strings to keep. |
|---|
| 161 | |
|---|
| 162 | :ret: A reduced, alphabetically balanced, list of strings. |
|---|
| 163 | """ |
|---|
| 164 | if len(names) < keep: |
|---|
| 165 | return names |
|---|
| 166 | elif keep < 1: |
|---|
| 167 | return [] |
|---|
| 168 | |
|---|
| 169 | def build_tree(names): |
|---|
| 170 | """Create a Huffman tree based on names |
|---|
| 171 | |
|---|
| 172 | The tree is a dict where keys denotes a letter in the name, and the |
|---|
| 173 | value is a node in the tree: |
|---|
| 174 | tree :== {char: [count, tree] or {None: None} when leaf |
|---|
| 175 | where count is the number of leafs in the sub tree. |
|---|
| 176 | """ |
|---|
| 177 | def insert(letters, tree): |
|---|
| 178 | if letters: |
|---|
| 179 | key = letters[0] |
|---|
| 180 | if key in tree: |
|---|
| 181 | # add tail to sub tree |
|---|
| 182 | value = tree[key] |
|---|
| 183 | subtree = value[1] |
|---|
| 184 | if insert(letters[1:], subtree): |
|---|
| 185 | value[0] += 1 # increase count |
|---|
| 186 | return True |
|---|
| 187 | else: |
|---|
| 188 | # insert into new sub tree |
|---|
| 189 | subtree = {} |
|---|
| 190 | insert(letters[1:], subtree) |
|---|
| 191 | tree[key] = [1, subtree] |
|---|
| 192 | return True |
|---|
| 193 | elif not None in tree: |
|---|
| 194 | # end of word |
|---|
| 195 | tree[None] = None |
|---|
| 196 | return True |
|---|
| 197 | |
|---|
| 198 | # build_tree |
|---|
| 199 | tree = {} |
|---|
| 200 | for name in names: |
|---|
| 201 | insert(name, tree) |
|---|
| 202 | return tree |
|---|
| 203 | |
|---|
| 204 | def remove_one(tree): |
|---|
| 205 | """Remove one leaf from the largest child node in the tree.""" |
|---|
| 206 | c = 0 # longest count |
|---|
| 207 | chars = [] # candidate chars to remove |
|---|
| 208 | keys = tree.keys() |
|---|
| 209 | keys.sort() |
|---|
| 210 | # search candidate chars to remove |
|---|
| 211 | for key in keys: |
|---|
| 212 | if tree[key]: |
|---|
| 213 | count = tree[key][0] |
|---|
| 214 | if count > c: |
|---|
| 215 | c = count |
|---|
| 216 | chars = [key] |
|---|
| 217 | elif count == c: |
|---|
| 218 | chars.append(key) |
|---|
| 219 | # remove |
|---|
| 220 | char = None |
|---|
| 221 | if chars: |
|---|
| 222 | # look for highest-valued punctuation |
|---|
| 223 | for c in reversed(chars): |
|---|
| 224 | if c in string.punctuation: |
|---|
| 225 | char = c |
|---|
| 226 | break |
|---|
| 227 | else: |
|---|
| 228 | # remove middle char |
|---|
| 229 | char = chars[len(chars) // 2] |
|---|
| 230 | if char is None: |
|---|
| 231 | del tree[None] |
|---|
| 232 | else: |
|---|
| 233 | remove_one(tree[char][1]) # subtree |
|---|
| 234 | tree[char][0] -= 1 |
|---|
| 235 | if not tree[char][0]: |
|---|
| 236 | del tree[char] |
|---|
| 237 | |
|---|
| 238 | def size_of(tree): |
|---|
| 239 | """Return the number of leafs in the tree""" |
|---|
| 240 | n = 0 |
|---|
| 241 | for value in tree.itervalues(): |
|---|
| 242 | if value: |
|---|
| 243 | n += value[0] |
|---|
| 244 | return n |
|---|
| 245 | |
|---|
| 246 | def get_names(tree, letters='', buf=None): |
|---|
| 247 | """Return a list with names based on Huffman tree""" |
|---|
| 248 | if buf is None: |
|---|
| 249 | buf=[] |
|---|
| 250 | for key, value in tree.iteritems(): |
|---|
| 251 | if key is None: |
|---|
| 252 | buf.append(letters) |
|---|
| 253 | else: |
|---|
| 254 | subtree = value[1] |
|---|
| 255 | get_names(subtree, letters+key, buf) |
|---|
| 256 | return buf |
|---|
| 257 | |
|---|
| 258 | # reduce_names |
|---|
| 259 | tree = build_tree(names) |
|---|
| 260 | while size_of(tree) > keep: |
|---|
| 261 | remove_one(tree) |
|---|
| 262 | return get_names(tree) |
|---|
| 263 | |
|---|
| 264 | |
|---|
| 265 | if sanitizer: |
|---|
| 266 | def sanitize_attrib(env, element): |
|---|
| 267 | if not WikiSystem(env).render_unsafe_content: |
|---|
| 268 | if arity(sanitizer.sanitize_attrs) == 1: |
|---|
| 269 | sanitized = sanitizer.sanitize_attrs(element.attrib) |
|---|
| 270 | else: # Trac 1.3.2+ |
|---|
| 271 | sanitized = sanitizer.sanitize_attrs(element.tag, |
|---|
| 272 | element.attrib) |
|---|
| 273 | element = Element(element.tag, **sanitized) |
|---|
| 274 | return element |
|---|
| 275 | else: |
|---|
| 276 | def sanitize_attrib(env, element): |
|---|
| 277 | if not WikiSystem(env).render_unsafe_content: |
|---|
| 278 | sanitized = getattr(tag, element.tag.localname) |
|---|
| 279 | for k, data, pos in (Stream(element) | TracHTMLSanitizer()): |
|---|
| 280 | sanitized.attrib = data[1] |
|---|
| 281 | break # only look at START |
|---|
| 282 | element = sanitized |
|---|
| 283 | return element |
|---|