source: wikiextrasplugin/tags/1.3.1/tracwikiextras/icons.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: 19.6 KB
Line 
1# -*- coding: iso-8859-1 -*-
2#
3# Copyright (C) 2011 Mikael Relbe <mikael@relbe.se>
4# All rights reserved.
5#
6# Parts of this code (smileys) are
7# Copyright (C) 2006 Christian Boos <cboos@neuf.fr>
8# All rights reserved.
9#
10# This software is licensed as described in the file COPYING, which
11# you should have received as part of this distribution. The terms
12# are also available at http://trac.edgewall.com/license.html.
13#
14# Author: Christian Boos <cboos@neuf.fr>
15#         Mikael Relbe <mikael@relbe.se>
16
17"""Decorate wiki pages with a huge set of modern icons.
18
19More than 3.000 icons are available using the wiki markup `(|name, size|)`, or
20the equivalent `Icon` macro, and as smileys. Smiley character sequences are
21defined in the `[wikiextras-smileys]` section in `trac.ini`. The icon library
22contains two sets of icons, shadowed and shadowless. Which set to use is
23defined in the `[wikiextras]` section in `trac.ini`.
24
25'''Icon Library License Terms'''
26
27The icon library contained herein is composed of the
28[http://p.yusukekamiyamane.com Fugue icon library] with additional icons, and
29can be used for any commercial or personal projects, but you may not lease,
30license or sublicense the icons.
31
32The [http://p.yusukekamiyamane.com Fugue icon library] is released under
33[http://creativecommons.org/licenses/by/3.0/ Creative Commons Attribution 3.0 license].
34[[BR]]
35Some icons are Copyright (C) [http://p.yusukekamiyamane.com/ Yusuke Kamiyamane].
36All rights reserved.
37
38Additional icons are released under same
39[http://trac.edgewall.org/wiki/TracLicense license terms] as Trac.
40[[BR]]
41Some icons are Copyright (C) [http://www.edgewall.org Edgewall Software].
42All rights reserved.
43"""
44
45import fnmatch
46import os
47import re
48
49from pkg_resources import resource_filename
50
51from trac.util.html import html as tag
52
53from trac.config import BoolOption, ConfigSection, IntOption, ListOption
54from trac.core import implements, Component
55from trac.util.compat import cleandoc
56from trac.web.chrome import ITemplateProvider
57from trac.wiki import IWikiMacroProvider, IWikiSyntaxProvider, format_to_html
58
59from tracwikiextras.util import prepare_regexp, reduce_names, render_table
60
61
62SIZE_DESCR = {'S': 'small', 'M': 'medium-sized', 'L': 'large'}
63
64FUGUE_ICONS = {
65    False: { # with shadow
66        'S': ('wikiextras-icons-16',
67              resource_filename(__name__, 'htdocs/icons/fugue/icons')),
68        'M': ('wikiextras-icons-24',
69              resource_filename(__name__,
70                                'htdocs/icons/fugue/bonus/icons-24')),
71        'L': ('wikiextras-icons-32',
72              resource_filename(__name__,
73                                'htdocs/icons/fugue/bonus/icons-32')),
74    },
75    True: { # shadowless
76        'S': ('wikiextras-icons-shadowless-16',
77              resource_filename(__name__,
78                                'htdocs/icons/fugue/icons-shadowless')),
79        'M': ('wikiextras-icons-shadowless-24',
80              resource_filename(
81                  __name__, 'htdocs/icons/fugue/bonus/icons-shadowless-24')),
82        'L': ('wikiextras-icons-shadowless-32',
83              resource_filename(
84                  __name__, 'htdocs/icons/fugue/bonus/icons-shadowless-32')),
85    },
86}
87
88
89
90class Icons(Component):
91    """Display icons in lined with text.
92
93    The wiki markup `(|name|)`, or the equivalent `Icon` macro, shows a named
94    icon that can be in line with text. During side-by-side wiki editing, the
95    same wiki markup, or macro, can be used as a temporary search facility to
96    find icons in the vast library. The number of icons presented to the wiki
97    author can be limited to prevent exhaustive network traffic. This limit is
98    defined in the `[wikiextras]` section in `trac.ini`.
99    """
100
101    implements(ITemplateProvider, IWikiMacroProvider, IWikiSyntaxProvider)
102
103    icon_limit = IntOption('wikiextras', 'icon_limit', 32,
104        """To prevent exhaustive network traffic, limit the maximum number of
105        icons generated by the macro `Icon`. Set to 0 for unlimited number of
106        icons (this will produce exhaustive network traffic--you have been
107        warned!)""")
108
109    shadowless = BoolOption('wikiextras', 'shadowless_icons', 'false',
110                            'Use shadowless icons.')
111
112    def icon_location(self, size='S'):
113        """ Returns `(prefix, abspath)` tuple based on `size` which is one
114        of `small`, `medium` or `large` (or an abbreviation thereof..
115
116        The `prefix` part defines the path in the URL that requests to these
117        resources are prefixed with.
118
119        The `abspath` is the absolute path to the directory containing the
120        resources on the local file system.
121        """
122        try:
123            return FUGUE_ICONS[self.shadowless][size[0].upper()]
124        except Exception:
125            return FUGUE_ICONS[self.shadowless]['S']
126
127    def _render_icon(self, formatter, name, size):
128        if not name:
129            return
130        size = size.upper()[0] if size else 'S'
131        name = name.lower()
132        if any(x in name for x in ['*', '?']):
133            #noinspection PyArgumentList
134            return ShowIcons(self.env)._render(
135                    formatter, 2, name, size, True, self.icon_limit)
136        else:
137            loc = self.icon_location(size)
138            return tag.img(src=formatter.href.chrome(loc[0], '%s.png' % name),
139                           alt=name, style="vertical-align: text-bottom")
140
141    # ITemplateProvider methods
142
143    def get_htdocs_dirs(self):
144        dirs = []
145        for data in FUGUE_ICONS.itervalues():
146            for d in data.itervalues():
147                dirs.append(tuple(d))
148        return dirs
149
150    def get_templates_dirs(self):
151        return []
152
153    # IWikiSyntaxProvider methods
154
155    wiki_pat = r'!?\(\|([-*?._a-z0-9]+)(?:,\s*(\w*))?\|\)'
156    wiki_re = re.compile(wiki_pat)
157
158    #noinspection PyUnusedLocal
159    def _format_icon(self, formatter, match, fullmatch=None):
160        m = Icons.wiki_re.match(match)
161        name, size = m.group(1, 2)
162        return self._render_icon(formatter, name, size)
163
164    def get_wiki_syntax(self):
165        yield (Icons.wiki_pat, self._format_icon)
166
167    def get_link_resolvers(self):
168        return []
169
170    # IWikiMacroProvider methods
171
172    def get_macros(self):
173        yield 'Icon'
174
175    #noinspection PyUnusedLocal
176    def get_macro_description(self, name):
177        return cleandoc("""Shows a named icon that can be in line with text.
178
179                Syntax:
180                {{{
181                [[Icon(name, size)]]
182                }}}
183                where
184                 * `name` is the name of the icon.  When `name` contains a
185                   pattern character (`*` or `?`), a 2-column preview of
186                   matching icons is presented, which should mainly be used for
187                   finding and selecting an icon during wiki page editing in
188                   side-by-side mode (however, no more than %d icons are
189                   presented to prevent exhaustive network traffic.)
190                 * `size` is optionally one of `small`, `medium` or `large` or
191                   an abbreviation thereof (defaults `small`).
192
193                Example:
194                {{{
195                [[Icon(smiley)]]
196                }}}
197
198                Use `ShowIcons` for static presentation of available icons.
199                Smileys like `:-)` are automatically rendered as icons. Use
200                `ShowSmileys` to se all available smileys.
201
202                Following wiki markup is equivalent to using this macro:
203                {{{
204                (|name, size|)
205                }}}
206                """ % self.icon_limit)
207
208    #noinspection PyUnusedLocal
209    def expand_macro(self, formatter, name, content):
210        # content = name, size
211        if not content:
212            return
213        args = [a.strip() for a in content.split(',')] + [None, None]
214        name, size = args[0], args[1]
215        return self._render_icon(formatter, name, size)
216
217
218class ShowIcons(Component):
219    """Macro to list available icons on a wiki page.
220
221    The `ShowIcons` macro displays a table of available icons, matching a
222    search criteria. The number of presented icons can be limited to prevent
223    exhaustive network traffic. This limit is defined in the `[wikiextras]`
224    section in `trac.ini`.
225    """
226
227    implements(ITemplateProvider, IWikiMacroProvider)
228
229    showicons_limit = IntOption('wikiextras', 'showicons_limit', 96,
230        """To prevent exhaustive network traffic, limit the maximum number of
231        icons generated by the macro `ShowIcons`. Set to 0 for
232        unlimited number of icons (this will produce exhaustive network
233        traffic--you have been warned!)""")
234
235    def _render(self, formatter, cols, name_pat, size, header, limit):
236        #noinspection PyArgumentList
237        icon = Icons(self.env)
238        icon_dir = icon.icon_location(size)[1]
239        files = fnmatch.filter(os.listdir(icon_dir), '%s.png' % name_pat)
240        icon_names = [os.path.splitext(p)[0] for p in files]
241        if limit:
242            displayed_icon_names = reduce_names(icon_names, limit)
243        else:
244            displayed_icon_names = icon_names
245        icon_table = render_table(displayed_icon_names, cols,
246                                  lambda name: icon._render_icon(formatter,
247                                                                 name, size))
248        if not len(icon_names):
249            message = 'No %s icon matches %s' % (SIZE_DESCR[size], name_pat)
250        elif len(icon_names) == 1:
251            message = 'Showing the only %s icon matching %s' % \
252                      (SIZE_DESCR[size], name_pat)
253        elif len(displayed_icon_names) == len(icon_names):
254            message = 'Showing all %d %s icons matching %s' % \
255                      (len(icon_names), SIZE_DESCR[size], name_pat)
256        else:
257            message = 'Showing %d of %d %s icons matching %s' % \
258                      (len(displayed_icon_names), len(icon_names),
259                       SIZE_DESCR[size], name_pat)
260        return tag.div(tag.p(tag.small(message)) if header else '', icon_table)
261
262    # ITemplateProvider methods
263
264    def get_htdocs_dirs(self):
265        return []
266
267    def get_templates_dirs(self):
268        return []
269
270    # IWikiMacroProvider methods
271
272    def get_macros(self):
273        yield 'ShowIcons'
274
275    #noinspection PyUnusedLocal
276    def get_macro_description(self, name):
277        #noinspection PyStringFormat
278        return cleandoc("""Renders in a table a list of available icons.
279                No more than %(showicons_limit)d icons are displayed to prevent
280                exhaustive network traffic.
281
282                Syntax:
283                {{{
284                [[ShowIcons(cols, name-pattern, size, header, limit)]]
285                }}}
286                where
287                 * `cols` is optionally the number of columns in the table
288                   (defaults 3).
289                 * `name-pattern` selects which icons to list (use `*` and
290                   `?`).
291                 * `size` is optionally one of `small`, `medium` or `large` or
292                   an abbreviation thereof (defaults `small`).
293                 * `header` is optionally one of `header` and `noheader` or
294                   an abbreviation thereof (header is displayed by default)
295                 * `limit` specifies an optional upper limit of number of
296                   displayed icons (however, no more than %(showicons_limit)d
297                   will be displayed).
298
299                The last three optional parameters (`size`, `header` and
300                `limit`) can be stated in any order.
301
302                Example:
303
304                {{{
305                [[ShowIcons(smile*)]]              # all small icons matching smile*
306                [[ShowIcons(4, smile*)]]           # four columns
307                [[ShowIcons(smile*, 10)]]          # limit to 10 icons
308                [[ShowIcons(smile*, 10, nohead)]]  # no header
309                [[ShowIcons(smile*, m)]]           # medium-size
310                }}}
311                """ % {'showicons_limit': self.showicons_limit})
312
313    #noinspection PyUnusedLocal
314    def expand_macro(self, formatter, name, content, args=None):
315        # content = cols, name-pattern, size, header, limit
316        args = []
317        if content:
318            args = [a.strip() for a in content.split(',')]
319        args += [''] * 2
320        a = args.pop(0)
321        # cols
322        if a.isdigit():
323            cols = max(int(a), 1)
324            a = args.pop(0)
325        else:
326            cols = 3
327        # name_pat
328        name_pat = a
329        if not name_pat:
330            name_pat = '*'
331        # size, header and limit
332        size = 'S'
333        header = True
334        limit = self.showicons_limit
335        while args:
336            a = args.pop(0).lower()
337            if a.isdigit():
338                limit = min(int(a), limit)
339            elif a and any(d.startswith(a) for d in SIZE_DESCR.values()):
340                size = a.upper()[0]
341            elif a and any(d.startswith(a) for d in ['header', 'noheader']):
342                header = a[0].startswith('h')
343        return self._render(formatter, cols, name_pat, size, header, limit)
344
345SMILEYS = {
346    ':)': 'smiley.png',
347    ':-)': 'smiley.png',
348    '=)': 'smiley.png',
349    ';-)': 'smiley-wink.png',
350    ';)': 'smiley-wink.png',
351    ':(': 'smiley-sad.png',
352    ':-(': 'smiley-sad.png',
353    ':|': 'smiley-neutral.png',
354    ':-|': 'smiley-neutral.png',
355    ':-?': 'smiley-confuse.png',
356    ':?': 'smiley-confuse.png',
357    ':D': 'smiley-lol.png',
358    ':-D': 'smiley-lol.png',
359    ':))': 'smiley-grin.png',
360    ':-))': 'smiley-grin.png',
361    ':-P': 'smiley-razz.png',
362    ':P': 'smiley-razz.png',
363    ':-O': 'smiley-red.png',
364    ':O': 'smiley-red.png',
365    ':-o': 'smiley-surprise.png',
366    ':o': 'smiley-surprise.png',
367    ':-X': 'smiley-zipper.png',
368    ':X': 'smiley-zipper.png',
369    'B-)': 'smiley-cool.png',
370    '8-)': 'smiley-nerd.png',
371    'B-O': 'smiley-eek.png',
372    '8-O': 'smiley-eek.png',
373    '>:>': 'smiley-evil.png',
374
375    '(!)': 'exclamation-red.png',
376    '(?)': 'question.png',
377    '(I)': 'light-bulb.png',
378    '(*)': 'asterisk.png',
379    '(X)': 'cross-circle.png',
380
381    '(Y)': 'thumb-up.png',
382    '(OK)': 'thumb-up.png',
383    '(N)': 'thumb.png',
384    '(NOK)': 'thumb.png',
385
386    '(./)': 'tick.png',
387}
388
389
390class Smileys(Component):
391    """Replace smiley characters like `:-)` with icons.
392
393    Smiley characters and icons are configurable in the `[wikiextras-smileys]`
394    section in `trac.ini`. Use the `ShowSmileys` macro to display a list of
395    currently defined smileys.
396    """
397
398    implements(IWikiMacroProvider, IWikiSyntaxProvider)
399
400    smileys_section = ConfigSection('wikiextras-smileys',
401            """The set of smileys is configurable by providing associations
402            between icon names and wiki keywords. A default set of icons and
403            keywords is defined, which can be revoked one-by-one (_remove) or
404            all at once (_remove_defaults).
405
406            Example:
407            {{{
408            [wikiextras-smileys]
409            _remove_defaults = true
410            _remove = :-( :(
411            smiley = :-) :)
412            smiley-wink = ;-) ;)
413            clock = (CLOCK) (TIME)
414            calendar-month = (CALENDAR) (DATE)
415            chart = (CHART)
416            document-excel = (EXCEL)
417            document-word = (WORD)
418            eye = (EYE)
419            new = (NEW)
420            tick = (TICK)
421            }}}
422
423            Keywords are space-separated!
424
425            A smiley can also be removed by associating its icon with nothing:
426            {{{
427            smiley =
428            }}}
429
430            Use the `ShowSmileys` macro to find out the current set of icons
431            and keywords.
432            """)
433
434    remove_defaults = BoolOption('wikiextras-smileys', '_remove_defaults',
435                                 False, doc="Set to true to remove all "
436                                            "default smileys.")
437
438    remove = ListOption('wikiextras-smileys', '_remove', sep=' ', doc="""\
439            Space-separated(!) list of keywords that shall not be interpreted
440            as smileys (even if defined in this section).""")
441
442    def __init__(self):
443        self.smileys = None
444
445    # IWikiSyntaxProvider methods
446
447    def get_wiki_syntax(self):
448        if self.smileys is None:
449            self.smileys = SMILEYS.copy()
450            if self.remove_defaults:
451                self.smileys = {}
452            for icon_name, value in self.smileys_section.options():
453                if not icon_name.startswith('_remove'):
454                    icon_file = icon_name
455                    if not icon_file.endswith('.png'):
456                        icon_file = '%s.png' % icon_file
457                    if value:
458                        for keyword in value.split():
459                            self.smileys[keyword.strip()] = icon_file
460                    else:
461                        # no keyword, remove all smileys associated with icon
462                        for k in self.smileys.keys():
463                            if self.smileys[k] == icon_file:
464                               del self.smileys[k]
465            for keyword in self.remove:
466                if keyword in self.smileys:
467                    del self.smileys[keyword]
468
469        if self.smileys:
470            yield (r"(?<!\w)!?(?:%s)" % prepare_regexp(self.smileys),
471                   self._format_smiley)
472        else:
473            yield (None, None)
474
475    def get_link_resolvers(self):
476        return []
477
478    #noinspection PyUnusedLocal
479    def _format_smiley(self, formatter, match, fullmatch=None):
480        #noinspection PyArgumentList
481        loc = Icons(self.env).icon_location()
482        return tag.img(src=formatter.href.chrome(loc[0], self.smileys[match]),
483                       alt=match, style="vertical-align: text-bottom")
484
485    # IWikiMacroProvider methods
486
487    def get_macros(self):
488        yield 'ShowSmileys'
489
490    #noinspection PyUnusedLocal
491    def get_macro_description(self, name):
492        return cleandoc("""Renders in a table the list of available smileys.
493                Optional argument is the number of columns in the table
494                (defaults 3).
495
496                Comment: Prefixing a character sequence with `!` prevents it
497                from being interpreted as a smiley.
498                """)
499
500    #noinspection PyUnusedLocal
501    def expand_macro(self, formatter, name, content, args=None):
502        # Merge smileys for presentation
503        # First collect wikitexts for each unique filename
504        syelims = {} # key=filename, value=wikitext
505        for wikitext, filename in self.smileys.iteritems():
506            if filename not in syelims:
507                syelims[filename] = [wikitext]
508            else:
509                syelims[filename].append(wikitext)
510        # Reverse
511        smileys = {}
512        for filename, wikitexts in syelims.iteritems():
513            wikitexts.sort()
514            smileys[' '.join(wikitexts)] = filename
515        return render_table(smileys.keys(), content,
516                            lambda s: self._format_smiley(formatter,
517                                                          s.split(' ', 1)[0]))
518
519
520class AboutWikiIcons(Component):
521    """Macro for displaying a wiki page on how to use icons and smileys.
522
523    Create a wiki page `WikiIcons` and insert the following line to show
524    detailed instructions to wiki authors on how to use icons and smileys in
525    wiki pages:
526    {{{
527    [[AboutWikiIcons]]
528    }}}
529    """
530
531    implements(IWikiMacroProvider)
532
533    # IWikiMacroProvider methods
534
535    def get_macros(self):
536        yield 'AboutWikiIcons'
537
538    #noinspection PyUnusedLocal
539    def get_macro_description(self, name):
540        return "Display a wiki page on how to use icons."
541
542    #noinspection PyUnusedLocal
543    def expand_macro(self, formatter, name, content, args=None):
544        help_file = resource_filename(__name__, 'doc/WikiIcons')
545        fd = open(help_file, 'r')
546        wiki_text = fd.read()
547        fd.close()
548        return format_to_html(self.env, formatter.context, wiki_text)
Note: See TracBrowser for help on using the repository browser.