| 1 | # -*- coding: iso-8859-1 -*- |
|---|
| 2 | # |
|---|
| 3 | # Copyright (C) 2011 Mikael Relbe <mikael@relbe.se> |
|---|
| 4 | # All rights reserved. |
|---|
| 5 | # |
|---|
| 6 | # This software is licensed as described in the file COPYING, which |
|---|
| 7 | # you should have received as part of this distribution. The terms |
|---|
| 8 | # are also available at http://trac.edgewall.com/license.html. |
|---|
| 9 | # |
|---|
| 10 | # Author: Mikael Relbe <mikael@relbe.se> |
|---|
| 11 | |
|---|
| 12 | """Use boxes to give wiki pages a modern look. |
|---|
| 13 | """ |
|---|
| 14 | |
|---|
| 15 | from pkg_resources import resource_filename |
|---|
| 16 | |
|---|
| 17 | from trac.util.html import html as tag |
|---|
| 18 | |
|---|
| 19 | from trac.config import BoolOption, IntOption |
|---|
| 20 | from trac.core import implements, Component, TracError |
|---|
| 21 | from trac.util.compat import cleandoc |
|---|
| 22 | from trac.util.translation import _ |
|---|
| 23 | from trac.web.api import IRequestFilter, IRequestHandler |
|---|
| 24 | from trac.web.chrome import ITemplateProvider, add_stylesheet |
|---|
| 25 | from trac.wiki import IWikiMacroProvider, format_to_html, parse_args |
|---|
| 26 | |
|---|
| 27 | from tracwikiextras.icons import Icons |
|---|
| 28 | from tracwikiextras.util import sanitize_attrib |
|---|
| 29 | |
|---|
| 30 | |
|---|
| 31 | # Urgency levels |
|---|
| 32 | WARN = 0 |
|---|
| 33 | HIGHLIGHT = 1 |
|---|
| 34 | ELABORATE = 2 |
|---|
| 35 | NEWS = 3 |
|---|
| 36 | NORMAL = 4 |
|---|
| 37 | |
|---|
| 38 | |
|---|
| 39 | class Boxes(Component): |
|---|
| 40 | """WikiProcessors for inserting boxes in a wiki page. |
|---|
| 41 | |
|---|
| 42 | Four processors are defined for creating boxes: |
|---|
| 43 | * `box` -- The core box processor. |
|---|
| 44 | * `rbox` (`lbox`) -- Display a right (left) aligned box to show |
|---|
| 45 | side notes and warnings etc. This will probably be the most |
|---|
| 46 | used box. |
|---|
| 47 | * `newsbox` -- Display news in a right aligned box. ''(This box |
|---|
| 48 | corresponds to the well-known ''`NewsFlash`'' macro.)'' |
|---|
| 49 | * `imagebox` -- Display a single image with caption in a centered box. |
|---|
| 50 | |
|---|
| 51 | The visual appearance of `box`, `rbox` and `lbox` is set by a |
|---|
| 52 | `type` parameter, which comes in a dozen or so flavors to call for |
|---|
| 53 | attention in an appropriate manner when displayed. Use the |
|---|
| 54 | `AboutWikiBoxes` macro for a demonstration. |
|---|
| 55 | |
|---|
| 56 | The width of right aligned boxes is adjustable and configured in the |
|---|
| 57 | `[wikiextras]` section in `trac.ini`. |
|---|
| 58 | |
|---|
| 59 | The visual appearance of content tables presented to the right, generated |
|---|
| 60 | by the built in `PageOutline` macro, is adjusted to be in line with these |
|---|
| 61 | boxes. The width of the content boxes can be set to coincide with the other |
|---|
| 62 | boxes, or be as narrow as possible. This is configured in the |
|---|
| 63 | `[wikiextras]` section in `trac.ini`. |
|---|
| 64 | |
|---|
| 65 | **The Icons component must be activated** since warning boxes and the like |
|---|
| 66 | uses the icon library. |
|---|
| 67 | """ |
|---|
| 68 | |
|---|
| 69 | implements(IRequestFilter, IRequestHandler, ITemplateProvider, |
|---|
| 70 | IWikiMacroProvider) |
|---|
| 71 | |
|---|
| 72 | rbox_width = IntOption('wikiextras', 'rbox_width', 300, |
|---|
| 73 | "Width of right aligned boxes.") |
|---|
| 74 | lbox_width = IntOption('wikiextras', 'lbox_width', 300, |
|---|
| 75 | """Width of left aligned boxes (defaults to |
|---|
| 76 | `rbox_width`).""") |
|---|
| 77 | wide_toc = BoolOption('wikiextras', 'wide_toc', 'false', |
|---|
| 78 | """Right aligned boxes with table of contents, |
|---|
| 79 | produced by the `PageOutline` macro, are either |
|---|
| 80 | as wide as ordinary right aligned boxes (`true`) or |
|---|
| 81 | narrow (`false`).""") |
|---|
| 82 | |
|---|
| 83 | shadowless = BoolOption('wikiextras', 'shadowless_boxes', 'false', |
|---|
| 84 | "Use shadowless boxes.") |
|---|
| 85 | |
|---|
| 86 | urgency_label = [(WARN, "warn"), (HIGHLIGHT, "highlight"), |
|---|
| 87 | (ELABORATE, "elaborate"), (NEWS, "news"), |
|---|
| 88 | (NORMAL, "normal")] |
|---|
| 89 | |
|---|
| 90 | urgency_bg = {WARN: 'red', HIGHLIGHT: 'yellow', ELABORATE: 'blue', |
|---|
| 91 | NEWS: 'green', NORMAL: 'white'} |
|---|
| 92 | |
|---|
| 93 | # map <type> -> (<urgency>, <icon name>, [<synonym>] |
|---|
| 94 | types = {'comment': (ELABORATE, 'comment', []), |
|---|
| 95 | 'configure': (NORMAL, 'configure', ['configuration', 'tool']), |
|---|
| 96 | 'details': (NORMAL, 'details', ['look', 'magnifier']), |
|---|
| 97 | 'discussion': (ELABORATE, 'discussion', ['chat', 'talk']), |
|---|
| 98 | 'information': (HIGHLIGHT, 'information', ['note']), |
|---|
| 99 | 'news': (NEWS, None, []), |
|---|
| 100 | 'nok': (ELABORATE, 'nok', ['bad', 'no']), |
|---|
| 101 | 'ok': (ELABORATE, 'ok', ['good', 'yes']), |
|---|
| 102 | 'question': (HIGHLIGHT, 'question', ['help']), |
|---|
| 103 | 'stop': (WARN, 'stop', ['critical']), |
|---|
| 104 | 'tips': (HIGHLIGHT, 'tips', []), |
|---|
| 105 | 'warning': (WARN, 'warning', ['bug', 'error', 'important']), |
|---|
| 106 | } |
|---|
| 107 | |
|---|
| 108 | def __init__(self): |
|---|
| 109 | self.word2type = {} |
|---|
| 110 | for name, data in self.types.iteritems(): |
|---|
| 111 | self.word2type[name] = name |
|---|
| 112 | for synonym in data[2]: |
|---|
| 113 | self.word2type[synonym] = name |
|---|
| 114 | |
|---|
| 115 | # IRequestFilter methods |
|---|
| 116 | |
|---|
| 117 | #noinspection PyUnusedLocal |
|---|
| 118 | def pre_process_request(self, req, handler): |
|---|
| 119 | return handler |
|---|
| 120 | |
|---|
| 121 | def post_process_request(self, req, template, data, content_type): |
|---|
| 122 | add_stylesheet(req, 'wikiextras/css/boxes.css') |
|---|
| 123 | add_stylesheet(req, '/wikiextras/dynamicboxes.css') |
|---|
| 124 | if self.shadowless: |
|---|
| 125 | add_stylesheet(req, 'wikiextras/css/boxes-shadowless.css') |
|---|
| 126 | return template, data, content_type |
|---|
| 127 | |
|---|
| 128 | # IRequestHandler methods |
|---|
| 129 | |
|---|
| 130 | def match_request(self, req): |
|---|
| 131 | return req.path_info == '/wikiextras/dynamicboxes.css' |
|---|
| 132 | |
|---|
| 133 | def process_request(self, req): |
|---|
| 134 | csstext = ('.wikiextras.box.right { width: %dpx; }\n' |
|---|
| 135 | '.wikiextras.box.icon.center, ' |
|---|
| 136 | '.wikiextras.box.icon.right { width: %dpx; }\n' % |
|---|
| 137 | (self.rbox_width - 22, self.rbox_width - 57)) |
|---|
| 138 | if self.wide_toc: |
|---|
| 139 | csstext += ('.wiki-toc { width: %dpx !important; }\n' % |
|---|
| 140 | (self.rbox_width - 22)) |
|---|
| 141 | else: |
|---|
| 142 | csstext += '.wiki-toc { width: auto !important; }\n' |
|---|
| 143 | |
|---|
| 144 | req.send(csstext, 'text/css', status=200) |
|---|
| 145 | |
|---|
| 146 | return None |
|---|
| 147 | |
|---|
| 148 | # ITemplateProvider methods |
|---|
| 149 | |
|---|
| 150 | def get_htdocs_dirs(self): |
|---|
| 151 | return [('wikiextras', resource_filename(__name__, 'htdocs'))] |
|---|
| 152 | |
|---|
| 153 | def get_templates_dirs(self): |
|---|
| 154 | return [] |
|---|
| 155 | |
|---|
| 156 | # IWikiMacroProvider methods |
|---|
| 157 | |
|---|
| 158 | def get_macros(self): |
|---|
| 159 | yield 'box' |
|---|
| 160 | yield 'rbox' |
|---|
| 161 | yield 'lbox' |
|---|
| 162 | yield 'newsbox' |
|---|
| 163 | yield 'imagebox' |
|---|
| 164 | |
|---|
| 165 | def _get_type_description(self, line_prefix=''): |
|---|
| 166 | urgency = {} # {'urgency': ('color', ["type -words"])} |
|---|
| 167 | # color |
|---|
| 168 | for u, color in self.urgency_bg.iteritems(): |
|---|
| 169 | urgency[u] = (color, []) |
|---|
| 170 | # words |
|---|
| 171 | for type, data in self.types.iteritems(): |
|---|
| 172 | urg, icon, words = data |
|---|
| 173 | urgency[urg][1].append(type) |
|---|
| 174 | for w in words: |
|---|
| 175 | urgency[urg][1].append(w) |
|---|
| 176 | descr = ["%s||= Urgency ''(box color)'' =||= type =||" % line_prefix] |
|---|
| 177 | for urg, label in self.urgency_label: |
|---|
| 178 | data = urgency[urg] |
|---|
| 179 | color = data[0] |
|---|
| 180 | words = data[1] |
|---|
| 181 | words.sort() |
|---|
| 182 | descr.append("%s||= %s ''(%s)'' =|| %s ||" % |
|---|
| 183 | (line_prefix, label, color, |
|---|
| 184 | ', '.join('`%s`' % w for w in words))) |
|---|
| 185 | return '\n'.join(descr) |
|---|
| 186 | |
|---|
| 187 | #noinspection PyUnusedLocal |
|---|
| 188 | def get_macro_description(self, name): |
|---|
| 189 | if name == 'box': |
|---|
| 190 | return cleandoc("""\ |
|---|
| 191 | View wiki text in a box. |
|---|
| 192 | |
|---|
| 193 | Syntax: |
|---|
| 194 | {{{ |
|---|
| 195 | {{{#!box type align=... width=... |
|---|
| 196 | wiki text |
|---|
| 197 | }}} |
|---|
| 198 | }}} |
|---|
| 199 | or preferably when content is short: |
|---|
| 200 | {{{ |
|---|
| 201 | [[box(wiki text, type=..., align=..., width=...)]] |
|---|
| 202 | }}} |
|---|
| 203 | where |
|---|
| 204 | * `type` is an optional flag, or parameter, to call for |
|---|
| 205 | attention depending on type of matter. When `type` is set, |
|---|
| 206 | the box is decorated with an icon (except for `news`) and |
|---|
| 207 | colored, depending on what ''urgency'' the type represents: |
|---|
| 208 | %s |
|---|
| 209 | `type` may be abbreviated as long as the abbreviation is |
|---|
| 210 | unique for one of the keywords above. |
|---|
| 211 | * `align` is optionally one of `right`, `left` or `center`. |
|---|
| 212 | The `rbox` macro is an alias for `align=right`. |
|---|
| 213 | * `width` is optional and sets the width of the box (defaults |
|---|
| 214 | `auto` except for right aligned boxes which defaults a fixed |
|---|
| 215 | width). `width` should be set when `align=center` for |
|---|
| 216 | proper results. |
|---|
| 217 | |
|---|
| 218 | Examples: |
|---|
| 219 | {{{ |
|---|
| 220 | {{{#!box warn |
|---|
| 221 | = Warning |
|---|
| 222 | Beware of the bugs |
|---|
| 223 | }}} |
|---|
| 224 | |
|---|
| 225 | [[box(Beware of the bugs, type=warn)]] |
|---|
| 226 | }}} |
|---|
| 227 | |
|---|
| 228 | A `style` parameter is also accepted, to allow for custom |
|---|
| 229 | styling of the box. See also the `rbox`, `newsbox` and |
|---|
| 230 | `imagebox` macros (processors). |
|---|
| 231 | """) % self._get_type_description(' ' * 5) |
|---|
| 232 | elif name in ('rbox', 'lbox'): |
|---|
| 233 | return cleandoc("""\ |
|---|
| 234 | |
|---|
| 235 | View a %(direction)s-aligned box. (This is a shorthand for |
|---|
| 236 | `box align=%(direction)s`) |
|---|
| 237 | |
|---|
| 238 | Syntax: |
|---|
| 239 | {{{ |
|---|
| 240 | {{{#!%(name)s type width=... |
|---|
| 241 | wiki text |
|---|
| 242 | }}} |
|---|
| 243 | }}} |
|---|
| 244 | or preferably when content is short: |
|---|
| 245 | {{{ |
|---|
| 246 | [[%(name)s(wiki text, type=..., width=...)]] |
|---|
| 247 | }}} |
|---|
| 248 | where |
|---|
| 249 | * `type` is an optional flag, or parameter, to call for |
|---|
| 250 | attention depending on type of matter. When `type` is set, |
|---|
| 251 | the box is decorated with an icon (except for `news`) and |
|---|
| 252 | colored, depending on what ''urgency'' the type represents: |
|---|
| 253 | %(type_description)s |
|---|
| 254 | `type` may be abbreviated as long as the abbreviation is |
|---|
| 255 | unique for one of the keywords above. |
|---|
| 256 | * `width` is optional and sets the width of the box (defaults |
|---|
| 257 | a fixed width). Use `width=auto` for an automatically sized |
|---|
| 258 | box. |
|---|
| 259 | |
|---|
| 260 | Examples: |
|---|
| 261 | {{{ |
|---|
| 262 | {{{#!%(name)s warn |
|---|
| 263 | = Warning |
|---|
| 264 | Beware of the bugs |
|---|
| 265 | }}} |
|---|
| 266 | |
|---|
| 267 | [[%(name)s(Beware of the bugs, type=warn)]] |
|---|
| 268 | }}} |
|---|
| 269 | |
|---|
| 270 | A `style` parameter is also accepted, to allow for custom |
|---|
| 271 | styling of the box. See also the `box`, `newsbox` and |
|---|
| 272 | `imagebox` macros (processors). |
|---|
| 273 | """) % { |
|---|
| 274 | 'name': name, |
|---|
| 275 | 'direction': 'right' if name is 'rbox' else 'left', |
|---|
| 276 | 'type_description': self._get_type_description(' ' * 5), |
|---|
| 277 | } |
|---|
| 278 | elif name == 'newsbox': |
|---|
| 279 | return cleandoc("""\ |
|---|
| 280 | Present a news box to the right. (This is a shorthand for |
|---|
| 281 | `rbox news`) |
|---|
| 282 | |
|---|
| 283 | Syntax: |
|---|
| 284 | {{{ |
|---|
| 285 | {{{#!newsbox |
|---|
| 286 | wiki text |
|---|
| 287 | }}} |
|---|
| 288 | }}} |
|---|
| 289 | |
|---|
| 290 | The following parameters are also accepted: |
|---|
| 291 | * `width` -- Set the width of the box (defaults a fixed |
|---|
| 292 | width). |
|---|
| 293 | * `style` -- Custom styling of the box. |
|---|
| 294 | |
|---|
| 295 | See also the `box`, `rbox` and `imagebox` macros (processors). |
|---|
| 296 | ''(Comment: This box corresponds to the well-known |
|---|
| 297 | ''`NewsFlash`'' macro.)'' |
|---|
| 298 | """) |
|---|
| 299 | elif name == 'imagebox': |
|---|
| 300 | return cleandoc("""\ |
|---|
| 301 | Present a centered box suitable for one image. |
|---|
| 302 | |
|---|
| 303 | Syntax: |
|---|
| 304 | {{{ |
|---|
| 305 | {{{#!imagebox |
|---|
| 306 | wiki text |
|---|
| 307 | }}} |
|---|
| 308 | }}} |
|---|
| 309 | |
|---|
| 310 | This box is typically used together with the `Image` macro: |
|---|
| 311 | {{{ |
|---|
| 312 | {{{#!imagebox |
|---|
| 313 | [[Image(file)]] |
|---|
| 314 | |
|---|
| 315 | Caption |
|---|
| 316 | }}} |
|---|
| 317 | }}} |
|---|
| 318 | |
|---|
| 319 | Note that the `size` parameter of the `Image` macro may not |
|---|
| 320 | behave as expected when using relative sizes (`%`). |
|---|
| 321 | |
|---|
| 322 | The following parameters are also accepted: |
|---|
| 323 | * `align` -- One of `right`, `left` or `center` (defaults |
|---|
| 324 | `center`). |
|---|
| 325 | * `width` -- Set the width of the box (defaults `auto` except |
|---|
| 326 | for right aligned boxes which defaults a fixed width). |
|---|
| 327 | * `style` -- Custom styling of the box. |
|---|
| 328 | |
|---|
| 329 | See also the `box`, `rbox` and `newsbox` macros (processors). |
|---|
| 330 | """) |
|---|
| 331 | |
|---|
| 332 | def _get_type(self, word): |
|---|
| 333 | # Accept unique abbrevs. of type |
|---|
| 334 | if not word: |
|---|
| 335 | return '' |
|---|
| 336 | if word in self.word2type: |
|---|
| 337 | return self.word2type[word] |
|---|
| 338 | type_ = '' |
|---|
| 339 | for w in self.word2type.iterkeys(): |
|---|
| 340 | try: |
|---|
| 341 | if w.startswith(word): |
|---|
| 342 | t = self.word2type[w] |
|---|
| 343 | if type_ and type_ != t: |
|---|
| 344 | return # 2nd found, not unique |
|---|
| 345 | type_ = t |
|---|
| 346 | except TypeError as e: |
|---|
| 347 | raise TracError(_("Invalid argument %(arg)s (%(type)s)", |
|---|
| 348 | arg=word, type=type(word))) |
|---|
| 349 | return type_ |
|---|
| 350 | |
|---|
| 351 | def _has_icon(self, type): |
|---|
| 352 | if type in self.types: |
|---|
| 353 | return self.types[type][1] is not None |
|---|
| 354 | |
|---|
| 355 | #noinspection PyUnusedLocal |
|---|
| 356 | def expand_macro(self, formatter, name, content, args=None): |
|---|
| 357 | class_list = ['wikiextras', 'box'] |
|---|
| 358 | style_list = [] |
|---|
| 359 | if args is None: |
|---|
| 360 | content, args = parse_args(content) |
|---|
| 361 | |
|---|
| 362 | #noinspection PyArgumentList |
|---|
| 363 | if not Icons(self.env).shadowless: |
|---|
| 364 | class_list.append('shadow') |
|---|
| 365 | |
|---|
| 366 | class_arg = args.get('class', '') |
|---|
| 367 | if class_arg: |
|---|
| 368 | class_list.append(class_arg) |
|---|
| 369 | |
|---|
| 370 | align = ('right' if name in ('newsbox', 'rbox') else |
|---|
| 371 | 'center' if name == 'imagebox' else |
|---|
| 372 | 'left' if name == 'lbox' else |
|---|
| 373 | '') |
|---|
| 374 | align = args.get('align', align) |
|---|
| 375 | if align: |
|---|
| 376 | class_list.append(align) |
|---|
| 377 | |
|---|
| 378 | if name == 'newsbox': |
|---|
| 379 | type = 'news' |
|---|
| 380 | elif name == 'imagebox': |
|---|
| 381 | type = 'image' |
|---|
| 382 | else: |
|---|
| 383 | type = args.get('type') |
|---|
| 384 | if not type: |
|---|
| 385 | for flag, value in args.iteritems(): |
|---|
| 386 | if value is True: |
|---|
| 387 | type = flag |
|---|
| 388 | break |
|---|
| 389 | type = self._get_type(type) |
|---|
| 390 | if type in self.types: |
|---|
| 391 | td = self.types[type] # type data |
|---|
| 392 | if td[1]: #icon |
|---|
| 393 | class_list += ['icon', td[1]] |
|---|
| 394 | else: |
|---|
| 395 | class_list.append(type) |
|---|
| 396 | bg = self.urgency_bg.get(td[0]) |
|---|
| 397 | if bg: |
|---|
| 398 | class_list.append(bg) |
|---|
| 399 | del td |
|---|
| 400 | elif type: |
|---|
| 401 | class_list.append(type) |
|---|
| 402 | |
|---|
| 403 | style = args.get('style', '') |
|---|
| 404 | if style: |
|---|
| 405 | style_list.append(style) |
|---|
| 406 | |
|---|
| 407 | width = args.get('width', '') |
|---|
| 408 | if width: |
|---|
| 409 | if width.isdigit(): |
|---|
| 410 | width = '%spx' % width |
|---|
| 411 | if width.endswith('px'): |
|---|
| 412 | # compensate for padding |
|---|
| 413 | if self._has_icon(type): |
|---|
| 414 | width = '%dpx' % (int(width[:-2]) - 57) |
|---|
| 415 | else: |
|---|
| 416 | width = '%dpx' % (int(width[:-2]) - 22) |
|---|
| 417 | style_list.append('width:%s' % width) |
|---|
| 418 | |
|---|
| 419 | html = format_to_html(self.env, formatter.context, content) |
|---|
| 420 | class_ = ' '.join(class_list) |
|---|
| 421 | style = ';'.join(style_list) |
|---|
| 422 | div = sanitize_attrib(self.env, tag.div(class_=class_, style=style)) |
|---|
| 423 | div(html) |
|---|
| 424 | return div |
|---|
| 425 | |
|---|
| 426 | |
|---|
| 427 | class AboutWikiBoxes(Component): |
|---|
| 428 | """Macro for displaying a wiki page on how to use boxes. |
|---|
| 429 | |
|---|
| 430 | Create a wiki page `WikiBoxes` and insert the following line to show |
|---|
| 431 | detailed instructions to wiki authors on how to use boxes in wiki pages: |
|---|
| 432 | {{{ |
|---|
| 433 | [[AboutWikiBoxes]] |
|---|
| 434 | }}} |
|---|
| 435 | """ |
|---|
| 436 | |
|---|
| 437 | implements(IWikiMacroProvider) |
|---|
| 438 | |
|---|
| 439 | # IWikiMacroProvider methods |
|---|
| 440 | |
|---|
| 441 | def get_macros(self): |
|---|
| 442 | yield 'AboutWikiBoxes' |
|---|
| 443 | |
|---|
| 444 | #noinspection PyUnusedLocal |
|---|
| 445 | def get_macro_description(self, name): |
|---|
| 446 | return "Display a wiki page on how to use boxes." |
|---|
| 447 | |
|---|
| 448 | #noinspection PyUnusedLocal |
|---|
| 449 | def expand_macro(self, formatter, name, content, args=None): |
|---|
| 450 | help_file = resource_filename(__name__, 'doc/WikiBoxes') |
|---|
| 451 | fd = open(help_file, 'r') |
|---|
| 452 | wiki_text = fd.read() |
|---|
| 453 | fd.close() |
|---|
| 454 | return format_to_html(self.env, formatter.context, wiki_text) |
|---|