Package trac :: Package wiki :: Module macros

Source Code for Module trac.wiki.macros

   1  # -*- coding: utf-8 -*- 
   2  # 
   3  # Copyright (C) 2005-2023 Edgewall Software 
   4  # Copyright (C) 2005-2006 Christopher Lenz <[email protected]> 
   5  # All rights reserved. 
   6  # 
   7  # This software is licensed as described in the file COPYING, which 
   8  # you should have received as part of this distribution. The terms 
   9  # are also available at https://trac.edgewall.org/wiki/TracLicense. 
  10  # 
  11  # This software consists of voluntary contributions made by many 
  12  # individuals. For the exact contribution history, see the revision 
  13  # history and logs, available at https://trac.edgewall.org/log/. 
  14  # 
  15  # Author: Christopher Lenz <[email protected]> 
  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  )  # ProcessorError unused, but imported for plugin use. 
  42  from trac.wiki.interwiki import InterWikiMap 
  43   
  44   
  45  # TODO: should be moved in .api 
46 -class WikiMacroBase(Component):
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 #: A gettext domain to translate the macro description 65 _domain = None 66 67 #: A macro description 68 _description = None 69 70 #: Hide from macro index 71 hide_from_macro_index = False 72
73 - def get_macros(self):
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
80 - def get_macro_description(self, name):
81 """Return the subclass's gettext domain and macro description""" 82 if self.hide_from_macro_index: 83 return None 84 domain, description = self._domain, self._description 85 if description: 86 return (domain, description) if domain else description 87 # For pre-0.12 compatibility 88 doc = inspect.getdoc(self.__class__) 89 return to_unicode(doc) if doc else ''
90
91 - def parse_macro(self, parser, name, content):
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
100 -class TitleIndexMacro(WikiMacroBase):
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
140 - def expand_macro(self, formatter, name, content):
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 # the function definitions for the different format styles 181 182 # the different page split formats, each corresponding to its rendering 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 # the different tree structures, each corresponding to its rendering 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 # remove key from path_elements in grouped entries for further 224 # grouping 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 # remove key from path_elements in grouped entries for 277 # further grouping 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 # the different rendering formats 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
326 -class RecentChangesMacro(WikiMacroBase):
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
352 - def expand_macro(self, formatter, name, content):
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 # TODO: - integrate the rest of the OutlineFormatter directly here 457 # - use formatter.wikidom instead of formatter.source 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
473 -class ImageMacro(WikiMacroBase):
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
544 - def is_inline(self, content):
545 args = [stripws(arg) for arg 546 in self._split_args_re.split(content or '')[1::2]] 547 return 'inline' in args
548 549 _split_re = r'''((?:[^%s"']|"[^"]*"|'[^']*')+)''' 550 _split_args_re = re.compile(_split_re % ',') 551 _split_filespec_re = re.compile(_split_re % ':') 552 # https://developer.mozilla.org/en-US/docs/Web/CSS/length 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
561 - def expand_macro(self, formatter, name, content):
562 args = None 563 if content: 564 content = stripws(content) 565 # parse arguments 566 # we expect the 1st argument to be a filename (filespec) 567 args = [stripws(arg) for arg 568 in self._split_args_re.split(content)[1::2]] 569 if not args: 570 return '' 571 # strip unicode white-spaces and ZWSPs are copied from attachments 572 # section (#10668) 573 filespec = args.pop(0) 574 575 # style information 576 attr = {} 577 style = {} 578 link = '' 579 # helper for the special case `source:` 580 # 581 from trac.versioncontrol.web_ui import BrowserModule 582 # FIXME: somehow use ResourceSystem.get_known_realms() 583 # ... or directly trac.wiki.extract_link 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 # 'width' keyword 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) # unquote "..." and '...' 630 if m: 631 val = m.group(1) 632 attr[str(key)] = val # will be used as a __call__ kwd 633 634 if self._quoted_re.match(filespec): 635 filespec = filespec.strip('\'"') 636 # parse filespec argument to get realm and id if contained. 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'): # absolute 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('//'): # server-relative 650 raw_url = url = filespec[1:] 651 desc = url.rsplit('?')[0] 652 elif filespec.startswith('/'): # project-relative 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: # realm:id:attachment-filename 661 # # or intertrac:realm:id 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: # source:path 673 # TODO: use context here as well 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: # #ticket:attachment or WikiPage:attachment 682 # FIXME: do something generic about shorthand forms... 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: # it's an attachment of the current resource 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' # avoid password prompt 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
746 - def _arg_as_length(self, val, key):
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
757 -class MacroListMacro(WikiMacroBase):
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
770 - def expand_macro(self, formatter, name, content):
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
819 -class TracIniMacro(WikiMacroBase):
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
837 - def expand_macro(self, formatter, name, content):
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
906 -class KnownMimeTypesMacro(WikiMacroBase):
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
914 - def expand_macro(self, formatter, name, content):
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")), # always use plural 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
943 -class TracGuideTocMacro(WikiMacroBase):
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
981 - def expand_macro(self, formatter, name, content):
982 curpage = formatter.resource.id 983 984 # scoped TOC (e.g. TranslateRu/TracGuide or 0.11/TracGuide ...) 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
1001 -def _arg_as_int(val, key=None, min=None, max=None):
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
1008 -def _invalid_macro_arg(val, key=None):
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