| 1 | from trac.core import * |
|---|
| 2 | from trac.wiki.api import IWikiMacroProvider |
|---|
| 3 | from trac.wiki import model |
|---|
| 4 | from trac.util import Markup |
|---|
| 5 | from trac.wiki import wiki_to_html, wiki_to_oneliner |
|---|
| 6 | from StringIO import StringIO |
|---|
| 7 | from tractags.api import TagEngine, ITagSpaceUser, sorted, set |
|---|
| 8 | import inspect |
|---|
| 9 | import re |
|---|
| 10 | import string |
|---|
| 11 | |
|---|
| 12 | class TagMacros(Component): |
|---|
| 13 | """ Versions of the old Wiki-only macros using the new tag API. """ |
|---|
| 14 | |
|---|
| 15 | implements(IWikiMacroProvider) |
|---|
| 16 | |
|---|
| 17 | def _page_titles(self, pages): |
|---|
| 18 | """ Extract page titles, if possible. """ |
|---|
| 19 | titles = {} |
|---|
| 20 | tagspace = TagEngine(self.env).tagspace.wiki |
|---|
| 21 | for pagename in pages: |
|---|
| 22 | href, link, title = tagspace.name_details(pagename) |
|---|
| 23 | titles[pagename] = title |
|---|
| 24 | return titles |
|---|
| 25 | |
|---|
| 26 | def _tag_details(self, links, tags): |
|---|
| 27 | """ Extract dictionary of tag:(href, title) for all tags. """ |
|---|
| 28 | for tag in tags: |
|---|
| 29 | if tag not in links: |
|---|
| 30 | links[tag] = TagEngine(self.env).get_tag_link(tag) |
|---|
| 31 | return links |
|---|
| 32 | |
|---|
| 33 | def _current_page(self, req): |
|---|
| 34 | return req.hdf.getValue('wiki.page_name', '') |
|---|
| 35 | |
|---|
| 36 | # IWikiMacroProvider methods |
|---|
| 37 | def get_macros(self): |
|---|
| 38 | yield 'TagCloud' |
|---|
| 39 | yield 'ListTagged' |
|---|
| 40 | yield 'TagIt' |
|---|
| 41 | yield 'ListTags' |
|---|
| 42 | |
|---|
| 43 | def get_macro_description(self, name): |
|---|
| 44 | import pydoc |
|---|
| 45 | return pydoc.getdoc(getattr(self, 'render_' + name.lower())) |
|---|
| 46 | |
|---|
| 47 | def render_macro(self, req, name, content): |
|---|
| 48 | from trac.web.chrome import add_stylesheet |
|---|
| 49 | from parseargs import parseargs |
|---|
| 50 | add_stylesheet(req, 'tags/css/tractags.css') |
|---|
| 51 | # Translate macro args into python args |
|---|
| 52 | args = [] |
|---|
| 53 | kwargs = {} |
|---|
| 54 | try: |
|---|
| 55 | # Set default args from config |
|---|
| 56 | _, config_args = parseargs(self.env.config.get('tags', '%s.args' % name.lower(), '')) |
|---|
| 57 | kwargs.update(config_args) |
|---|
| 58 | if content is not None: |
|---|
| 59 | args, macro_args = parseargs(content) |
|---|
| 60 | kwargs.update(macro_args) |
|---|
| 61 | except Exception, e: |
|---|
| 62 | raise TracError("Invalid arguments '%s' (%s %s)" % (content, e.__class__.__name__, e)) |
|---|
| 63 | |
|---|
| 64 | return getattr(self, 'render_' + name.lower(), content)(req, *args, **kwargs) |
|---|
| 65 | |
|---|
| 66 | # Macro implementations |
|---|
| 67 | def render_tagcloud(self, req, smallest=10, biggest=20, showcount=True, tagspace=None, mincount=1, tagspaces=[]): |
|---|
| 68 | """ This macro displays a [http://en.wikipedia.org/wiki/Tag_cloud tag cloud] (weighted list) |
|---|
| 69 | of all tags. |
|---|
| 70 | |
|---|
| 71 | ||'''Argument'''||'''Description'''|| |
|---|
| 72 | ||`tagspace=<tagspace>`||Specify the tagspace the macro should operate on.|| |
|---|
| 73 | ||`tagspaces=(<tagspace>,...)`||Specify a set of tagspaces the macro should operate on.|| |
|---|
| 74 | ||`smallest=<n>`||The lower bound of the font size for the tag cloud.|| |
|---|
| 75 | ||`biggest=<n>`||The upper bound of the font size for the tag cloud.|| |
|---|
| 76 | ||`showcount=true|false`||Show the count of objects for each tag?|| |
|---|
| 77 | ||`mincount=<n>`||Hide tags with a count less than `<n>`.|| |
|---|
| 78 | """ |
|---|
| 79 | |
|---|
| 80 | smallest = int(smallest) |
|---|
| 81 | biggest = int(biggest) |
|---|
| 82 | mincount = int(mincount) |
|---|
| 83 | |
|---|
| 84 | engine = TagEngine(self.env) |
|---|
| 85 | # Get wiki tagspace |
|---|
| 86 | if tagspace: |
|---|
| 87 | tagspaces = [tagspace] |
|---|
| 88 | else: |
|---|
| 89 | tagspaces = tagspaces or engine.tagspaces |
|---|
| 90 | cloud = {} |
|---|
| 91 | |
|---|
| 92 | for tag, names in engine.get_tags(tagspaces=tagspaces, detailed=True).iteritems(): |
|---|
| 93 | count = len(names) |
|---|
| 94 | if count >= mincount: |
|---|
| 95 | cloud[tag] = len(names) |
|---|
| 96 | |
|---|
| 97 | tags = cloud.keys() |
|---|
| 98 | |
|---|
| 99 | # No tags? |
|---|
| 100 | if not tags: return '' |
|---|
| 101 | |
|---|
| 102 | # by_count maps tag counts to an index in the set of counts |
|---|
| 103 | by_count = list(set(cloud.values())) |
|---|
| 104 | by_count.sort() |
|---|
| 105 | by_count = dict([(c, float(i)) for i, c in enumerate(by_count)]) |
|---|
| 106 | |
|---|
| 107 | taginfo = self._tag_details({}, tags) |
|---|
| 108 | tags.sort() |
|---|
| 109 | rlen = float(biggest - smallest) |
|---|
| 110 | tlen = float(len(by_count)) |
|---|
| 111 | scale = 1.0 |
|---|
| 112 | if tlen: |
|---|
| 113 | scale = rlen / tlen |
|---|
| 114 | out = StringIO() |
|---|
| 115 | out.write('<ul class="tagcloud">\n') |
|---|
| 116 | last = tags[-1] |
|---|
| 117 | for tag in tags: |
|---|
| 118 | if tag == last: |
|---|
| 119 | cls = ' class="last"' |
|---|
| 120 | else: |
|---|
| 121 | cls = '' |
|---|
| 122 | if showcount != 'false': |
|---|
| 123 | count = ' <span class="tagcount">(%i)</span>' % cloud[tag] |
|---|
| 124 | else: |
|---|
| 125 | count = '' |
|---|
| 126 | out.write('<li%s><a rel="tag" title="%s" style="font-size: %ipx" href="%s">%s</a>%s</li>\n' % ( |
|---|
| 127 | cls, |
|---|
| 128 | taginfo[tag][1], |
|---|
| 129 | smallest + int(by_count[cloud[tag]] * scale), |
|---|
| 130 | taginfo[tag][0], |
|---|
| 131 | tag, |
|---|
| 132 | count)) |
|---|
| 133 | out.write('</ul>\n') |
|---|
| 134 | return out.getvalue() |
|---|
| 135 | |
|---|
| 136 | def render_listtagged(self, req, *tags, **kwargs): |
|---|
| 137 | """ List tagged objects. Optionally accepts a list of tags to match |
|---|
| 138 | against. The special tag '''. (dot)''' inserts the current Wiki page name. |
|---|
| 139 | |
|---|
| 140 | `[[ListTagged(<tag>, ...)]]` |
|---|
| 141 | |
|---|
| 142 | ||'''Argument'''||'''Description'''|| |
|---|
| 143 | ||`tagspace=<tagspace>`||Specify the tagspace the macro should operate on.|| |
|---|
| 144 | ||`tagspaces=(<tagspace>,...)`||Specify a set of tagspaces the macro should operate on.|| |
|---|
| 145 | ||`operation=intersection|union`||The set operation to perform on the discovered objects.|| |
|---|
| 146 | ||`showheadings=true|false`||List objects under the tagspace they occur in.|| |
|---|
| 147 | """ |
|---|
| 148 | |
|---|
| 149 | if 'tagspace' in kwargs: |
|---|
| 150 | tagspaces = [kwargs.get('tagspace', None)] |
|---|
| 151 | else: |
|---|
| 152 | tagspaces = kwargs.get('tagspaces', '') or \ |
|---|
| 153 | list(TagEngine(self.env).tagspaces) |
|---|
| 154 | showheadings = kwargs.get('showheadings', 'false') |
|---|
| 155 | operation = kwargs.get('operation', 'intersection') |
|---|
| 156 | if operation not in ('union', 'intersection'): |
|---|
| 157 | raise TracError("Invalid tag set operation '%s'" % operation) |
|---|
| 158 | |
|---|
| 159 | engine = TagEngine(self.env) |
|---|
| 160 | page_name = req.hdf.get('wiki.page_name') |
|---|
| 161 | if page_name: |
|---|
| 162 | tags = [tag == '.' and page_name or tag for tag in tags] |
|---|
| 163 | |
|---|
| 164 | taginfo = {} |
|---|
| 165 | out = StringIO() |
|---|
| 166 | out.write('<ul class="listtagged">') |
|---|
| 167 | # Cull empty names |
|---|
| 168 | tagged_names = [(tagspace, names) for tagspace, names in |
|---|
| 169 | engine.get_tagged_names(tags=tags, tagspaces=tagspaces, |
|---|
| 170 | operation=operation, detailed=True).iteritems() |
|---|
| 171 | if names] |
|---|
| 172 | for tagspace, tagspace_names in sorted(tagged_names): |
|---|
| 173 | if showheadings == 'true': |
|---|
| 174 | out.write('<lh>%s tags</lh>' % tagspace) |
|---|
| 175 | for name, tags in sorted(tagspace_names.iteritems()): |
|---|
| 176 | if tagspace == 'wiki' and unicode(name).startswith('tags/'): continue |
|---|
| 177 | tags = sorted(tags) |
|---|
| 178 | taginfo = self._tag_details(taginfo, tags) |
|---|
| 179 | href, link, title = engine.name_details(tagspace, name) |
|---|
| 180 | htitle = wiki_to_oneliner(title, self.env) |
|---|
| 181 | name_tags = ['<a href="%s" title="%s">%s</a>' |
|---|
| 182 | % (taginfo[tag][0], taginfo[tag][1], tag) |
|---|
| 183 | for tag in tags] |
|---|
| 184 | if not name_tags: |
|---|
| 185 | name_tags = '' |
|---|
| 186 | else: |
|---|
| 187 | name_tags = ' (' + ', '.join(sorted(name_tags)) + ')' |
|---|
| 188 | out.write('<li>%s %s%s</li>\n' % |
|---|
| 189 | (link, htitle, name_tags)) |
|---|
| 190 | out.write('</ul>') |
|---|
| 191 | |
|---|
| 192 | return out.getvalue() |
|---|
| 193 | |
|---|
| 194 | def render_tagit(self, req, *tags): |
|---|
| 195 | """ '''''Deprecated. Does nothing.''''' """ |
|---|
| 196 | return '' |
|---|
| 197 | |
|---|
| 198 | def render_listtags(self, req, *tags, **kwargs): |
|---|
| 199 | """ List all tags. |
|---|
| 200 | |
|---|
| 201 | ||'''Argument'''||'''Description'''|| |
|---|
| 202 | ||`tagspace=<tagspace>`||Specify the tagspace the macro should operate on.|| |
|---|
| 203 | ||`tagspaces=(<tagspace>,...)`||Specify a set of tagspaces the macro should operate on.|| |
|---|
| 204 | ||`shownames=true|false`||Whether to show the objects that tags appear on ''(long)''.|| |
|---|
| 205 | """ |
|---|
| 206 | |
|---|
| 207 | if tags: |
|---|
| 208 | # Backwards compatibility |
|---|
| 209 | return self.render_listtagged(req, *tags, **kwargs) |
|---|
| 210 | |
|---|
| 211 | page = self._current_page(req) |
|---|
| 212 | engine = TagEngine(self.env) |
|---|
| 213 | |
|---|
| 214 | showpages = kwargs.get('showpages', None) or kwargs.get('shownames', 'false') |
|---|
| 215 | |
|---|
| 216 | if 'tagspace' in kwargs: |
|---|
| 217 | tagspaces = [kwargs['tagspace']] |
|---|
| 218 | else: |
|---|
| 219 | tagspaces = kwargs.get('tagspaces', []) or \ |
|---|
| 220 | list(TagEngine(self.env).tagspaces) |
|---|
| 221 | |
|---|
| 222 | out = StringIO() |
|---|
| 223 | out.write('<ul class="listtags">\n') |
|---|
| 224 | tag_details = {} |
|---|
| 225 | for tag, names in sorted(engine.get_tags(tagspaces=tagspaces, detailed=True).iteritems()): |
|---|
| 226 | href, title = engine.get_tag_link(tag) |
|---|
| 227 | htitle = wiki_to_oneliner(title, self.env) |
|---|
| 228 | out.write('<li><a href="%s" title="%s">%s</a> %s <span class="tagcount">(%i)</span>' % (href, title, tag, htitle, len(names))) |
|---|
| 229 | if showpages == 'true': |
|---|
| 230 | out.write('\n') |
|---|
| 231 | out.write(self.render_listtagged(req, tag, tagspaces=tagspaces)) |
|---|
| 232 | out.write('</li>\n') |
|---|
| 233 | |
|---|
| 234 | out.write('</ul>\n') |
|---|
| 235 | |
|---|
| 236 | return out.getvalue() |
|---|