1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 from fnmatch import fnmatchcase
18 from itertools import groupby
19 import fnmatch
20 import inspect
21 import io
22 import os
23 import re
24
25 from trac.core import *
26 from trac.resource import (
27 Resource, ResourceNotFound, get_resource_name, get_resource_summary,
28 get_resource_url
29 )
30 from trac.util import as_int
31 from trac.util.datefmt import format_date, from_utimestamp, user_time
32 from trac.util.html import Markup, escape, find_element, tag
33 from trac.util.presentation import separated
34 from trac.util.text import unicode_quote, to_unicode, stripws
35 from trac.util.translation import _, dgettext, cleandoc_, tag_
36 from trac.web.chrome import chrome_resource_path
37 from trac.wiki.api import IWikiMacroProvider, WikiSystem, parse_args
38 from trac.wiki.formatter import (
39 MacroError, OutlineFormatter, ProcessorError, extract_link, format_to_html,
40 format_to_oneliner, system_message
41 )
42 from trac.wiki.interwiki import InterWikiMap
43
44
45
47 """Abstract base class for wiki macros and processors.
48
49 On usage error, the `MacroError` or `ProcessorError` exception should be
50 raised, to ensure proper display of the error message in the rendered
51 wiki content.
52
53 The `_description` attribute contains the macro help.
54
55 Set the `hide_from_macro_index` attribute to `True` to prevent
56 displaying help in the macro index (`[[MacroList]]`). If the
57 default value of `False` and the `_description` is empty,
58 "No documentation found" will be displayed.
59 """
60
61 implements(IWikiMacroProvider)
62 abstract = True
63
64
65 _domain = None
66
67
68 _description = None
69
70
71 hide_from_macro_index = False
72
74 """Yield the name of the macro based on the class name."""
75 name = self.__class__.__name__
76 if name.endswith('Macro'):
77 name = name[:-5]
78 yield name
79
90
92 raise NotImplementedError
93
94 - def expand_macro(self, formatter, name, content, args=None):
95 raise NotImplementedError(
96 "pre-0.11 Wiki macro %s by provider %s no longer supported" %
97 (name, self.__class__))
98
99
101 _domain = 'messages'
102 _description = cleandoc_(
103 """Insert an alphabetic list of all wiki pages into the output.
104
105 Accepts a prefix string as parameter: if provided, only pages with names
106 that start with the prefix are included in the resulting list. If this
107 parameter is omitted, all pages are listed. If the prefix is specified,
108 a second argument of value `hideprefix` can be given as well, in order
109 to remove that prefix from the output.
110
111 The prefix string supports the standard relative-path notation ''when
112 using the macro in a wiki page''. A prefix string starting with `./`
113 will be relative to the current page, and parent pages can be
114 specified using `../`.
115
116 Several named parameters can be specified:
117 - `format=compact`: The pages are displayed as comma-separated links.
118 - `format=group`: The list of pages will be structured in groups
119 according to common prefix. This format also supports a `min=n`
120 argument, where `n` is the minimal number of pages for a group.
121 - `format=hierarchy`: The list of pages will be structured according
122 to the page name path hierarchy. This format also supports a `min=n`
123 argument, where higher `n` flatten the display hierarchy
124 - `depth=n`: limit the depth of the pages to list. If set to 0,
125 only toplevel pages will be shown, if set to 1, only immediate
126 children pages will be shown, etc. If not set, or set to -1,
127 all pages in the hierarchy will be shown.
128 - `include=page1:page*2`: include only pages that match an item in the
129 colon-separated list of pages. If the list is empty, or if no `include`
130 argument is given, include all pages.
131 - `exclude=page1:page*2`: exclude pages that match an item in the colon-
132 separated list of pages.
133
134 The `include` and `exclude` lists accept shell-style patterns.
135 """)
136
137 SPLIT_RE = re.compile(r"(/| )")
138 NUM_SPLIT_RE = re.compile(r"([0-9.]+)")
139
141 args, kw = parse_args(content)
142 prefix = args[0].strip() if args else None
143 hideprefix = args and len(args) > 1 and args[1].strip() == 'hideprefix'
144 minsize = _arg_as_int(kw.get('min', 1), 'min', min=1)
145 minsize_group = max(minsize, 2)
146 depth = _arg_as_int(kw.get('depth', -1), 'depth', min=-1)
147 format = kw.get('format', '')
148
149 def parse_list(name):
150 return [inc.strip() for inc in kw.get(name, '').split(':')
151 if inc.strip()]
152
153 includes = parse_list('include') or ['*']
154 excludes = parse_list('exclude')
155
156 wiki = formatter.wiki
157 resource = formatter.resource
158 if prefix and resource and resource.realm == 'wiki':
159 prefix = wiki.resolve_relative_name(prefix, resource.id)
160
161 start = prefix.count('/') if prefix else 0
162
163 if hideprefix:
164 omitprefix = lambda page: page[len(prefix):]
165 else:
166 omitprefix = lambda page: page
167
168 pages = sorted(page for page in wiki.get_pages(prefix)
169 if (depth < 0 or depth >= page.count('/') - start)
170 and 'WIKI_VIEW' in formatter.perm('wiki', page)
171 and any(fnmatchcase(page, inc) for inc in includes)
172 and not any(fnmatchcase(page, exc) for exc in excludes))
173
174 if format == 'compact':
175 return tag(
176 separated((tag.a(wiki.format_page_name(omitprefix(p)),
177 href=formatter.href.wiki(p)) for p in pages),
178 ', '))
179
180
181
182
183 def split_pages_group(pages):
184 """Return a list of (path elements, page_name) pairs,
185 where path elements correspond to the page name (without prefix)
186 splitted at Camel Case word boundaries, numbers and '/'.
187 """
188 page_paths = []
189 for page in pages:
190 path = [elt.strip() for elt in self.SPLIT_RE.split(
191 self.NUM_SPLIT_RE.sub(r" \1 ",
192 wiki.format_page_name(omitprefix(page), split=True)))]
193 page_paths.append(([elt for elt in path if elt], page))
194 return page_paths
195
196 def split_pages_hierarchy(pages):
197 """Return a list of (path elements, page_name) pairs,
198 where path elements correspond to the page name (without prefix)
199 splitted according to the '/' hierarchy.
200 """
201 return [(wiki.format_page_name(omitprefix(page)).split("/"), page)
202 for page in pages]
203
204
205 def tree_group(entries):
206 """Transform a flat list of entries into a tree structure.
207
208 `entries` is a list of `(path_elements, page_name)` pairs
209
210 Return a list organized in a tree structure, in which:
211 - a leaf is a page name
212 - a node is a `(key, nodes)` pairs, where:
213 - `key` is the leftmost of the path elements, common to the
214 grouped (path element, page_name) entries
215 - `nodes` is a list of nodes or leaves
216 """
217 def keyfn(args):
218 elts, name = args
219 return elts[0] if elts else ''
220 groups = []
221
222 for key, grouper in groupby(entries, keyfn):
223
224
225 grouped_entries = [(path_elements[1:], page_name)
226 for path_elements, page_name in grouper]
227
228 if key and len(grouped_entries) >= minsize_group:
229 subnodes = tree_group(sorted(grouped_entries))
230 if len(subnodes) == 1:
231 subkey, subnodes = subnodes[0]
232 node = (key + subkey, subnodes)
233 groups.append(node)
234 elif self.SPLIT_RE.match(key):
235 for elt in subnodes:
236 if isinstance(elt, tuple):
237 subkey, subnodes = elt
238 elt = (key + subkey, subnodes)
239 groups.append(elt)
240 else:
241 node = (key, subnodes)
242 groups.append(node)
243 else:
244 for path_elements, page_name in grouped_entries:
245 groups.append(page_name)
246 return groups
247
248 def tree_hierarchy(entries):
249 """Transform a flat list of entries into a tree structure.
250
251 `entries` is a list of `(path_elements, page_name)` pairs
252
253 Return a list organized in a tree structure, in which:
254 - a leaf is a `(rest, page)` pair, where:
255 - `rest` is the rest of the path to be shown
256 - `page` is a page name
257 - a node is a `(key, nodes, page)` pair, where:
258 - `key` is the leftmost of the path elements, common to the
259 grouped (path element, page_name) entries
260 - `page` is a page name (if one exists for that node)
261 - `nodes` is a list of nodes or leaves
262 """
263 def keyfn(args):
264 elts, name = args
265 return elts[0] if elts else ''
266 groups = []
267
268 for key, grouper in groupby(entries, keyfn):
269 grouped_entries = [e for e in grouper]
270 sub_entries = [e for e in grouped_entries if len(e[0]) > 1]
271 key_entries = [e for e in grouped_entries if len(e[0]) == 1]
272 key_entry = key_entries[0] if key_entries else None
273 key_page = key_entry[1] if key_entries else None
274
275 if key and len(sub_entries) >= minsize:
276
277
278 sub_entries = [(path_elements[1:], page)
279 for path_elements, page in sub_entries]
280
281 subnodes = tree_hierarchy(sorted(sub_entries))
282 node = (key, key_page, subnodes)
283 groups.append(node)
284 else:
285 if key_entry:
286 groups.append(key_entry)
287 groups.extend(sub_entries)
288 return groups
289
290
291 def render_group(group):
292 return tag.ul(
293 tag.li(tag(tag.strong(elt[0].strip('/')), render_group(elt[1]))
294 if isinstance(elt, tuple) else
295 tag.a(wiki.format_page_name(omitprefix(elt)),
296 href=formatter.href.wiki(elt)))
297 for elt in group)
298
299 def render_hierarchy(group):
300 return tag.ul(
301 tag.li(tag(tag.a(elt[0], href=formatter.href.wiki(elt[1]))
302 if elt[1] else tag(elt[0]),
303 render_hierarchy(elt[2]))
304 if len(elt) == 3 else
305 tag.a('/'.join(elt[0]),
306 href=formatter.href.wiki(elt[1])))
307 for elt in group)
308
309 transform = {
310 'group': lambda p: render_group(tree_group(split_pages_group(p))),
311 'hierarchy': lambda p: render_hierarchy(
312 tree_hierarchy(split_pages_hierarchy(p))),
313 }.get(format)
314
315 if transform:
316 titleindex = transform(pages)
317 else:
318 titleindex = tag.ul(
319 tag.li(tag.a(wiki.format_page_name(omitprefix(page)),
320 href=formatter.href.wiki(page)))
321 for page in pages)
322
323 return tag.div(titleindex, class_='titleindex')
324
325
327 _domain = 'messages'
328 _description = cleandoc_(
329 """List all pages that have recently been modified, ordered by the
330 time they were last modified.
331
332 This macro accepts two ordered arguments and a named argument. The named
333 argument can be placed in any position within the argument list.
334
335 The first parameter is a prefix string: if provided, only pages with names
336 that start with the prefix are included in the resulting list. If this
337 parameter is omitted, all pages are included in the list.
338
339 The second parameter is the maximum number of pages to include in the
340 list.
341
342 The `group` parameter determines how the list is presented:
343 `group=date` :: The pages are presented in bulleted lists that are
344 grouped by date (default).
345 `group=none` :: The pages are presented in a single bulleted list.
346
347 Tip: if you only want to specify a maximum number of entries and
348 don't want to filter by prefix, specify an empty first parameter,
349 e.g. `[[RecentChanges(,10,group=none)]]`.
350 """)
351
353 args, kw = parse_args(content)
354 prefix = args[0].strip() if args else None
355 limit = _arg_as_int(args[1].strip(), min=1) if len(args) > 1 else None
356 group = kw.get('group', 'date')
357
358 sql = """SELECT name, max(version) AS max_version,
359 max(time) AS max_time FROM wiki"""
360 args = []
361 if prefix:
362 with self.env.db_query as db:
363 sql += " WHERE name %s" % db.prefix_match()
364 args.append(db.prefix_match_value(prefix))
365 sql += " GROUP BY name ORDER BY max_time DESC"
366 if limit:
367 sql += " LIMIT %s"
368 args.append(limit)
369
370 entries_per_date = []
371 prevdate = None
372 for name, version, ts in self.env.db_query(sql, args):
373 if not 'WIKI_VIEW' in formatter.perm('wiki', name, version):
374 continue
375 req = formatter.req
376 date = user_time(req, format_date, from_utimestamp(ts))
377 if date != prevdate:
378 prevdate = date
379 entries_per_date.append((date, []))
380 version = int(version)
381 diff_href = None
382 if version > 1:
383 diff_href = formatter.href.wiki(name, action='diff',
384 version=version)
385 page_name = formatter.wiki.format_page_name(name)
386 entries_per_date[-1][1].append((page_name, name, version,
387 diff_href))
388
389 items_per_date = (
390 (date, (tag.li(tag.a(page, href=formatter.href.wiki(name)),
391 tag.small(' (', tag.a(_("diff"), href=diff_href),
392 ')') if diff_href else None,
393 '\n')
394 for page, name, version, diff_href in entries))
395 for date, entries in entries_per_date)
396
397 if group == 'date':
398 out = ((tag.h3(date, class_='section'), tag.ul(entries))
399 for date, entries in items_per_date)
400 else:
401 out = tag.ul(entries for date, entries in items_per_date)
402 return tag.div(out, class_="wikipage")
403
404
405 -class PageOutlineMacro(WikiMacroBase):
406 _domain = 'messages'
407 _description = cleandoc_(
408 """Display a structural outline of the current wiki page, each item in the
409 outline being a link to the corresponding heading.
410
411 This macro accepts four optional parameters:
412
413 * The first is a number or range that allows configuring the minimum and
414 maximum level of headings that should be included in the outline. For
415 example, specifying "1" here will result in only the top-level headings
416 being included in the outline. Specifying "2-3" will make the outline
417 include all headings of level 2 and 3, as a nested list. The default is
418 to include all heading levels.
419 * The second parameter can be used to specify a custom title (the default
420 is no title).
421 * The third parameter selects the style of the outline. This can be
422 either `inline` or `pullout` (the latter being the default). The
423 `inline` style renders the outline as normal part of the content, while
424 `pullout` causes the outline to be rendered in a box that is by default
425 floated to the right side of the other content.
426 * The fourth parameter specifies whether the outline is numbered or not.
427 It can be either `numbered` or `unnumbered` (the former being the
428 default). This parameter only has an effect in `inline` style.
429 """)
430
431 - def expand_macro(self, formatter, name, content):
432 min_depth, max_depth = 1, 6
433 title = None
434 inline = False
435 numbered = True
436 if content:
437 argv = [arg.strip() for arg in content.split(',')]
438 if len(argv) > 0:
439 depth = argv[0]
440 if '-' in depth:
441 min_depth, max_depth = \
442 [_arg_as_int(d, min=min_depth, max=max_depth)
443 for d in depth.split('-', 1)]
444 else:
445 min_depth = max_depth = \
446 _arg_as_int(depth, min=min_depth, max=max_depth)
447 if len(argv) > 1:
448 title = argv[1].strip()
449 for arg in argv[2:]:
450 arg = arg.strip().lower()
451 if arg == 'inline':
452 inline = True
453 elif arg == 'unnumbered':
454 numbered = False
455
456
457
458 out = io.StringIO()
459 oformatter = OutlineFormatter(self.env, formatter.context)
460 oformatter.format(formatter.source, out, max_depth, min_depth,
461 shorten=not inline)
462 outline = Markup(out.getvalue())
463
464 if title:
465 outline = tag.h4(title, class_='section') + outline
466 if not inline:
467 outline = tag.div(outline, class_='wiki-toc')
468 elif not numbered:
469 outline = tag.div(outline, class_='wiki-toc-un')
470 return outline
471
472
474 _domain = 'messages'
475 _description = cleandoc_(
476 """Embed an image in wiki-formatted text.
477
478 The first argument is the file specification. The file specification may
479 reference attachments in three ways:
480 * `module:id:file`, where module can be either '''wiki''' or '''ticket''',
481 to refer to the attachment named ''file'' of the specified wiki page or
482 ticket.
483 * `id:file`: same as above, but id is either a ticket shorthand or a Wiki
484 page name.
485 * `file` to refer to a local attachment named 'file'. This only works from
486 within that wiki page or a ticket.
487
488 The file specification may also refer to:
489 * repository files, using the `source:file` syntax
490 (`source:file@rev` works also).
491 * files, using direct URLs: `/file` for a project-relative,
492 `//file` for a server-relative, or `http://server/file` for
493 absolute location. An InterWiki prefix may be used.
494 * embedded data using the
495 [http://tools.ietf.org/html/rfc2397 rfc2397] `data` URL scheme,
496 provided the URL is enclosed in quotes.
497
498 The remaining arguments are optional and allow configuring the attributes
499 and style of the rendered `<img>` element:
500 * digits and unit are interpreted as the size (ex. 120px, 25%)
501 for the image
502 * `right`, `left`, `center`, `top`, `bottom` and `middle` are interpreted
503 as the alignment for the image (alternatively, the first three can be
504 specified using `align=...` and the last three using `valign=...`)
505 * `link=some TracLinks...` replaces the link to the image source by the
506 one specified using a TracLinks. If no value is specified, the link is
507 simply removed.
508 * `inline` specifies that the content generated be an inline XHTML
509 element. By default, inline content is not generated, therefore images
510 won't be rendered in section headings and other one-line content.
511 * `nolink` means without link to image source (deprecated, use `link=`)
512 * `key=value` style are interpreted as HTML attributes or CSS style
513 indications for the image. Valid keys are:
514 * align, valign, border, width, height, alt, title, longdesc, class,
515 margin, margin-(left,right,top,bottom), id and usemap
516 * `border`, `margin`, and `margin-`* can only be a single number
517 (units are pixels).
518 * `margin` is superseded by `center` which uses auto margins
519
520 Examples:
521 {{{
522 [[Image(photo.jpg)]] # simplest
523 [[Image(photo.jpg, 120px)]] # with image width size
524 [[Image(photo.jpg, right)]] # aligned by keyword
525 [[Image(photo.jpg, nolink)]] # without link to source
526 [[Image(photo.jpg, align=right)]] # aligned by attribute
527 }}}
528
529 You can use an image from a wiki page, ticket or other module.
530 {{{
531 [[Image(OtherPage:foo.bmp)]] # from a wiki page
532 [[Image(base/sub:bar.bmp)]] # from hierarchical wiki page
533 [[Image(#3:baz.bmp)]] # from another ticket
534 [[Image(ticket:36:boo.jpg)]] # from another ticket (long form)
535 [[Image(source:/img/bee.jpg)]] # from the repository
536 [[Image(htdocs:foo/bar.png)]] # from project htdocs dir
537 [[Image(shared:foo/bar.png)]] # from shared htdocs dir (since 1.0.2)
538 }}}
539
540 ''Adapted from the Image.py macro created by Shun-ichi Goto
541 <[email protected]>''
542 """)
543
548
549 _split_re = r'''((?:[^%s"']|"[^"]*"|'[^']*')+)'''
550 _split_args_re = re.compile(_split_re % ',')
551 _split_filespec_re = re.compile(_split_re % ':')
552
553 _size_re = re.compile(r'(?:[0-9]+|'
554 r'(?:[0-9]+(?:\.[0-9]+)?|\.[0-9]+)(?:%|ch|em|ex|ic|'
555 r'rem|vh|vw|vmax|vmin|vb|vi|px|cm|mm|Q|in|pc|pt))$')
556 _attr_re = re.compile('(align|valign|border|width|height|alt'
557 '|margin(?:-(?:left|right|top|bottom))?'
558 '|title|longdesc|class|id|usemap)=(.+)')
559 _quoted_re = re.compile("(?:[\"'])(.*)(?:[\"'])$")
560
562 args = None
563 if content:
564 content = stripws(content)
565
566
567 args = [stripws(arg) for arg
568 in self._split_args_re.split(content)[1::2]]
569 if not args:
570 return ''
571
572
573 filespec = args.pop(0)
574
575
576 attr = {}
577 style = {}
578 link = ''
579
580
581 from trac.versioncontrol.web_ui import BrowserModule
582
583
584 try:
585 browser_links = [res[0] for res in
586 BrowserModule(self.env).get_link_resolvers()]
587 except Exception:
588 browser_links = []
589 while args:
590 arg = args.pop(0)
591 if self._size_re.match(arg):
592
593 attr['width'] = arg
594 elif arg == 'nolink':
595 link = None
596 elif arg.startswith('link='):
597 val = arg.split('=', 1)[1]
598 elt = extract_link(self.env, formatter.context, val.strip())
599 elt = find_element(elt, 'href')
600 link = None
601 if elt is not None:
602 link = elt.attrib.get('href')
603 elif arg in ('left', 'right'):
604 style['float'] = arg
605 elif arg == 'center':
606 style['margin-left'] = style['margin-right'] = 'auto'
607 style['display'] = 'block'
608 style.pop('margin', '')
609 elif arg in ('top', 'bottom', 'middle'):
610 style['vertical-align'] = arg
611 else:
612 match = self._attr_re.match(arg)
613 if match:
614 key, val = match.groups()
615 if (key == 'align' and
616 val in ('left', 'right', 'center')) or \
617 (key == 'valign' and
618 val in ('top', 'middle', 'bottom')):
619 args.append(val)
620 elif key in ('margin-top', 'margin-bottom'):
621 style[key] = self._arg_as_length(val, key)
622 elif key in ('margin', 'margin-left', 'margin-right') and \
623 'display' not in style:
624 style[key] = self._arg_as_length(val, key)
625 elif key == 'border':
626 style['border'] = '%s solid' % \
627 self._arg_as_length(val, key)
628 else:
629 m = self._quoted_re.search(val)
630 if m:
631 val = m.group(1)
632 attr[str(key)] = val
633
634 if self._quoted_re.match(filespec):
635 filespec = filespec.strip('\'"')
636
637 parts = [i.strip('\'"')
638 for i in self._split_filespec_re.split(filespec)[1::2]]
639 realm = parts[0] if parts else None
640 url = raw_url = desc = None
641 attachment = None
642 interwikimap = InterWikiMap(self.env)
643 if realm in ('http', 'https', 'ftp', 'data'):
644 raw_url = url = filespec
645 desc = url.rsplit('?')[0]
646 elif realm in interwikimap:
647 url, desc = interwikimap.url(realm, ':'.join(parts[1:]))
648 raw_url = url
649 elif filespec.startswith('//'):
650 raw_url = url = filespec[1:]
651 desc = url.rsplit('?')[0]
652 elif filespec.startswith('/'):
653 params = ''
654 if '?' in filespec:
655 filespec, params = filespec.rsplit('?', 1)
656 url = formatter.href(filespec)
657 if params:
658 url += '?' + params
659 raw_url, desc = url, filespec
660 elif len(parts) == 3:
661
662 realm, id, filename = parts
663 intertrac_target = "%s:%s" % (id, filename)
664 it = formatter.get_intertrac_url(realm, intertrac_target)
665 if it:
666 url, desc = it
667 raw_url = url + unicode_quote('?format=raw')
668 else:
669 attachment = Resource(realm, id).child('attachment', filename)
670 elif len(parts) == 2:
671 realm, filename = parts
672 if realm in browser_links:
673
674 rev = None
675 if '@' in filename:
676 filename, rev = filename.rsplit('@', 1)
677 url = formatter.href.browser(filename, rev=rev)
678 raw_url = formatter.href.browser(filename, rev=rev,
679 format='raw')
680 desc = filespec
681 else:
682
683 realm = None
684 id, filename = parts
685 if id and id[0] == '#':
686 realm = 'ticket'
687 id = id[1:]
688 elif id == 'htdocs':
689 raw_url = url = formatter.href.chrome('site', filename)
690 desc = os.path.basename(filename)
691 elif id == 'shared':
692 raw_url = url = formatter.href.chrome('shared', filename)
693 desc = os.path.basename(filename)
694 else:
695 realm = 'wiki'
696 if realm:
697 attachment = Resource(realm, id).child('attachment',
698 filename)
699 elif len(parts) == 1:
700 attachment = formatter.resource.child('attachment', filespec)
701 else:
702 return system_message(_("No filespec given"))
703 if attachment:
704 try:
705 desc = get_resource_summary(self.env, attachment)
706 except ResourceNotFound:
707 link = None
708 raw_url = chrome_resource_path(formatter.context.req,
709 'common/attachment.png')
710 desc = _('No image "%(id)s" attached to %(parent)s',
711 id=attachment.id,
712 parent=get_resource_name(self.env, attachment.parent))
713 else:
714 if 'ATTACHMENT_VIEW' in formatter.perm(attachment):
715 url = get_resource_url(self.env, attachment,
716 formatter.href)
717 raw_url = get_resource_url(self.env, attachment,
718 formatter.href, format='raw')
719
720 if desc:
721 for key in ('title', 'alt'):
722 if key not in attr:
723 attr[key] = desc
724 for key in ('width', 'height'):
725 if key not in attr:
726 continue
727 val = attr[key]
728 if not self._size_re.match(val):
729 del attr[key]
730 continue
731 if not val.endswith('%') and not val.isdigit():
732 style[key] = val
733 del attr[key]
734 if style:
735 attr['style'] = '; '.join('%s:%s' % (k, escape(style[k]))
736 for k in sorted(style))
737 if not WikiSystem(self.env).is_safe_origin(raw_url,
738 formatter.context.req):
739 attr['crossorigin'] = 'anonymous'
740 result = tag.img(src=raw_url, **attr)
741 if link is not None:
742 result = tag.a(result, href=link or url,
743 style='padding:0; border:none')
744 return result
745
747 if val.isdigit():
748 return val + 'px'
749 if self._size_re.match(val):
750 return val
751 int_val = as_int(val, None, min=1)
752 if int_val is not None:
753 return '%dpx' % int_val
754 raise _invalid_macro_arg(val, key)
755
756
758 _domain = 'messages'
759 _description = cleandoc_(
760 """Display a list of all installed Wiki macros, including documentation if
761 available.
762
763 Optionally, the name of a specific macro can be provided as an argument. In
764 that case, only the documentation for that macro will be rendered.
765
766 Note that this macro will not be able to display the documentation of
767 macros if the `PythonOptimize` option is enabled for mod_python!
768 """)
769
771 content = content.strip() if content else ''
772 name_filter = content.strip('*')
773
774 def get_macro_descr():
775 for macro_provider in formatter.wiki.macro_providers:
776 names = list(macro_provider.get_macros() or [])
777 if name_filter and not any(name.startswith(name_filter)
778 for name in names):
779 continue
780 try:
781 name_descriptions = [
782 (name, macro_provider.get_macro_description(name))
783 for name in names]
784 except Exception as e:
785 yield system_message(
786 _("Error: Can't get description for macro %(name)s",
787 name=names[0]), e), names
788 else:
789 for descr, pairs in groupby(name_descriptions,
790 key=lambda p: p[1]):
791 if descr:
792 if isinstance(descr, (tuple, list)):
793 descr = dgettext(descr[0],
794 to_unicode(descr[1])) \
795 if descr[1] else ''
796 else:
797 descr = to_unicode(descr) or ''
798 if content == '*':
799 descr = format_to_oneliner(
800 self.env, formatter.context, descr,
801 shorten=True)
802 else:
803 descr = format_to_html(
804 self.env, formatter.context, descr)
805 elif descr is None:
806 continue
807 yield descr, [name for name, descr in pairs]
808
809 return tag.div(class_='trac-macrolist')(
810 (tag.h3(tag.code('[[', names[0], ']]'), id='%s-macro' % names[0]),
811 len(names) > 1 and tag.p(tag.strong(_("Aliases:")),
812 [tag.code(' [[', alias, ']]')
813 for alias in names[1:]]) or None,
814 description or tag.em(_("No documentation found")))
815 for description, names in sorted(get_macro_descr(),
816 key=lambda item: item[1][0]))
817
818
820 _domain = 'messages'
821 _description = cleandoc_(
822 """Produce documentation for the Trac configuration file.
823
824 Typically, this will be used in the TracIni page. The macro accepts
825 two ordered arguments and two named arguments.
826
827 The ordered arguments are a configuration section filter,
828 and a configuration option name filter: only the configuration
829 options whose section and name start with the filters are output.
830
831 The named arguments can be specified:
832
833 section :: a glob-style filtering on the section names
834 option :: a glob-style filtering on the option names
835 """)
836
838 from trac.config import ConfigSection, Option
839
840 args, kw = parse_args(content)
841 filters = {}
842 for name, index in (('section', 0), ('option', 1)):
843 pattern = kw.get(name, '').strip()
844 if pattern:
845 filters[name] = fnmatch.translate(pattern)
846 continue
847 prefix = args[index].strip() if index < len(args) else ''
848 if prefix:
849 filters[name] = re.escape(prefix)
850 has_option_filter = 'option' in filters
851 for name in ('section', 'option'):
852 filters[name] = re.compile(filters[name], re.IGNORECASE).match \
853 if name in filters \
854 else lambda v: True
855 section_filter = filters['section']
856 option_filter = filters['option']
857
858 section_registry = ConfigSection.get_registry(self.compmgr)
859 option_registry = Option.get_registry(self.compmgr)
860 options = {}
861 for (section, key), option in option_registry.iteritems():
862 if section_filter(section) and option_filter(key):
863 options.setdefault(section, {})[key] = option
864 if not has_option_filter:
865 for section in section_registry:
866 if section_filter(section):
867 options.setdefault(section, {})
868 for section in options:
869 options[section] = sorted(options[section].itervalues(),
870 key=lambda option: option.name)
871 sections = [(section, section_registry[section].doc
872 if section in section_registry else '')
873 for section in sorted(options)]
874
875 def default_cell(option):
876 default = option.default
877 if default is not None and default != '':
878 return tag.td(tag.code(option.dumps(default)),
879 class_='default')
880 else:
881 return tag.td(_("(no default)"), class_='nodefault')
882
883 def options_table(section, options):
884 if options:
885 return tag.table(class_='wiki')(
886 tag.tbody(
887 tag.tr(
888 tag.td(tag.a(tag.code(option.name),
889 class_='tracini-option',
890 href='#%s-%s-option' %
891 (section, option.name))),
892 tag.td(format_to_html(self.env, formatter.context,
893 option.doc)),
894 default_cell(option),
895 id='%s-%s-option' % (section, option.name),
896 class_='odd' if idx % 2 else 'even')
897 for idx, option in enumerate(options)))
898
899 return tag.div(class_='tracini')(
900 (tag.h3(tag.code('[%s]' % section), id='%s-section' % section),
901 format_to_html(self.env, formatter.context, section_doc),
902 options_table(section, options.get(section)))
903 for section, section_doc in sections)
904
905
907 _domain = 'messages'
908 _description = cleandoc_(
909 """List all known mime-types which can be used as WikiProcessors.
910
911 Can be given an optional argument which is interpreted as mime-type filter.
912 """)
913
915 from trac.mimeview.api import Mimeview
916 mime_map = Mimeview(self.env).mime_map
917 mime_type_filter = ''
918 args, kw = parse_args(content)
919 if args:
920 mime_type_filter = args.pop(0).strip().rstrip('*')
921
922 mime_types = {}
923 for key, mime_type in mime_map.iteritems():
924 if (not mime_type_filter or
925 mime_type.startswith(mime_type_filter)) and key != mime_type:
926 mime_types.setdefault(mime_type, []).append(key)
927
928 return tag.div(class_='mimetypes')(
929 tag.table(class_='wiki')(
930 tag.thead(tag.tr(
931 tag.th(_("MIME Types")),
932 tag.th(tag.a("WikiProcessors",
933 href=formatter.context.href.wiki(
934 'WikiProcessors'))))),
935 tag.tbody(
936 tag.tr(tag.th(tag.code(mime_type),
937 style="text-align: left"),
938 tag.td(tag.code(
939 ' '.join(sorted(mime_types[mime_type])))))
940 for mime_type in sorted(mime_types))))
941
942
944 _domain = 'messages'
945 _description = cleandoc_(
946 """Display a table of content for the Trac guide.
947
948 This macro shows a quick and dirty way to make a table-of-contents
949 for the !Help/Guide. The table of contents will contain the Trac* and
950 WikiFormatting pages, and can't be customized. See the
951 [https://trac-hacks.org/wiki/TocMacro TocMacro] for a more customizable
952 table of contents.
953 """)
954
955 TOC = [('TracGuide', 'Index'),
956 ('TracInstall', 'Installation'),
957 ('TracInterfaceCustomization', 'Customization'),
958 ('TracPlugins', 'Plugins'),
959 ('TracUpgrade', 'Upgrading'),
960 ('TracIni', 'Configuration'),
961 ('TracAdmin', 'Administration'),
962 ('TracBackup', 'Backup'),
963 ('TracLogging', 'Logging'),
964 ('TracPermissions', 'Permissions'),
965 ('TracWiki', 'The Wiki'),
966 ('WikiFormatting', 'Wiki Formatting'),
967 ('TracTimeline', 'Timeline'),
968 ('TracBrowser', 'Repository Browser'),
969 ('TracRevisionLog', 'Revision Log'),
970 ('TracChangeset', 'Changesets'),
971 ('TracTickets', 'Tickets'),
972 ('TracWorkflow', 'Workflow'),
973 ('TracRoadmap', 'Roadmap'),
974 ('TracQuery', 'Ticket Queries'),
975 ('TracBatchModify', 'Batch Modify'),
976 ('TracReports', 'Reports'),
977 ('TracRss', 'RSS Support'),
978 ('TracNotification', 'Notification'),
979 ]
980
982 curpage = formatter.resource.id
983
984
985 prefix = ''
986 idx = curpage.find('/')
987 if idx > 0:
988 prefix = curpage[:idx+1]
989
990 ws = WikiSystem(self.env)
991 return tag.div(
992 tag.h4(_('Table of Contents')),
993 tag.ul([tag.li(tag.a(title, href=formatter.href.wiki(prefix+ref),
994 class_=(not ws.has_page(prefix+ref) and
995 'missing')),
996 class_=(prefix+ref == curpage and 'active'))
997 for ref, title in self.TOC]),
998 class_='wiki-toc')
999
1000
1002 int_val = as_int(val, None, min=min, max=max)
1003 if int_val is None:
1004 raise _invalid_macro_arg(val, key)
1005 return int_val
1006
1007
1009 expr = tag.code("%s=%s" % (key, val)) if key else tag.code(val)
1010 return MacroError(tag_("Invalid macro argument %(expr)s", expr=expr))
1011