Edgewall Software

source: trunk/trac/wiki/formatter.py

Last change on this file was 17657, checked in by Jun Omae, 8 months ago

1.5.4dev: update copyright year to 2023 (refs #13402)

[skip ci]

  • Property svn:eol-style set to native
File size: 61.9 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2003-2023 Edgewall Software
4# Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
5# Copyright (C) 2004-2005 Christopher Lenz <cmlenz@gmx.de>
6# Copyright (C) 2005-2007 Christian Boos <cboos@edgewall.org>
7# All rights reserved.
8#
9# This software is licensed as described in the file COPYING, which
10# you should have received as part of this distribution. The terms
11# are also available at https://trac.edgewall.org/wiki/TracLicense.
12#
13# This software consists of voluntary contributions made by many
14# individuals. For the exact contribution history, see the revision
15# history and logs, available at https://trac.edgewall.org/log/.
16#
17# Author: Jonas Borgström <jonas@edgewall.com>
18# Christopher Lenz <cmlenz@gmx.de>
19# Christian Boos <cboos@edgewall.org>
20
21import io
22import re
23
24from trac.core import *
25from trac.mimeview import *
26from trac.resource import get_relative_resource, get_resource_url
27from trac.util import arity, as_int
28from trac.util.text import (
29 exception_to_unicode, shorten_line, to_unicode, unicode_quote,
30 unquote_label
31)
32from trac.util.html import (
33 Element, Fragment, Markup, TracHTMLSanitizer, escape, plaintext, tag,
34 to_fragment
35)
36from trac.util.translation import _, tag_
37from trac.wiki.api import WikiSystem, parse_args
38from trac.wiki.parser import WikiParser, parse_processor_args
39
40__all__ = ['Formatter', 'MacroError', 'ProcessorError',
41 'concat_path_query_fragment', 'extract_link', 'format_to',
42 'format_to_html', 'format_to_oneliner',
43 'split_url_into_path_query_fragment', 'wiki_to_outline']
44
45
46def _markup_to_unicode(markup):
47 if isinstance(markup, Fragment):
48 return Markup(markup)
49 else:
50 return to_unicode(markup)
51
52
53def system_message(msg, text=None):
54 return tag.div(tag.strong(msg), text and tag.pre(text),
55 class_="system-message")
56
57
58def split_url_into_path_query_fragment(target):
59 """Split a target along `?` and `#` in `(path, query, fragment)`.
60
61 >>> split_url_into_path_query_fragment('http://path?a=1&b=2#frag?ment')
62 ('http://path', '?a=1&b=2', '#frag?ment')
63 >>> split_url_into_path_query_fragment('http://path#frag?ment')
64 ('http://path', '', '#frag?ment')
65 >>> split_url_into_path_query_fragment('http://path?a=1&b=2')
66 ('http://path', '?a=1&b=2', '')
67 >>> split_url_into_path_query_fragment('http://path')
68 ('http://path', '', '')
69 """
70 query = fragment = ''
71 idx = target.find('#')
72 if idx >= 0:
73 target, fragment = target[:idx], target[idx:]
74 idx = target.find('?')
75 if idx >= 0:
76 target, query = target[:idx], target[idx:]
77 return target, query, fragment
78
79
80def concat_path_query_fragment(path, query, fragment=None):
81 """Assemble `path`, `query` and `fragment` into a proper URL.
82
83 Can be used to re-assemble an URL decomposed using
84 `split_url_into_path_query_fragment` after modification.
85
86 >>> concat_path_query_fragment('/wiki/page', '?version=1')
87 '/wiki/page?version=1'
88 >>> concat_path_query_fragment('/wiki/page#a', '?version=1', '#b')
89 '/wiki/page?version=1#b'
90 >>> concat_path_query_fragment('/wiki/page?version=1#a', '?format=txt')
91 '/wiki/page?version=1&format=txt#a'
92 >>> concat_path_query_fragment('/wiki/page?version=1', '&format=txt')
93 '/wiki/page?version=1&format=txt'
94 >>> concat_path_query_fragment('/wiki/page?version=1', 'format=txt')
95 '/wiki/page?version=1&format=txt'
96 >>> concat_path_query_fragment('/wiki/page?version=1#a', '?format=txt', '#')
97 '/wiki/page?version=1&format=txt'
98 """
99 p, q, f = split_url_into_path_query_fragment(path)
100 if query:
101 q += ('&' if q else '?') + query.lstrip('?&')
102 if fragment:
103 f = fragment
104 return p + q + ('' if f == '#' else f)
105
106
107class MacroError(TracError):
108 """Exception raised on incorrect macro usage.
109
110 The exception is trapped by the wiki formatter and the message is
111 rendered in a `pre` tag, wrapped in a div with class `system-message`.
112
113 :since: 1.0.11
114 """
115 pass
116
117class ProcessorError(TracError):
118 """Exception raised on incorrect processor usage.
119
120 The exception is trapped by the wiki formatter and the message is
121 rendered in a `pre` tag, wrapped in a div with class `system-message`.
122
123 :since: 0.12
124 """
125 pass
126
127
128class WikiProcessor(object):
129
130 _code_block_re = re.compile(r'^<div(?:\s+class="([^"]+)")?>(.*)</div>$')
131 _block_elem_re = re.compile(r'^\s*<(?:div|table)(?:\s+[^>]+)?>',
132 re.I | re.M)
133
134 def __init__(self, formatter, name, args=None):
135 """Find the processor by name
136
137 :param formatter: the formatter embedding a call for this processor
138 :param name: the name of the processor
139 :param args: extra parameters for the processor
140
141 (''since 0.11'')
142 """
143 self.formatter = formatter
144 self.env = formatter.env
145 self.name = name
146 self.args = args
147 self.error = None
148 self.macro_provider = None
149
150 # FIXME: move these tables outside of __init__
151 builtin_processors = {'html': self._html_processor,
152 'htmlcomment': self._htmlcomment_processor,
153 'default': self._default_processor,
154 'comment': self._comment_processor,
155 'div': self._div_processor,
156 'rtl': self._rtl_processor,
157 'span': self._span_processor,
158 'Span': self._span_processor,
159 'td': self._td_processor,
160 'th': self._th_processor,
161 'tr': self._tr_processor,
162 'table': self._table_processor,
163 }
164
165 self.inline_check = {'html': self._html_is_inline,
166 'htmlcomment': True, 'comment': True,
167 'span': True, 'Span': True,
168 }.get(name)
169
170 self._sanitizer = TracHTMLSanitizer(
171 safe_schemes=formatter.wiki.safe_schemes,
172 safe_origins=formatter.wiki.safe_origins)
173
174 self.processor = builtin_processors.get(name)
175 if not self.processor:
176 # Find a matching wiki macro
177 for macro_provider in WikiSystem(self.env).macro_providers:
178 for macro_name in macro_provider.get_macros() or []:
179 if self.name == macro_name:
180 if hasattr(macro_provider, 'expand_macro'):
181 self.processor = self._macro_processor
182 else:
183 raise TracError(
184 tag_("Pre-0.11 macros with the %(method)s "
185 "method are no longer supported.",
186 method=tag.code("render_macro")))
187 self.macro_provider = macro_provider
188 self.inline_check = getattr(macro_provider, 'is_inline',
189 False)
190 break
191 if not self.processor:
192 # Find a matching mimeview renderer
193 mimeview = Mimeview(formatter.env)
194 for renderer in mimeview.renderers:
195 if renderer.get_quality_ratio(self.name) > 1:
196 self.processor = self._mimeview_processor
197 break
198 if not self.processor:
199 mimetype = mimeview.get_mimetype(self.name)
200 if mimetype:
201 self.name = mimetype
202 self.processor = self._mimeview_processor
203 if not self.processor:
204 self.processor = self._default_processor
205 self.error = _("No macro or processor named '%(name)s' found",
206 name=name)
207
208 # inline checks
209
210 def _html_is_inline(self, text):
211 if text:
212 tag = text[1:].lstrip()
213 idx = tag.find(' ')
214 if idx > -1:
215 tag = tag[:idx]
216 return tag.lower() in ('a', 'span', 'bdo', 'img',
217 'big', 'small', 'font',
218 'tt', 'i', 'b', 'u', 's', 'strike',
219 'em', 'strong', 'dfn', 'code', 'q',
220 'samp', 'kbd', 'var', 'cite', 'abbr',
221 'acronym', 'sub', 'sup')
222 # builtin processors
223
224 def _comment_processor(self, text):
225 return ''
226
227 def _default_processor(self, text):
228 if self.args and 'lineno' in self.args:
229 self.name = \
230 Mimeview(self.formatter.env).get_mimetype('text/plain')
231 return self._mimeview_processor(text)
232 else:
233 return tag.pre(text, class_="wiki")
234
235 def _html_processor(self, text):
236 if WikiSystem(self.env).render_unsafe_content:
237 return Markup(text)
238 return self._sanitizer.sanitize(text)
239
240 def _htmlcomment_processor(self, text):
241 if "--" in text:
242 return system_message(_('Error: Forbidden character sequence '
243 '"--" in htmlcomment wiki code block'))
244 return Markup('<!--\n%s-->\n' % text)
245
246 def _elt_processor(self, eltname, format_to, text):
247 # Note: as long as _processor_param_re is not re.UNICODE, **args is OK.
248 # Also, parse_args is using strict mode when processing [[span(...)]].
249 elt = getattr(tag, eltname)(**(self.args or {}))
250 if not WikiSystem(self.env).render_unsafe_content:
251 sanitized_elt = getattr(tag, eltname)
252 sanitized_elt.attrib = self._sanitizer.sanitize_attrs(eltname,
253 elt.attrib)
254 elt = sanitized_elt
255 elt.append(format_to(self.env, self.formatter.context, text))
256 return elt
257
258 def _div_processor(self, text):
259 if not self.args:
260 self.args = {}
261 self.args.setdefault('class', 'wikipage')
262 return self._elt_processor('div', format_to_html, text)
263
264 def _rtl_processor(self, text):
265 if not self.args:
266 self.args = {}
267 self.args['class'] = ('rtl ' + self.args.get('class', '')).rstrip()
268 return self._elt_processor('div', format_to_html, text)
269
270 def _span_processor(self, text):
271 if self.args is None:
272 args, self.args = parse_args(text, strict=True)
273 text = ', '.join(args)
274 return self._elt_processor('span', format_to_oneliner, text)
275
276 def _td_processor(self, text):
277 return self._tablecell_processor('td', text)
278
279 def _th_processor(self, text):
280 return self._tablecell_processor('th', text)
281
282 def _tr_processor(self, text):
283 try:
284 elt = self._elt_processor('tr', self._format_row, text)
285 self.formatter.open_table()
286 return elt
287 except ProcessorError as e:
288 return system_message(e)
289
290 def _table_processor(self, text):
291 if not self.args:
292 self.args = {}
293 self.args.setdefault('class', 'wiki')
294 try:
295 return self._elt_processor('table', self._format_table, text)
296 except ProcessorError as e:
297 return system_message(e)
298
299 def _tablecell_processor(self, eltname, text):
300 self.formatter.open_table_row()
301 return self._elt_processor(eltname, format_to_html, text)
302
303 _has_multiple_tables_re = re.compile(r"</table>.*?<table",
304 re.MULTILINE | re.DOTALL)
305
306 _inner_table_re = re.compile(r"""\s*
307 <table[^>]*>\s*
308 ((?:<tr[^>]*>)?
309 (.*?)
310 (?:</tr>)?)\s*
311 </table>\s*$
312 """, re.MULTILINE | re.DOTALL | re.VERBOSE)
313
314 # Note: the need for "parsing" that crude way the formatted content
315 # will go away as soon as we have a WikiDOM to manipulate...
316
317 def _parse_inner_table(self, text):
318 if self._has_multiple_tables_re.search(text):
319 raise ProcessorError(_("!#%(name)s must contain at most one table",
320 name=self.name))
321 match = self._inner_table_re.match(text)
322 if not match:
323 raise ProcessorError(_("!#%(name)s must contain at least one table"
324 " cell (and table cells only)",
325 name=self.name))
326 return Markup(match.group(1 if self.name == 'table' else 2))
327
328 def _format_row(self, env, context, text):
329 if text:
330 out = io.StringIO()
331 Formatter(env, context).format(text, out)
332 text = self._parse_inner_table(out.getvalue())
333 return text
334
335 def _format_table(self, env, context, text):
336 if text:
337 out = io.StringIO()
338 Formatter(env, context).format(text, out)
339 text = self._parse_inner_table(out.getvalue())
340 return text
341
342 # generic processors
343
344 def _macro_processor(self, text):
345 self.env.log.debug('Executing Wiki macro %s by provider %s',
346 self.name, self.macro_provider)
347 if arity(self.macro_provider.expand_macro) == 4:
348 return self.macro_provider.expand_macro(self.formatter, self.name,
349 text, self.args)
350 else:
351 return self.macro_provider.expand_macro(self.formatter, self.name,
352 text)
353
354 def _mimeview_processor(self, text):
355 annotations = []
356 context = self.formatter.context.child()
357 args = self.args.copy() if self.args else self.args
358 if args and 'lineno' in args:
359 lineno = as_int(args.pop('lineno'), 1, min=1)
360 context.set_hints(lineno=lineno)
361 id = str(args.pop('id', '')) or \
362 self.formatter._unique_anchor('a')
363 context.set_hints(id=id + '-')
364 if 'marks' in args:
365 context.set_hints(marks=args.pop('marks'))
366 annotations.append('lineno')
367 if args: # Remaining args are assumed to be lexer options
368 context.set_hints(lexer_options=args)
369 return tag.div(class_='wiki-code')(
370 Mimeview(self.env).render(context, self.name, text,
371 annotations=annotations))
372 # TODO: use convert('text/html') instead of render
373
374 def process(self, text, in_paragraph=False):
375 if self.error:
376 text = system_message(tag_("Error: Failed to load processor "
377 "%(name)s", name=tag.code(self.name)),
378 self.error)
379 else:
380 text = self.processor(text)
381 return text or ''
382
383 def is_inline(self, text):
384 if callable(self.inline_check):
385 return self.inline_check(text)
386 else:
387 return self.inline_check
388
389 def ensure_inline(self, text, in_paragraph=True):
390 content_for_span = None
391 interrupt_paragraph = False
392 if isinstance(text, Element):
393 tagname = text.tag.lower()
394 if tagname == 'div':
395 class_ = text.attrib.get('class', '')
396 if class_ and 'code' in class_:
397 content_for_span = text.children
398 else:
399 interrupt_paragraph = True
400 elif tagname == 'table':
401 interrupt_paragraph = True
402 else:
403 # FIXME: do something smarter for Streams
404 text = _markup_to_unicode(text)
405 match = re.match(self._code_block_re, text)
406 if match:
407 if match.group(1) and 'code' in match.group(1):
408 content_for_span = match.group(2)
409 else:
410 interrupt_paragraph = True
411 elif re.match(self._block_elem_re, text):
412 interrupt_paragraph = True
413 if content_for_span:
414 text = tag.span(class_='code-block')(*content_for_span)
415 elif interrupt_paragraph and in_paragraph:
416 text = "</p>%s<p>" % _markup_to_unicode(text)
417 return text
418
419
420class Formatter(object):
421 """Base Wiki formatter.
422
423 Parses and formats wiki text, in a given `RenderingContext`.
424 """
425
426 flavor = 'default'
427
428 def __init__(self, env, context):
429 self.env = env
430 self.context = context.child()
431 self.context.set_hints(disable_warnings=True)
432 self.req = context.req
433 self.href = context.href
434 self.resource = context.resource
435 self.perm = context.perm
436 self.wiki = WikiSystem(self.env)
437 self.wikiparser = WikiParser(self.env)
438 self._anchors = {}
439 self._open_tags = []
440 self._safe_schemes = None
441 if not self.wiki.render_unsafe_content:
442 self._safe_schemes = set(self.wiki.safe_schemes)
443
444
445 def split_link(self, target):
446 return split_url_into_path_query_fragment(target)
447
448 # -- Pre- IWikiSyntaxProvider rules (Font styles)
449
450 _indirect_tags = {
451 'MM_BOLD': ('<strong>', '</strong>'),
452 'WC_BOLD': ('<strong>', '</strong>'),
453 'MM_ITALIC': ('<em>', '</em>'),
454 'WC_ITALIC': ('<em>', '</em>'),
455 'MM_UNDERLINE': ('<span class="underline">', '</span>'),
456 'MM_STRIKE': ('<del>', '</del>'),
457 'MM_SUBSCRIPT': ('<sub>', '</sub>'),
458 'MM_SUPERSCRIPT': ('<sup>', '</sup>'),
459 }
460
461 def _get_open_tag(self, tag):
462 """Retrieve opening tag for direct or indirect `tag`."""
463 if not isinstance(tag, tuple):
464 tag = self._indirect_tags[tag]
465 return tag[0]
466
467 def _get_close_tag(self, tag):
468 """Retrieve closing tag for direct or indirect `tag`."""
469 if not isinstance(tag, tuple):
470 tag = self._indirect_tags[tag]
471 return tag[1]
472
473 def tag_open_p(self, tag):
474 """Do we currently have any open tag with `tag` as end-tag?"""
475 return tag in self._open_tags
476
477 def pop_tags(self):
478 while self._open_tags:
479 yield self._open_tags.pop()
480
481 def flush_tags(self):
482 for tag in self.pop_tags():
483 self.out.write(self._get_close_tag(tag))
484
485 def open_tag(self, tag_open, tag_close=None):
486 """Open an inline style tag.
487
488 If `tag_close` is not specified, `tag_open` is an indirect tag (0.12)
489 """
490 if tag_close:
491 self._open_tags.append((tag_open, tag_close))
492 else:
493 self._open_tags.append(tag_open)
494 tag_open = self._get_open_tag(tag_open)
495 return tag_open
496
497 def close_tag(self, open_tag, close_tag=None):
498 """Open a inline style tag.
499
500 If `close_tag` is not specified, it's an indirect tag (0.12)
501 """
502 tmp = ''
503 for i in range(len(self._open_tags) - 1, -1, -1):
504 tag = self._open_tags[i]
505 tmp += self._get_close_tag(tag)
506 if (open_tag == tag,
507 (open_tag, close_tag) == tag)[bool(close_tag)]:
508 del self._open_tags[i]
509 for j in range(i, len(self._open_tags)):
510 tmp += self._get_open_tag(self._open_tags[j])
511 break
512 return tmp
513
514 def _indirect_tag_handler(self, match, tag):
515 """Handle binary inline style tags (indirect way, 0.12)"""
516 if self._list_stack and not self.in_list_item:
517 self.close_list()
518
519 if self.tag_open_p(tag):
520 return self.close_tag(tag)
521 else:
522 return self.open_tag(tag)
523
524 def _bolditalic_formatter(self, match, fullmatch):
525 if self._list_stack and not self.in_list_item:
526 self.close_list()
527
528 bold_open = self.tag_open_p('MM_BOLD')
529 italic_open = self.tag_open_p('MM_ITALIC')
530 if bold_open and italic_open:
531 bold_idx = self._open_tags.index('MM_BOLD')
532 italic_idx = self._open_tags.index('MM_ITALIC')
533 if italic_idx < bold_idx:
534 close_tags = ('MM_BOLD', 'MM_ITALIC')
535 else:
536 close_tags = ('MM_ITALIC', 'MM_BOLD')
537 open_tags = ()
538 elif bold_open:
539 close_tags = ('MM_BOLD',)
540 open_tags = ('MM_ITALIC',)
541 elif italic_open:
542 close_tags = ('MM_ITALIC',)
543 open_tags = ('MM_BOLD',)
544 else:
545 close_tags = ()
546 open_tags = ('MM_BOLD', 'MM_ITALIC')
547
548 tmp = []
549 tmp.extend(self.close_tag(tag) for tag in close_tags)
550 tmp.extend(self.open_tag(tag) for tag in open_tags)
551 return ''.join(tmp)
552
553 def _bold_formatter(self, match, fullmatch):
554 return self._indirect_tag_handler(match, 'MM_BOLD')
555
556 def _bold_wc_formatter(self, match, fullmatch):
557 return self._indirect_tag_handler(match, 'WC_BOLD')
558
559 def _italic_formatter(self, match, fullmatch):
560 return self._indirect_tag_handler(match, 'MM_ITALIC')
561
562 def _italic_wc_formatter(self, match, fullmatch):
563 return self._indirect_tag_handler(match, 'WC_ITALIC')
564
565 def _underline_formatter(self, match, fullmatch):
566 return self._indirect_tag_handler(match, 'MM_UNDERLINE')
567
568 def _strike_formatter(self, match, fullmatch):
569 return self._indirect_tag_handler(match, 'MM_STRIKE')
570
571 def _subscript_formatter(self, match, fullmatch):
572 return self._indirect_tag_handler(match, 'MM_SUBSCRIPT')
573
574 def _superscript_formatter(self, match, fullmatch):
575 return self._indirect_tag_handler(match, 'MM_SUPERSCRIPT')
576
577 def _inlinecode_formatter(self, match, fullmatch):
578 return tag.code(fullmatch.group('inline'))
579
580 def _inlinecode2_formatter(self, match, fullmatch):
581 return tag.code(fullmatch.group('inline2'))
582
583 # pre-0.12 public API (no longer used by Trac itself but kept for plugins)
584
585 def simple_tag_handler(self, match, open_tag, close_tag):
586 """Generic handler for simple binary style tags"""
587 if self.tag_open_p((open_tag, close_tag)):
588 return self.close_tag(open_tag, close_tag)
589 else:
590 self.open_tag(open_tag, close_tag)
591 return open_tag
592
593 # -- Post- IWikiSyntaxProvider rules
594
595 # WikiCreole line breaks
596
597 def _linebreak_wc_formatter(self, match, fullmatch):
598 return '<br />'
599
600 # E-mails
601
602 def _email_formatter(self, match, fullmatch):
603 from trac.web.chrome import Chrome
604 omatch = Chrome(self.env).format_emails(self.context, match)
605 if omatch == match: # not obfuscated, make a link
606 return self._make_mail_link('mailto:'+match, match)
607 else:
608 return omatch
609
610 # HTML escape of &, < and >
611
612 def _htmlescape_formatter(self, match, fullmatch):
613 return "&amp;" if match == "&" \
614 else "&lt;" if match == "<" else "&gt;"
615
616 # Short form (shref) and long form (lhref) of TracLinks
617
618 def _shrefbr_formatter(self, match, fullmatch):
619 ns = fullmatch.group('snsbr')
620 target = unquote_label(fullmatch.group('stgtbr'))
621 match = match[1:-1]
622 return '&lt;%s&gt;' % \
623 self._make_link(ns, target, match, match, fullmatch)
624
625 def _shref_formatter(self, match, fullmatch):
626 ns = fullmatch.group('sns')
627 target = unquote_label(fullmatch.group('stgt'))
628 return self._make_link(ns, target, match, match, fullmatch)
629
630 def _lhref_formatter(self, match, fullmatch):
631 rel = fullmatch.group('rel')
632 ns = fullmatch.group('lns')
633 target = unquote_label(fullmatch.group('ltgt'))
634 label = fullmatch.group('label')
635 return self._make_lhref_link(match, fullmatch, rel, ns, target, label)
636
637 def _make_lhref_link(self, match, fullmatch, rel, ns, target, label):
638 if not label: # e.g. `[http://target]` or `[wiki:target]`
639 if target:
640 if ns and target.startswith('//'): # for `[http://target]`
641 label = ns + ':' + target # use `http://target`
642 else: # for `wiki:target`
643 label = target.lstrip('/') # use only `target`
644 else: # e.g. `[search:]`
645 label = ns
646 else:
647 label = unquote_label(label)
648 if rel:
649 if not label:
650 label = self.wiki.make_label_from_target(rel)
651 path, query, fragment = self.split_link(rel)
652 if path.startswith('//'):
653 path = '/' + path.lstrip('/')
654 elif path.startswith('/'):
655 path = self.href + path
656 else:
657 resource = get_relative_resource(self.resource, path)
658 path = get_resource_url(self.env, resource, self.href)
659 if resource.id:
660 target = concat_path_query_fragment(str(resource.id),
661 query, fragment)
662 if resource.realm == 'wiki':
663 target = '/' + target # Avoid wiki page scoping
664 return self._make_link(resource.realm, target, match,
665 label, fullmatch)
666 return tag.a(label,
667 href=concat_path_query_fragment(path, query, fragment))
668 else:
669 return self._make_link(ns or 'wiki', target or '', match, label,
670 fullmatch)
671
672 def _make_link(self, ns, target, match, label, fullmatch):
673 # first check for an alias defined in trac.ini
674 ns = self.env.config['intertrac'].get(ns, ns)
675 if ns in self.wikiparser.link_resolvers:
676 resolver = self.wikiparser.link_resolvers[ns]
677 if arity(resolver) == 5:
678 return resolver(self, ns, target, escape(label, False),
679 fullmatch)
680 else:
681 return resolver(self, ns, target, escape(label, False))
682 elif ns == "mailto":
683 from trac.web.chrome import Chrome
684 chrome = Chrome(self.env)
685 if chrome.never_obfuscate_mailto:
686 otarget, olabel = target, label
687 else:
688 otarget = chrome.format_emails(self.context, target)
689 olabel = chrome.format_emails(self.context, label)
690 if (otarget, olabel) == (target, label):
691 return self._make_mail_link('mailto:'+target, label)
692 else:
693 return olabel or otarget
694 elif target.startswith('//'):
695 if self._safe_schemes is None or ns in self._safe_schemes:
696 return self._make_ext_link(ns + ':' + target, label)
697 else:
698 return escape(match)
699 else:
700 return self._make_intertrac_link(ns, target, label) or \
701 self._make_interwiki_link(ns, target, label) or \
702 escape(match)
703
704 def _make_intertrac_link(self, ns, target, label):
705 res = self.get_intertrac_url(ns, target)
706 if res:
707 return self._make_ext_link(res[0], label, res[1])
708
709 def get_intertrac_url(self, ns, target):
710 intertrac = self.env.config['intertrac']
711 url = intertrac.get(ns + '.url')
712 name = _("Trac project %(name)s", name=ns)
713 if not url and ns.lower() == 'trac':
714 url = 'https://trac.edgewall.org'
715 name = _("The Trac Project")
716 if url:
717 name = intertrac.get(ns + '.title', name)
718 url = '%s/intertrac/%s' % (url, unicode_quote(target))
719 if target:
720 title = _("%(target)s in %(name)s", target=target, name=name)
721 else:
722 title = name
723 return url, title
724
725 def shorthand_intertrac_helper(self, ns, target, label, fullmatch):
726 if fullmatch: # short form
727 it_group = fullmatch.groupdict().get('it_' + ns)
728 if it_group:
729 alias = it_group.strip()
730 intertrac = self.env.config['intertrac']
731 target = '%s:%s' % (ns, target[len(it_group):])
732 return self._make_intertrac_link(intertrac.get(alias, alias),
733 target, label) or label
734
735 def _make_interwiki_link(self, ns, target, label):
736 from trac.wiki.interwiki import InterWikiMap
737 interwiki = InterWikiMap(self.env)
738 if ns in interwiki:
739 url, title = interwiki.url(ns, target)
740 if url:
741 return self._make_ext_link(url, label, title)
742
743 def _make_ext_link(self, url, text, title=''):
744 local_url = self.env.project_url or self.env.abs_href.base
745 if not url.startswith(local_url):
746 return tag.a(tag.span('\u200b', class_="icon"), text,
747 class_="ext-link", href=url, title=title or None)
748 else:
749 return tag.a(text, href=url, title=title or None)
750
751 def _make_mail_link(self, url, text, title=''):
752 return tag.a(tag.span('\u200b', class_="icon"), text,
753 class_="mail-link", href=url, title=title or None)
754
755 # Anchors
756
757 def _anchor_formatter(self, match, fullmatch):
758 anchor = fullmatch.group('anchorname')
759 label = fullmatch.group('anchorlabel') or ''
760 if label:
761 label = format_to_oneliner(self.env, self.context, label)
762 return '<span class="wikianchor" id="%s">%s</span>' % (anchor, label)
763
764 def _unique_anchor(self, anchor):
765 i = 1
766 anchor_base = anchor
767 while anchor in self._anchors:
768 anchor = anchor_base + str(i)
769 i += 1
770 self._anchors[anchor] = True
771 return anchor
772
773 # WikiMacros or WikiCreole links
774
775 def _macrolink_formatter(self, match, fullmatch):
776 # check for a known [[macro]]
777 macro_or_link = match[2:-2]
778 if macro_or_link.startswith('=#'):
779 fullmatch = WikiParser._set_anchor_wc_re.match(macro_or_link)
780 if fullmatch:
781 return self._anchor_formatter(macro_or_link, fullmatch)
782 fullmatch = WikiParser._macro_re.match(macro_or_link)
783 if fullmatch:
784 name = fullmatch.group('macroname')
785 args = fullmatch.group('macroargs')
786 macro = False # not a macro
787 macrolist = name[-1] == '?'
788 if name.lower() == 'br' or name == '?':
789 macro = None
790 else:
791 macro = WikiProcessor(self, (name, name[:-1])[macrolist])
792 if macro.error:
793 macro = False
794 if macro is not False:
795 if macrolist:
796 macro = WikiProcessor(self, 'MacroList')
797 return self._macro_formatter(match, fullmatch, macro)
798 fullmatch = WikiParser._creolelink_re.match(macro_or_link)
799 return self._lhref_formatter(match, fullmatch)
800
801 def _macro_formatter(self, match, fullmatch, macro, only_inline=False):
802 name = fullmatch.group('macroname')
803 if name and name[-1] == '?': # Macro?() shortcut for MacroList(Macro)
804 args = name[:-1] or '*'
805 else:
806 args = fullmatch.group('macroargs')
807 if name.lower() == 'br':
808 return self.emit_linebreak(args)
809 in_paragraph = not (getattr(self, 'in_list_item', True) or
810 getattr(self, 'in_table', True) or
811 getattr(self, 'in_def_list', True))
812 try:
813 return macro.ensure_inline(macro.process(args), in_paragraph)
814 except MacroError as e:
815 return system_message(_("Macro %(name)s(%(args)s) failed",
816 name=name, args=args), to_fragment(e))
817 except Exception as e:
818 self.env.log.error("Macro %s(%s) failed for %s:%s", name,
819 args, self.resource,
820 exception_to_unicode(e, traceback=True))
821 return system_message(_("Error: Macro %(name)s(%(args)s) failed",
822 name=name, args=args), to_fragment(e))
823 def emit_linebreak(self, args):
824 if args:
825 sep = ':' if ':' in args else '='
826 kv = args.split(sep, 1)
827 if kv[0] == 'clear':
828 clear = kv[-1] if kv[-1] in ['left', 'right'] else 'both'
829 return '<br style="clear: {0}" />'.format(clear)
830 return '<br />'
831
832 # Headings
833
834 def _parse_heading(self, match, fullmatch, shorten):
835 match = match.strip()
836
837 hdepth = fullmatch.group('hdepth')
838 depth = len(hdepth)
839 anchor = fullmatch.group('hanchor') or ''
840 htext = fullmatch.group('htext').strip()
841 if htext.endswith(hdepth):
842 htext = htext[:-depth]
843 heading = format_to_oneliner(self.env, self.context, htext, False)
844 if anchor:
845 anchor = anchor[1:]
846 else:
847 sans_markup = plaintext(heading, keeplinebreaks=False)
848 anchor = WikiParser._anchor_re.sub('', sans_markup)
849 if not anchor or anchor[0].isdigit() or anchor[0] in '.-':
850 # an ID must start with a Name-start character in XHTML
851 anchor = 'a' + anchor # keeping 'a' for backward compat
852 anchor = self._unique_anchor(anchor)
853 if shorten:
854 heading = format_to_oneliner(self.env, self.context, htext, True)
855 return depth, heading, anchor
856
857 def _heading_formatter(self, match, fullmatch):
858 self.close_table()
859 self.close_paragraph()
860 self.close_indentation()
861 self.close_list()
862 self.close_def_list()
863 depth, heading, anchor = self._parse_heading(match, fullmatch, False)
864 self.out.write('<h%d class="section" id="%s">%s</h%d>' %
865 (depth, anchor, heading, depth))
866
867 # Generic indentation (as defined by lists and quotes)
868
869 def _set_tab(self, depth):
870 """Append a new tab if needed and truncate tabs deeper than `depth`
871
872 given: -*-----*--*---*--
873 setting: *
874 results in: -*-----*-*-------
875 """
876 tabstops = []
877 for ts in self._tabstops:
878 if ts >= depth:
879 break
880 tabstops.append(ts)
881 tabstops.append(depth)
882 self._tabstops = tabstops
883
884 # Lists
885
886 def _list_formatter(self, match, fullmatch):
887 ldepth = len(fullmatch.group('ldepth'))
888 listid = match[ldepth]
889 self.in_list_item = True
890 class_ = start = None
891 if listid in WikiParser.BULLET_CHARS:
892 type_ = 'ul'
893 else:
894 type_ = 'ol'
895 lstart = fullmatch.group('lstart')
896 if listid == 'i':
897 class_ = 'lowerroman'
898 elif listid == 'I':
899 class_ = 'upperroman'
900 elif listid.isdigit() and lstart != '1':
901 start = int(lstart)
902 elif listid.islower():
903 class_ = 'loweralpha'
904 if len(lstart) == 1 and lstart != 'a':
905 start = ord(lstart) - ord('a') + 1
906 elif listid.isupper():
907 class_ = 'upperalpha'
908 if len(lstart) == 1 and lstart != 'A':
909 start = ord(lstart) - ord('A') + 1
910 self._set_list_depth(ldepth, type_, class_, start)
911 return ''
912
913 def _get_list_depth(self):
914 """Return the space offset associated to the deepest opened list."""
915 if self._list_stack:
916 return self._list_stack[-1][1]
917 return -1
918
919 def _set_list_depth(self, depth, new_type=None, lclass=None, start=None):
920 def open_list():
921 self.close_table()
922 self.close_paragraph()
923 self.close_indentation() # FIXME: why not lists in quotes?
924 self._list_stack.append((new_type, depth))
925 self._set_tab(depth)
926 class_attr = ' class="%s"' % lclass if lclass else ''
927 start_attr = ' start="%s"' % start if start is not None else ''
928 self.out.write('<' + new_type + class_attr + start_attr + '><li>')
929 def close_item():
930 self.flush_tags()
931 self.out.write('</li>')
932 def close_list(tp):
933 self._list_stack.pop()
934 close_item()
935 self.out.write('</%s>' % tp)
936
937 # depending on the indent/dedent, open or close lists
938 if depth > self._get_list_depth():
939 open_list()
940 else:
941 while self._list_stack:
942 deepest_type, deepest_offset = self._list_stack[-1]
943 if depth >= deepest_offset:
944 break
945 close_list(deepest_type)
946 if new_type and depth >= 0:
947 if self._list_stack:
948 old_type, old_offset = self._list_stack[-1]
949 if new_type and old_type != new_type:
950 close_list(old_type)
951 open_list()
952 else:
953 if old_offset != depth: # adjust last depth
954 self._list_stack[-1] = (old_type, depth)
955 close_item()
956 self.out.write('<li>')
957 else:
958 open_list()
959
960 def close_list(self, depth=-1):
961 self._set_list_depth(depth)
962
963 # Definition Lists
964
965 def _definition_formatter(self, match, fullmatch):
966 if self.in_def_list:
967 tmp = '</dd>'
968 else:
969 self.close_paragraph()
970 tmp = '<dl class="wiki">'
971 definition = match[:match.find('::')]
972 tmp += '<dt>%s</dt><dd>' % format_to_oneliner(self.env, self.context,
973 definition)
974 self.in_def_list = True
975 return tmp
976
977 def close_def_list(self):
978 if self.in_def_list:
979 self.out.write('</dd></dl>\n')
980 self.in_def_list = False
981
982 # Blockquote
983
984 def _indent_formatter(self, match, fullmatch):
985 idepth = len(fullmatch.group('idepth'))
986 if self._list_stack:
987 ltype, ldepth = self._list_stack[-1]
988 if idepth < ldepth:
989 for _, ldepth in self._list_stack:
990 if idepth > ldepth:
991 self.in_list_item = True
992 self._set_list_depth(idepth)
993 return ''
994 elif idepth <= ldepth + (3 if ltype == 'ol' else 2):
995 self.in_list_item = True
996 return ''
997 if not self.in_def_list:
998 self._set_quote_depth(idepth)
999 return ''
1000
1001 def close_indentation(self):
1002 self._set_quote_depth(0)
1003
1004 def _get_quote_depth(self):
1005 """Return the space offset associated to the deepest opened quote."""
1006 return self._quote_stack[-1] if self._quote_stack else 0
1007
1008 def _set_quote_depth(self, depth, citation=False):
1009 def open_quote(depth):
1010 self.close_table()
1011 self.close_paragraph()
1012 self.close_list()
1013 def open_one_quote(d):
1014 self._quote_stack.append(d)
1015 self._set_tab(d)
1016 class_attr = ' class="citation"' if citation else ''
1017 self.out.write('<blockquote%s>\n' % class_attr)
1018 if citation:
1019 for d in range(quote_depth+1, depth+1):
1020 open_one_quote(d)
1021 else:
1022 open_one_quote(depth)
1023 def close_quote():
1024 self.close_table()
1025 self.close_paragraph()
1026 self._quote_stack.pop()
1027 self.out.write('</blockquote>\n')
1028 quote_depth = self._get_quote_depth()
1029 if depth > quote_depth:
1030 self._set_tab(depth)
1031 tabstops = self._tabstops[::-1]
1032 while tabstops:
1033 tab = tabstops.pop()
1034 if tab > quote_depth:
1035 open_quote(tab)
1036 else:
1037 while self._quote_stack:
1038 deepest_offset = self._quote_stack[-1]
1039 if depth >= deepest_offset:
1040 break
1041 close_quote()
1042 if not citation and depth > 0:
1043 if self._quote_stack:
1044 old_offset = self._quote_stack[-1]
1045 if old_offset != depth: # adjust last depth
1046 self._quote_stack[-1] = depth
1047 else:
1048 open_quote(depth)
1049 if depth > 0:
1050 self.in_quote = True
1051
1052 # Table
1053
1054 def _table_cell_formatter(self, match, fullmatch):
1055 self.open_table()
1056 self.open_table_row()
1057 self.continue_table = 1
1058 separator = fullmatch.group('table_cell_sep')
1059 is_last = fullmatch.group('table_cell_last')
1060 numpipes = len(separator)
1061 cell = 'td'
1062 if separator[0] == '=':
1063 numpipes -= 1
1064 if separator[-1] == '=':
1065 numpipes -= 1
1066 cell = 'th'
1067 colspan = numpipes // 2
1068 if is_last is not None:
1069 if is_last and is_last[-1] == '\\':
1070 self.continue_table_row = 1
1071 colspan -= 1
1072 if not colspan:
1073 return ''
1074 attrs = ''
1075 if colspan > 1:
1076 attrs = ' colspan="%d"' % int(colspan)
1077 # alignment: ||left || right||default|| default || center ||
1078 after_sep = fullmatch.end('table_cell_sep')
1079 alignleft = after_sep < len(self.line) and self.line[after_sep] != ' '
1080 # lookahead next || (FIXME: this fails on ` || ` inside the cell)
1081 next_sep = re.search(r'([^!])=?\|\|', self.line[after_sep:])
1082 alignright = next_sep and next_sep.group(1) != ' '
1083 textalign = None
1084 if alignleft:
1085 if not alignright:
1086 textalign = 'left'
1087 elif alignright:
1088 textalign = 'right'
1089 elif next_sep: # check for the extra spaces specifying a center align
1090 first_extra = after_sep + 1
1091 last_extra = after_sep + next_sep.start() - 1
1092 if first_extra < last_extra and \
1093 self.line[first_extra] == self.line[last_extra] == ' ':
1094 textalign = 'center'
1095 if textalign:
1096 attrs += ' style="text-align: %s"' % textalign
1097 td = '<%s%s>' % (cell, attrs)
1098 if self.in_table_cell:
1099 close_tags = ''.join(self._get_close_tag(tag)
1100 for tag in self.pop_tags())
1101 td = '%s</%s>%s' % (close_tags, self.in_table_cell, td)
1102 self.in_table_cell = cell
1103 return td
1104
1105 def _table_row_sep_formatter(self, match, fullmatch):
1106 self.open_table()
1107 self.close_table_row(force=True)
1108 params = fullmatch.group('table_row_params')
1109 if params:
1110 tr = WikiProcessor(self, 'tr', self.parse_processor_args(params))
1111 processed = _markup_to_unicode(tr.process(''))
1112 params = processed[3:processed.find('>')]
1113 self.open_table_row(params or '')
1114 self.continue_table = 1
1115 self.continue_table_row = 1
1116
1117 def open_table(self):
1118 if not self.in_table:
1119 self.close_paragraph()
1120 self.close_list()
1121 self.close_def_list()
1122 self.in_table = 1
1123 self.out.write('<table class="wiki">\n')
1124
1125 def open_table_row(self, params=''):
1126 if not self.in_table_row:
1127 self.open_table()
1128 self.in_table_row = 1
1129 self.out.write('<tr%s>' % params)
1130
1131 def close_table_row(self, force=False):
1132 if self.in_table_row and (not self.continue_table_row or force):
1133 self.in_table_row = 0
1134 if self.in_table_cell:
1135 self.flush_tags()
1136 self.out.write('</%s>' % self.in_table_cell)
1137 self.in_table_cell = ''
1138 self.out.write('</tr>')
1139 self.continue_table_row = 0
1140
1141 def close_table(self):
1142 if self.in_table:
1143 self.close_table_row(force=True)
1144 self.out.write('</table>\n')
1145 self.in_table = 0
1146
1147 # Paragraphs
1148
1149 def open_paragraph(self):
1150 if not self.paragraph_open:
1151 self.out.write('<p>\n')
1152 self.paragraph_open = 1
1153
1154 def close_paragraph(self):
1155 self.flush_tags()
1156 if self.paragraph_open:
1157 self.out.write('</p>\n')
1158 self.paragraph_open = 0
1159
1160 # Code blocks
1161
1162 def parse_processor_args(self, line):
1163 return parse_processor_args(line)
1164
1165 def handle_code_block(self, line, startmatch=None):
1166 if startmatch:
1167 self.in_code_block += 1
1168 if self.in_code_block == 1:
1169 name = startmatch.group(2)
1170 if name:
1171 args = parse_processor_args(line[startmatch.end():])
1172 self.code_processor = WikiProcessor(self, name, args)
1173 else:
1174 self.code_processor = None
1175 self.code_buf = []
1176 self.code_prefix = line[:line.find(WikiParser.STARTBLOCK)]
1177 else:
1178 self.code_buf.append(line)
1179 if not self.code_processor:
1180 self.code_processor = WikiProcessor(self, 'default')
1181 elif line.strip() == WikiParser.ENDBLOCK:
1182 self.in_code_block -= 1
1183 if self.in_code_block == 0 and self.code_processor:
1184 if self.code_processor.name not in ('th', 'td', 'tr'):
1185 self.close_table()
1186 self.close_paragraph()
1187 if self.code_buf:
1188 if self.code_prefix and all(not l or
1189 l.startswith(self.code_prefix)
1190 for l in self.code_buf):
1191 code_indent = len(self.code_prefix)
1192 self.code_buf = [l[code_indent:]
1193 for l in self.code_buf]
1194 self.code_buf.append('')
1195 code_text = '\n'.join(self.code_buf)
1196 processed = self._exec_processor(self.code_processor,
1197 code_text)
1198 self.out.write(_markup_to_unicode(processed))
1199 else:
1200 self.code_buf.append(line)
1201 elif not self.code_processor:
1202 match = WikiParser._processor_re.match(line)
1203 if match:
1204 self.code_prefix = match.group(1)
1205 name = match.group(2)
1206 args = parse_processor_args(line[match.end():])
1207 self.code_processor = WikiProcessor(self, name, args)
1208 else:
1209 self.code_buf.append(line)
1210 self.code_processor = WikiProcessor(self, 'default')
1211 else:
1212 self.code_buf.append(line)
1213
1214 def close_code_blocks(self):
1215 while self.in_code_block > 0:
1216 self.handle_code_block(WikiParser.ENDBLOCK)
1217
1218 def _exec_processor(self, processor, text):
1219 try:
1220 return processor.process(text)
1221 except ProcessorError as e:
1222 return system_message(_("Processor %(name)s failed",
1223 name=processor.name), to_fragment(e))
1224 except Exception as e:
1225 self.env.log.error("Processor %s failed for %s:%s",
1226 processor.name, self.resource,
1227 exception_to_unicode(e, traceback=True))
1228 return system_message(_("Error: Processor %(name)s failed",
1229 name=processor.name), to_fragment(e))
1230
1231 # > quotes
1232
1233 def handle_quote_block(self, line):
1234 self.close_paragraph()
1235 depth = line.find('>')
1236 # Close lists up to current level:
1237 #
1238 # - first level item
1239 # - second level item
1240 # > citation part of first level item
1241 #
1242 # (depth == 3, _list_stack == [1, 3])
1243 if not self._quote_buffer and depth < self._get_list_depth():
1244 self.close_list(depth)
1245 self._quote_buffer.append(line[depth + 1:])
1246
1247 def close_quote_block(self, escape_newlines):
1248 if self._quote_buffer:
1249 # avoid an extra <blockquote> when there's consistently one space
1250 # after the '>'
1251 if all(not line or line[0] in '> ' for line in self._quote_buffer):
1252 self._quote_buffer = [line[bool(line and line[0] == ' '):]
1253 for line in self._quote_buffer]
1254 self.out.write('<blockquote class="citation">\n')
1255 Formatter(self.env, self.context).format(self._quote_buffer,
1256 self.out, escape_newlines)
1257 self.out.write('</blockquote>\n')
1258 self._quote_buffer = []
1259
1260 # -- Wiki engine
1261
1262 def handle_match(self, fullmatch):
1263 for itype, match in fullmatch.groupdict().items():
1264 if match and itype not in self.wikiparser.helper_patterns:
1265 # Check for preceding escape character '!'
1266 if match[0] == '!':
1267 return escape(match[1:])
1268 if itype in self.wikiparser.external_handlers:
1269 external_handler = self.wikiparser.external_handlers[itype]
1270 return external_handler(self, match, fullmatch)
1271 else:
1272 internal_handler = getattr(self, '_%s_formatter' % itype)
1273 return internal_handler(match, fullmatch)
1274
1275 def replace(self, fullmatch):
1276 """Replace one match with its corresponding expansion"""
1277 replacement = self.handle_match(fullmatch)
1278 if replacement:
1279 return _markup_to_unicode(replacement)
1280
1281 _normalize_re = re.compile(r'[\v\f]', re.UNICODE)
1282
1283 def reset(self, source, out=None):
1284 if isinstance(source, str):
1285 source = re.sub(self._normalize_re, ' ', source)
1286 self.source = source
1287 class NullOut(object):
1288 def write(self, data):
1289 pass
1290 self.out = out or NullOut()
1291 self._open_tags = []
1292 self._list_stack = []
1293 self._quote_stack = []
1294 self._tabstops = []
1295 self._quote_buffer = []
1296
1297 self.in_code_block = 0
1298 self.in_table = 0
1299 self.in_def_list = 0
1300 self.in_table_row = 0
1301 self.continue_table = 0
1302 self.continue_table_row = 0
1303 self.in_table_cell = ''
1304 self.paragraph_open = 0
1305 return source
1306
1307 def format(self, text, out=None, escape_newlines=False):
1308 text = self.reset(text, out)
1309 if isinstance(text, str):
1310 text = text.splitlines()
1311
1312 for line in text:
1313 # Detect start of code block (new block or embedded block)
1314 block_start_match = None
1315 if WikiParser.ENDBLOCK not in line:
1316 block_start_match = WikiParser._startblock_re.match(line)
1317 # Handle content or end of code block
1318 if self.in_code_block:
1319 self.handle_code_block(line, block_start_match)
1320 continue
1321 # Handle citation quotes '> ...'
1322 if line.strip().startswith('>'):
1323 self.handle_quote_block(line)
1324 continue
1325 # Handle end of citation quotes
1326 self.close_quote_block(escape_newlines)
1327 # Handle start of a new block
1328 if block_start_match:
1329 self.handle_code_block(line, block_start_match)
1330 continue
1331 # Handle Horizontal ruler
1332 if line[0:4] == '----':
1333 self.close_table()
1334 self.close_paragraph()
1335 self.close_indentation()
1336 self.close_list()
1337 self.close_def_list()
1338 self.out.write('<hr />\n')
1339 continue
1340 # Handle new paragraph
1341 if line == '':
1342 self.close_table()
1343 self.close_paragraph()
1344 self.close_indentation()
1345 self.close_list()
1346 self.close_def_list()
1347 continue
1348
1349 # Tab expansion and clear tabstops if no indent
1350 line = line.replace('\t', ' '*8)
1351 if not line.startswith(' '):
1352 self._tabstops = []
1353
1354 # Handle end of indentation
1355 if not line.startswith(' ') and self._quote_stack:
1356 self.close_indentation()
1357
1358 self.in_list_item = False
1359 self.in_quote = False
1360 # Throw a bunch of regexps on the problem
1361 self.line = line
1362 result = re.sub(self.wikiparser.rules, self.replace, line)
1363
1364 if not self.in_list_item:
1365 self.close_list()
1366
1367 if not self.in_quote:
1368 self.close_indentation()
1369
1370 if self.in_def_list and not line.startswith(' '):
1371 self.close_def_list()
1372
1373 if self.in_table and not self.continue_table:
1374 self.close_table()
1375 self.continue_table = 0
1376
1377 sep = '\n'
1378 if not(self.in_list_item or self.in_def_list or self.in_table):
1379 if len(result):
1380 self.open_paragraph()
1381 if escape_newlines and self.paragraph_open and \
1382 not result.rstrip().endswith('<br />'):
1383 sep = '<br />' + sep
1384 self.out.write(result + sep)
1385 self.close_table_row()
1386
1387 self.close_code_blocks()
1388 self.close_quote_block(escape_newlines)
1389 self.close_table()
1390 self.close_paragraph()
1391 self.close_indentation()
1392 self.close_list()
1393 self.close_def_list()
1394
1395
1396class OneLinerFormatter(Formatter):
1397 """
1398 A special version of the wiki formatter that only implement a
1399 subset of the wiki formatting functions. This version is useful
1400 for rendering short wiki-formatted messages on a single line
1401 """
1402 flavor = 'oneliner'
1403
1404 # Override a few formatters to disable some wiki syntax in "oneliner"-mode
1405 def _list_formatter(self, match, fullmatch):
1406 return match
1407 def _indent_formatter(self, match, fullmatch):
1408 return match
1409 def _citation_formatter(self, match, fullmatch):
1410 return escape(match, False)
1411 def _heading_formatter(self, match, fullmatch):
1412 return escape(match, False)
1413 def _definition_formatter(self, match, fullmatch):
1414 return escape(match, False)
1415 def _table_cell_formatter(self, match, fullmatch):
1416 return match
1417 def _table_row_sep_formatter(self, match, fullmatch):
1418 return ''
1419
1420 def _linebreak_wc_formatter(self, match, fullmatch):
1421 return ' '
1422
1423 def _macro_formatter(self, match, fullmatch, macro):
1424 name = fullmatch.group('macroname')
1425 if name.lower() == 'br':
1426 return ' '
1427 args = fullmatch.group('macroargs')
1428 if macro.is_inline(args):
1429 return Formatter._macro_formatter(self, match, fullmatch, macro)
1430 else:
1431 return '[[%s%s]]' % (name, '(...)' if args else '')
1432
1433 def format(self, text, out, shorten=False):
1434 if not text:
1435 return
1436 text = self.reset(text, out)
1437
1438 # Simplify code blocks
1439 in_code_block = 0
1440 processor = None
1441 buf = io.StringIO()
1442 for line in text.strip().splitlines():
1443 if WikiParser.ENDBLOCK not in line and \
1444 WikiParser._startblock_re.match(line):
1445 in_code_block += 1
1446 elif line.strip() == WikiParser.ENDBLOCK:
1447 if in_code_block:
1448 in_code_block -= 1
1449 if in_code_block == 0:
1450 if processor != 'comment':
1451 buf.write(' [...]\n')
1452 processor = None
1453 elif in_code_block:
1454 if not processor:
1455 if line.startswith('#!'):
1456 processor = line[2:].strip()
1457 else:
1458 buf.write(line + '\n')
1459 result = buf.getvalue()[:-len('\n')]
1460
1461 if shorten:
1462 result = shorten_line(result)
1463
1464 result = re.sub(self.wikiparser.rules, self.replace, result)
1465 result = result.replace('[...]', '[\u2026]')
1466 if result.endswith('...'):
1467 result = result[:-3] + '\u2026'
1468
1469 self.out.write(result)
1470 # Close all open 'one line'-tags
1471 self.flush_tags()
1472 # Flush unterminated code blocks
1473 if in_code_block > 0:
1474 self.out.write('[\u2026]')
1475
1476
1477class OutlineFormatter(Formatter):
1478 """Special formatter that generates an outline of all the headings."""
1479 flavor = 'outline'
1480
1481 # Avoid the possible side-effects of rendering WikiProcessors
1482 def _macro_formatter(self, match, fullmatch, macro):
1483 name = fullmatch.group('macroname')
1484 if name.lower() == 'br':
1485 return ' '
1486 args = fullmatch.group('macroargs')
1487 if macro.is_inline(args):
1488 return Formatter._macro_formatter(self, match, fullmatch, macro)
1489 return ''
1490
1491 def handle_code_block(self, line, startmatch=None):
1492 if WikiParser.ENDBLOCK not in line and \
1493 WikiParser._startblock_re.match(line):
1494 self.in_code_block += 1
1495 elif line.strip() == WikiParser.ENDBLOCK:
1496 self.in_code_block -= 1
1497
1498 def format(self, text, out, max_depth=6, min_depth=1, shorten=True):
1499 self.shorten = shorten
1500 whitespace_indent = ' '
1501 self.outline = []
1502 Formatter.format(self, text)
1503
1504 if min_depth > max_depth:
1505 min_depth, max_depth = max_depth, min_depth
1506 max_depth = min(6, max_depth)
1507 min_depth = max(1, min_depth)
1508
1509 curr_depth = min_depth - 1
1510 out.write('\n')
1511 for depth, anchor, text in self.outline:
1512 if depth < min_depth or depth > max_depth:
1513 continue
1514 if depth > curr_depth: # Deeper indent
1515 for i in range(curr_depth, depth):
1516 out.write(whitespace_indent * (2*i) + '<ol>\n' +
1517 whitespace_indent * (2*i+1) + '<li>\n')
1518 elif depth < curr_depth: # Shallower indent
1519 for i in range(curr_depth-1, depth-1, -1):
1520 out.write(whitespace_indent * (2*i+1) + '</li>\n' +
1521 whitespace_indent * (2*i) + '</ol>\n')
1522 out.write(whitespace_indent * (2*depth-1) + '</li>\n' +
1523 whitespace_indent * (2*depth-1) + '<li>\n')
1524 else: # Same indent
1525 out.write( whitespace_indent * (2*depth-1) + '</li>\n' +
1526 whitespace_indent * (2*depth-1) + '<li>\n')
1527 curr_depth = depth
1528 out.write(whitespace_indent * (2*depth) +
1529 '<a href="#%s">%s</a>\n' % (anchor, text))
1530 # Close out all indentation
1531 for i in range(curr_depth-1, min_depth-2, -1):
1532 out.write(whitespace_indent * (2*i+1) + '</li>\n' +
1533 whitespace_indent * (2*i) + '</ol>\n')
1534
1535 def _heading_formatter(self, match, fullmatch):
1536 depth, heading, anchor = self._parse_heading(match, fullmatch,
1537 self.shorten)
1538 heading = re.sub(r'</?a(?: .*?)?>', '', heading) # Strip out link tags
1539 self.outline.append((depth, anchor, heading))
1540
1541
1542class LinkFormatter(OutlineFormatter):
1543 """Special formatter that focuses on TracLinks."""
1544 flavor = 'link'
1545
1546 def _heading_formatter(self, match, fullmatch):
1547 return ''
1548
1549 def match(self, wikitext):
1550 """Return the Wiki match found at the beginning of the `wikitext`"""
1551 wikitext = self.reset(wikitext)
1552 self.line = wikitext
1553 match = re.match(self.wikiparser.rules, wikitext)
1554 if match:
1555 return self.handle_match(match)
1556
1557
1558# Pure Wiki Formatter
1559
1560class HtmlFormatter(object):
1561 """Format parsed wiki text to HTML"""
1562
1563 flavor = 'default'
1564
1565 def __init__(self, env, context, wikidom):
1566 self.env = env
1567 self.context = context
1568 if isinstance(wikidom, str):
1569 wikidom = WikiParser(env).parse(wikidom)
1570 self.wikidom = wikidom
1571
1572 def generate(self, escape_newlines=False):
1573 """Generate HTML elements.
1574
1575 newlines in the wikidom will be preserved if `escape_newlines` is set.
1576 """
1577 # FIXME: compatibility code only for now
1578 out = io.StringIO()
1579 Formatter(self.env, self.context).format(self.wikidom, out,
1580 escape_newlines)
1581 return Markup(out.getvalue())
1582
1583
1584class InlineHtmlFormatter(object):
1585 """Format parsed wiki text to inline elements HTML.
1586
1587 Block level content will be discarded or compacted.
1588 """
1589
1590 flavor = 'oneliner'
1591
1592 def __init__(self, env, context, wikidom):
1593 self.env = env
1594 self.context = context
1595 if isinstance(wikidom, str):
1596 wikidom = WikiParser(env).parse(wikidom)
1597 self.wikidom = wikidom
1598
1599 def generate(self, shorten=False):
1600 """Generate HTML inline elements.
1601
1602 If `shorten` is set, the generation will stop once enough characters
1603 have been emitted.
1604 """
1605 # FIXME: compatibility code only for now
1606 out = io.StringIO()
1607 OneLinerFormatter(self.env, self.context).format(self.wikidom, out,
1608 shorten)
1609 return Markup(out.getvalue())
1610
1611
1612def format_to(env, flavor, context, wikidom, **options):
1613 if flavor is None:
1614 flavor = context.get_hint('wiki_flavor', 'html')
1615 if flavor == 'oneliner':
1616 return format_to_oneliner(env, context, wikidom, **options)
1617 else:
1618 return format_to_html(env, context, wikidom, **options)
1619
1620def format_to_html(env, context, wikidom, escape_newlines=None):
1621 if not wikidom:
1622 return Markup()
1623 if escape_newlines is None:
1624 escape_newlines = context.get_hint('preserve_newlines', False)
1625 return HtmlFormatter(env, context, wikidom).generate(escape_newlines)
1626
1627def format_to_oneliner(env, context, wikidom, shorten=None):
1628 if not wikidom:
1629 return Markup()
1630 if shorten is None:
1631 shorten = context.get_hint('shorten_lines', False)
1632 return InlineHtmlFormatter(env, context, wikidom).generate(shorten)
1633
1634def extract_link(env, context, wikidom):
1635 if not wikidom:
1636 return Markup()
1637 return LinkFormatter(env, context).match(wikidom)
1638
1639
1640# pre-0.11 wiki text to Markup compatibility methods
1641
1642def wiki_to_outline(wikitext, env, db=None,
1643 absurls=False, max_depth=None, min_depth=None, req=None):
1644 """:deprecated: will be removed in 1.0 and replaced by something else"""
1645 if not wikitext:
1646 return Markup()
1647 abs_ref, href = (req or env).abs_href, (req or env).href
1648 from trac.web.chrome import web_context
1649 context = web_context(req, absurls=absurls)
1650 out = io.StringIO()
1651 OutlineFormatter(env, context).format(wikitext, out, max_depth, min_depth)
1652 return Markup(out.getvalue())
Note: See TracBrowser for help on using the repository browser.