source: wikiextrasplugin/tags/1.3.1/tracwikiextras/boxes.py

Last change on this file was 17033, checked in by Ryan J Ollos, 6 years ago

TracWikiExtras 1.3.1dev: Make imports compatible with Trac < 1.0.2

Refs #13374.

  • Property svn:eol-style set to native
  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 16.1 KB
Line 
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
15from pkg_resources import resource_filename
16
17from trac.util.html import html as tag
18
19from trac.config import BoolOption, IntOption
20from trac.core import implements, Component, TracError
21from trac.util.compat import cleandoc
22from trac.util.translation import _
23from trac.web.api import IRequestFilter, IRequestHandler
24from trac.web.chrome import ITemplateProvider, add_stylesheet
25from trac.wiki import IWikiMacroProvider, format_to_html, parse_args
26
27from tracwikiextras.icons import Icons
28from tracwikiextras.util import sanitize_attrib
29
30
31# Urgency levels
32WARN = 0
33HIGHLIGHT = 1
34ELABORATE = 2
35NEWS = 3
36NORMAL = 4
37
38
39class 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
427class 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)
Note: See TracBrowser for help on using the repository browser.