Mercurial > genshi > mirror
diff examples/trac/trac/wiki/formatter.py @ 39:93b4dcbafd7b trunk
Copy Trac to main branch.
author | cmlenz |
---|---|
date | Mon, 03 Jul 2006 18:53:27 +0000 |
parents | |
children |
line wrap: on
line diff
new file mode 100644 --- /dev/null +++ b/examples/trac/trac/wiki/formatter.py @@ -0,0 +1,1010 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2003-2006 Edgewall Software +# Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com> +# Copyright (C) 2004-2005 Christopher Lenz <cmlenz@gmx.de> +# Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr> +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://trac.edgewall.com/license.html. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://projects.edgewall.com/trac/. +# +# Author: Jonas Borgström <jonas@edgewall.com> +# Christopher Lenz <cmlenz@gmx.de> +# Christian Boos <cboos@neuf.fr> + +import re +import os +import urllib + +from StringIO import StringIO + +from trac.core import * +from trac.mimeview import * +from trac.wiki.api import WikiSystem +from trac.util.text import shorten_line, to_unicode +from trac.util.markup import escape, Markup, Element, html + +__all__ = ['wiki_to_html', 'wiki_to_oneliner', 'wiki_to_outline', + 'wiki_to_link', 'Formatter' ] + + +def system_message(msg, text=None): + return html.DIV(html.STRONG(msg), text and html.PRE(text), + class_="system-message") + + +class WikiProcessor(object): + + _code_block_re = re.compile('^<div(?:\s+class="([^"]+)")?>(.*)</div>$') + + def __init__(self, env, name): + # TODO: transmit `formatter` argument + self.env = env + self.name = name + self.error = None + self.macro_provider = None + + builtin_processors = {'html': self._html_processor, + 'default': self._default_processor, + 'comment': self._comment_processor} + + self.processor = builtin_processors.get(name) + if not self.processor: + # Find a matching wiki macro + for macro_provider in WikiSystem(self.env).macro_providers: + for macro_name in macro_provider.get_macros(): + if self.name == macro_name: + self.processor = self._macro_processor + self.macro_provider = macro_provider + break + if not self.processor: + # Find a matching mimeview renderer + from trac.mimeview.api import Mimeview + mimetype = Mimeview(self.env).get_mimetype(self.name) + if mimetype: + self.name = mimetype + self.processor = self._mimeview_processor + else: + self.processor = self._default_processor + self.error = "No macro or processor named '%s' found" % name + + # builtin processors + + def _comment_processor(self, req, text): + return '' + + def _default_processor(self, req, text): + return html.PRE(text, class_="wiki") + + def _html_processor(self, req, text): + from HTMLParser import HTMLParseError + try: + return Markup(text).sanitize() + except HTMLParseError, e: + self.env.log.warn(e) + return system_message('HTML parsing error: %s' % escape(e.msg), + text.splitlines()[e.lineno - 1].strip()) + + # generic processors + + def _macro_processor(self, req, text): + # TODO: macro should take a `formatter` argument + self.env.log.debug('Executing Wiki macro %s by provider %s' + % (self.name, self.macro_provider)) + return self.macro_provider.render_macro(req, self.name, text) + + def _mimeview_processor(self, req, text): + # TODO: transmit context from `formatter` + return Mimeview(self.env).render(req, self.name, text) + + def process(self, req, text, in_paragraph=False): + if self.error: + text = system_message(Markup('Error: Failed to load processor ' + '<code>%s</code>', self.name), + self.error) + else: + text = self.processor(req, text) + if in_paragraph: + content_for_span = None + interrupt_paragraph = False + if isinstance(text, Element): + tagname = text.tagname.lower() + if tagname == 'div': + class_ = text.attr.get('class_', '') + if class_ and 'code' in class_: + content_for_span = text.children + else: + interrupt_paragraph = True + elif tagname == 'table': + interrupt_paragraph = True + else: + match = re.match(self._code_block_re, text) + if match: + if match.group(1) and 'code' in match.group(1): + content_for_span = match.group(2) + else: + interrupt_paragraph = True + elif text.startswith('<table'): + interrupt_paragraph = True + if content_for_span: + text = html.SPAN(content_for_span, class_='code-block') + elif interrupt_paragraph: + text = "</p>%s<p>" % to_unicode(text) + return text + + +class Formatter(object): + flavor = 'default' + + # Some constants used for clarifying the Wiki regexps: + + BOLDITALIC_TOKEN = "'''''" + BOLD_TOKEN = "'''" + ITALIC_TOKEN = "''" + UNDERLINE_TOKEN = "__" + STRIKE_TOKEN = "~~" + SUBSCRIPT_TOKEN = ",," + SUPERSCRIPT_TOKEN = r"\^" + INLINE_TOKEN = "`" + STARTBLOCK_TOKEN = r"\{\{\{" + STARTBLOCK = "{{{" + ENDBLOCK_TOKEN = r"\}\}\}" + ENDBLOCK = "}}}" + + LINK_SCHEME = r"[\w.+-]+" # as per RFC 2396 + INTERTRAC_SCHEME = r"[a-zA-Z.+-]*?" # no digits (support for shorthand links) + + QUOTED_STRING = r"'[^']+'|\"[^\"]+\"" + + SHREF_TARGET_FIRST = r"[\w/?!#@]" + SHREF_TARGET_MIDDLE = r"(?:\|(?=[^|\s])|[^|<>\s])" + SHREF_TARGET_LAST = r"[a-zA-Z0-9/=]" # we don't want "_" + + LHREF_RELATIVE_TARGET = r"[/.][^\s[\]]*" + + # Sequence of regexps used by the engine + + _pre_rules = [ + # Font styles + r"(?P<bolditalic>!?%s)" % BOLDITALIC_TOKEN, + r"(?P<bold>!?%s)" % BOLD_TOKEN, + r"(?P<italic>!?%s)" % ITALIC_TOKEN, + r"(?P<underline>!?%s)" % UNDERLINE_TOKEN, + r"(?P<strike>!?%s)" % STRIKE_TOKEN, + r"(?P<subscript>!?%s)" % SUBSCRIPT_TOKEN, + r"(?P<superscript>!?%s)" % SUPERSCRIPT_TOKEN, + r"(?P<inlinecode>!?%s(?P<inline>.*?)%s)" \ + % (STARTBLOCK_TOKEN, ENDBLOCK_TOKEN), + r"(?P<inlinecode2>!?%s(?P<inline2>.*?)%s)" \ + % (INLINE_TOKEN, INLINE_TOKEN)] + + # Rules provided by IWikiSyntaxProviders will be inserted here + + _post_rules = [ + # > ... + r"(?P<citation>^(?P<cdepth>>(?: *>)*))", + # &, < and > to &, < and > + r"(?P<htmlescape>[&<>])", + # wiki:TracLinks + r"(?P<shref>!?((?P<sns>%s):(?P<stgt>%s|%s(?:%s*%s)?)))" \ + % (LINK_SCHEME, QUOTED_STRING, + SHREF_TARGET_FIRST, SHREF_TARGET_MIDDLE, SHREF_TARGET_LAST), + # [[macro]] call + (r"(?P<macro>!?\[\[(?P<macroname>[\w/+-]+)" + r"(\]\]|\((?P<macroargs>.*?)\)\]\]))"), + # [wiki:TracLinks with label] + (r"(?P<lhref>!?\[(?:" + r"(?P<rel>%s)|" % LHREF_RELATIVE_TARGET + # ./... or /... + r"(?:(?P<lns>%s):)?(?P<ltgt>%s|[^\]\s]*))" % \ + (LINK_SCHEME, QUOTED_STRING) + # wiki:TracLinks or wiki:"trac links" + r"(?:\s+(?P<label>%s|[^\]]+))?\])" % QUOTED_STRING), # label + # == heading == #hanchor + r"(?P<heading>^\s*(?P<hdepth>=+)\s.*\s(?P=hdepth)\s*" + r"(?P<hanchor>#[\w:](?<!\d)[\w:.-]*)?$)", + # * list + r"(?P<list>^(?P<ldepth>\s+)(?:[-*]|\d+\.|[a-zA-Z]\.|[ivxIVX]{1,5}\.) )", + # definition:: + r"(?P<definition>^\s+((?:%s.*?%s|%s.*?%s|[^%s%s])+?::)(?:\s+|$))" + % (INLINE_TOKEN, INLINE_TOKEN, STARTBLOCK_TOKEN, ENDBLOCK_TOKEN, + INLINE_TOKEN, STARTBLOCK[0]), + # (leading space) + r"(?P<indent>^(?P<idepth>\s+)(?=\S))", + # || table || + r"(?P<last_table_cell>\|\|\s*$)", + r"(?P<table_cell>\|\|)"] + + _processor_re = re.compile('#\!([\w+-][\w+-/]*)') + _anchor_re = re.compile('[^\w:.-]+', re.UNICODE) + + def __init__(self, env, req=None, absurls=False, db=None): + self.env = env + self.req = req + self._db = db + self._absurls = absurls + self._anchors = {} + self._open_tags = [] + self.href = absurls and (req or env).abs_href or (req or env).href + self._local = env.config.get('project', 'url') \ + or (req or env).abs_href.base + self.wiki = WikiSystem(self.env) + + def _get_db(self): + if not self._db: + self._db = self.env.get_db_cnx() + return self._db + db = property(fget=_get_db) + + def split_link(self, target): + """Split a target along "?" and "#" in `(path, query, fragment)`.""" + query = fragment = '' + idx = target.find('#') + if idx >= 0: + target, fragment = target[:idx], target[idx:] + idx = target.find('?') + if idx >= 0: + target, query = target[:idx], target[idx:] + return (target, query, fragment) + + # -- Pre- IWikiSyntaxProvider rules (Font styles) + + def tag_open_p(self, tag): + """Do we currently have any open tag with `tag` as end-tag?""" + return tag in self._open_tags + + def close_tag(self, tag): + tmp = '' + for i in xrange(len(self._open_tags)-1, -1, -1): + tmp += self._open_tags[i][1] + if self._open_tags[i][1] == tag: + del self._open_tags[i] + for j in xrange(i, len(self._open_tags)): + tmp += self._open_tags[j][0] + break + return tmp + + def open_tag(self, open, close): + self._open_tags.append((open, close)) + + def simple_tag_handler(self, match, open_tag, close_tag): + """Generic handler for simple binary style tags""" + if match[0] == '!': + return match[1:] + if self.tag_open_p((open_tag, close_tag)): + return self.close_tag(close_tag) + else: + self.open_tag(open_tag, close_tag) + return open_tag + + def _bolditalic_formatter(self, match, fullmatch): + if match[0] == '!': + return match[1:] + italic = ('<i>', '</i>') + italic_open = self.tag_open_p(italic) + tmp = '' + if italic_open: + tmp += italic[1] + self.close_tag(italic[1]) + tmp += self._bold_formatter(match, fullmatch) + if not italic_open: + tmp += italic[0] + self.open_tag(*italic) + return tmp + + def _bold_formatter(self, match, fullmatch): + return self.simple_tag_handler(match, '<strong>', '</strong>') + + def _italic_formatter(self, match, fullmatch): + return self.simple_tag_handler(match, '<i>', '</i>') + + def _underline_formatter(self, match, fullmatch): + return self.simple_tag_handler(match, '<span class="underline">', + '</span>') + + def _strike_formatter(self, match, fullmatch): + return self.simple_tag_handler(match, '<del>', '</del>') + + def _subscript_formatter(self, match, fullmatch): + return self.simple_tag_handler(match, '<sub>', '</sub>') + + def _superscript_formatter(self, match, fullmatch): + return self.simple_tag_handler(match, '<sup>', '</sup>') + + def _inlinecode_formatter(self, match, fullmatch): + return html.TT(fullmatch.group('inline')) + + def _inlinecode2_formatter(self, match, fullmatch): + return html.TT(fullmatch.group('inline2')) + + # -- Post- IWikiSyntaxProvider rules + + # HTML escape of &, < and > + + def _htmlescape_formatter(self, match, fullmatch): + return match == "&" and "&" or match == "<" and "<" or ">" + + # Short form (shref) and long form (lhref) of TracLinks + + def _unquote(self, text): + if text and text[0] in "'\"" and text[0] == text[-1]: + return text[1:-1] + else: + return text + + def _shref_formatter(self, match, fullmatch): + ns = fullmatch.group('sns') + target = self._unquote(fullmatch.group('stgt')) + return self._make_link(ns, target, match, match) + + def _lhref_formatter(self, match, fullmatch): + rel = fullmatch.group('rel') + ns = fullmatch.group('lns') or (not rel and 'wiki') + target = self._unquote(fullmatch.group('ltgt')) + label = fullmatch.group('label') + if not label: # e.g. `[http://target]` or `[wiki:target]` + if target: + if target.startswith('//'): # for `[http://target]` + label = ns+':'+target # use `http://target` + else: # for `wiki:target` + label = target # use only `target` + else: # e.g. `[search:]` + label = ns + else: + label = self._unquote(label) + if rel: + return self._make_relative_link(rel, label or rel) + else: + return self._make_link(ns, target, match, label) + + def _make_link(self, ns, target, match, label): + # first check for an alias defined in trac.ini + ns = self.env.config.get('intertrac', ns) or ns + if ns in self.wiki.link_resolvers: + return self.wiki.link_resolvers[ns](self, ns, target, + escape(label, False)) + elif target.startswith('//') or ns == "mailto": + return self._make_ext_link(ns+':'+target, label) + else: + return self._make_intertrac_link(ns, target, label) or \ + self._make_interwiki_link(ns, target, label) or \ + match + + def _make_intertrac_link(self, ns, target, label): + url = self.env.config.get('intertrac', ns + '.url') + if url: + name = self.env.config.get('intertrac', ns + '.title', + 'Trac project %s' % ns) + sep = target.find(':') + if sep != -1: + url = '%s/%s/%s' % (url, target[:sep], target[sep + 1:]) + else: + url = '%s/search?q=%s' % (url, urllib.quote_plus(target)) + return self._make_ext_link(url, label, '%s in %s' % (target, name)) + else: + return None + + def shorthand_intertrac_helper(self, ns, target, label, fullmatch): + if fullmatch: # short form + it_group = fullmatch.group('it_%s' % ns) + if it_group: + alias = it_group.strip() + intertrac = self.env.config.get('intertrac', alias) or alias + target = '%s:%s' % (ns, target[len(it_group):]) + return self._make_intertrac_link(intertrac, target, label) or \ + label + return None + + def _make_interwiki_link(self, ns, target, label): + from trac.wiki.interwiki import InterWikiMap + interwiki = InterWikiMap(self.env) + if ns in interwiki: + url, title = interwiki.url(ns, target) + return self._make_ext_link(url, label, title) + else: + return None + + def _make_ext_link(self, url, text, title=''): + if not url.startswith(self._local): + return html.A(html.SPAN(text, class_="icon"), + class_="ext-link", href=url, title=title or None) + else: + return html.A(text, href=url, title=title or None) + + def _make_relative_link(self, url, text): + if url.startswith('//'): # only the protocol will be kept + return html.A(text, class_="ext-link", href=url) + else: + return html.A(text, href=url) + + # WikiMacros + + def _macro_formatter(self, match, fullmatch): + name = fullmatch.group('macroname') + if name.lower() == 'br': + return '<br />' + args = fullmatch.group('macroargs') + try: + macro = WikiProcessor(self.env, name) + return macro.process(self.req, args, True) + except Exception, e: + self.env.log.error('Macro %s(%s) failed' % (name, args), + exc_info=True) + return system_message('Error: Macro %s(%s) failed' % (name, args), + e) + + # Headings + + def _parse_heading(self, match, fullmatch, shorten): + match = match.strip() + + depth = min(len(fullmatch.group('hdepth')), 5) + anchor = fullmatch.group('hanchor') or '' + heading = match[depth+1:-depth-1-len(anchor)] + heading = wiki_to_oneliner(heading, self.env, self.db, shorten, + self._absurls) + if anchor: + anchor = anchor[1:] + else: + sans_markup = heading.plaintext(keeplinebreaks=False) + anchor = self._anchor_re.sub('', sans_markup) + if not anchor or anchor[0].isdigit() or anchor[0] in '.-': + # an ID must start with a Name-start character in XHTML + anchor = 'a' + anchor # keeping 'a' for backward compat + i = 1 + anchor_base = anchor + while anchor in self._anchors: + anchor = anchor_base + str(i) + i += 1 + self._anchors[anchor] = True + return (depth, heading, anchor) + + def _heading_formatter(self, match, fullmatch): + self.close_table() + self.close_paragraph() + self.close_indentation() + self.close_list() + self.close_def_list() + depth, heading, anchor = self._parse_heading(match, fullmatch, False) + self.out.write('<h%d id="%s">%s</h%d>' % + (depth, anchor, heading, depth)) + + # Generic indentation (as defined by lists and quotes) + + def _set_tab(self, depth): + """Append a new tab if needed and truncate tabs deeper than `depth` + + given: -*-----*--*---*-- + setting: * + results in: -*-----*-*------- + """ + tabstops = [] + for ts in self._tabstops: + if ts >= depth: + break + tabstops.append(ts) + tabstops.append(depth) + self._tabstops = tabstops + + # Lists + + def _list_formatter(self, match, fullmatch): + ldepth = len(fullmatch.group('ldepth')) + listid = match[ldepth] + self.in_list_item = True + class_ = start = None + if listid in '-*': + type_ = 'ul' + else: + type_ = 'ol' + idx = '01iI'.find(listid) + if idx >= 0: + class_ = ('arabiczero', None, 'lowerroman', 'upperroman')[idx] + elif listid.isdigit(): + start = match[ldepth:match.find('.')] + elif listid.islower(): + class_ = 'loweralpha' + elif listid.isupper(): + class_ = 'upperalpha' + self._set_list_depth(ldepth, type_, class_, start) + return '' + + def _get_list_depth(self): + """Return the space offset associated to the deepest opened list.""" + return self._list_stack and self._list_stack[-1][1] or 0 + + def _set_list_depth(self, depth, new_type, list_class, start): + def open_list(): + self.close_table() + self.close_paragraph() + self.close_indentation() # FIXME: why not lists in quotes? + self._list_stack.append((new_type, depth)) + self._set_tab(depth) + class_attr = list_class and ' class="%s"' % list_class or '' + start_attr = start and ' start="%s"' % start or '' + self.out.write('<'+new_type+class_attr+start_attr+'><li>') + def close_list(tp): + self._list_stack.pop() + self.out.write('</li></%s>' % tp) + + # depending on the indent/dedent, open or close lists + if depth > self._get_list_depth(): + open_list() + else: + while self._list_stack: + deepest_type, deepest_offset = self._list_stack[-1] + if depth >= deepest_offset: + break + close_list(deepest_type) + if depth > 0: + if self._list_stack: + old_type, old_offset = self._list_stack[-1] + if new_type and old_type != new_type: + close_list(old_type) + open_list() + else: + if old_offset != depth: # adjust last depth + self._list_stack[-1] = (old_type, depth) + self.out.write('</li><li>') + else: + open_list() + + def close_list(self): + self._set_list_depth(0, None, None, None) + + # Definition Lists + + def _definition_formatter(self, match, fullmatch): + tmp = self.in_def_list and '</dd>' or '<dl>' + definition = match[:match.find('::')] + tmp += '<dt>%s</dt><dd>' % wiki_to_oneliner(definition, self.env, + self.db) + self.in_def_list = True + return tmp + + def close_def_list(self): + if self.in_def_list: + self.out.write('</dd></dl>\n') + self.in_def_list = False + + # Blockquote + + def _indent_formatter(self, match, fullmatch): + idepth = len(fullmatch.group('idepth')) + if self._list_stack: + ltype, ldepth = self._list_stack[-1] + if idepth < ldepth: + for _, ldepth in self._list_stack: + if idepth > ldepth: + self.in_list_item = True + self._set_list_depth(idepth, None, None, None) + return '' + elif idepth <= ldepth + (ltype == 'ol' and 3 or 2): + self.in_list_item = True + return '' + if not self.in_def_list: + self._set_quote_depth(idepth) + return '' + + def _citation_formatter(self, match, fullmatch): + cdepth = len(fullmatch.group('cdepth').replace(' ', '')) + self._set_quote_depth(cdepth, True) + return '' + + def close_indentation(self): + self._set_quote_depth(0) + + def _get_quote_depth(self): + """Return the space offset associated to the deepest opened quote.""" + return self._quote_stack and self._quote_stack[-1] or 0 + + def _set_quote_depth(self, depth, citation=False): + def open_quote(depth): + self.close_table() + self.close_paragraph() + self.close_list() + def open_one_quote(d): + self._quote_stack.append(d) + self._set_tab(d) + class_attr = citation and ' class="citation"' or '' + self.out.write('<blockquote%s>' % class_attr + os.linesep) + if citation: + for d in range(quote_depth+1, depth+1): + open_one_quote(d) + else: + open_one_quote(depth) + def close_quote(): + self.close_table() + self.close_paragraph() + self._quote_stack.pop() + self.out.write('</blockquote>' + os.linesep) + quote_depth = self._get_quote_depth() + if depth > quote_depth: + self._set_tab(depth) + tabstops = self._tabstops[::-1] + while tabstops: + tab = tabstops.pop() + if tab > quote_depth: + open_quote(tab) + else: + while self._quote_stack: + deepest_offset = self._quote_stack[-1] + if depth >= deepest_offset: + break + close_quote() + if not citation and depth > 0: + if self._quote_stack: + old_offset = self._quote_stack[-1] + if old_offset != depth: # adjust last depth + self._quote_stack[-1] = depth + else: + open_quote(depth) + if depth > 0: + self.in_quote = True + + # Table + + def _last_table_cell_formatter(self, match, fullmatch): + return '' + + def _table_cell_formatter(self, match, fullmatch): + self.open_table() + self.open_table_row() + if self.in_table_cell: + return '</td><td>' + else: + self.in_table_cell = 1 + return '<td>' + + def open_table(self): + if not self.in_table: + self.close_paragraph() + self.close_list() + self.close_def_list() + self.in_table = 1 + self.out.write('<table class="wiki">' + os.linesep) + + def open_table_row(self): + if not self.in_table_row: + self.open_table() + self.in_table_row = 1 + self.out.write('<tr>') + + def close_table_row(self): + if self.in_table_row: + self.in_table_row = 0 + if self.in_table_cell: + self.in_table_cell = 0 + self.out.write('</td>') + + self.out.write('</tr>') + + def close_table(self): + if self.in_table: + self.close_table_row() + self.out.write('</table>' + os.linesep) + self.in_table = 0 + + # Paragraphs + + def open_paragraph(self): + if not self.paragraph_open: + self.out.write('<p>' + os.linesep) + self.paragraph_open = 1 + + def close_paragraph(self): + if self.paragraph_open: + while self._open_tags != []: + self.out.write(self._open_tags.pop()[1]) + self.out.write('</p>' + os.linesep) + self.paragraph_open = 0 + + # Code blocks + + def handle_code_block(self, line): + if line.strip() == Formatter.STARTBLOCK: + self.in_code_block += 1 + if self.in_code_block == 1: + self.code_processor = None + self.code_text = '' + else: + self.code_text += line + os.linesep + if not self.code_processor: + self.code_processor = WikiProcessor(self.env, 'default') + elif line.strip() == Formatter.ENDBLOCK: + self.in_code_block -= 1 + if self.in_code_block == 0 and self.code_processor: + self.close_table() + self.close_paragraph() + self.out.write(to_unicode(self.code_processor.process( + self.req, self.code_text))) + else: + self.code_text += line + os.linesep + elif not self.code_processor: + match = Formatter._processor_re.search(line) + if match: + name = match.group(1) + self.code_processor = WikiProcessor(self.env, name) + else: + self.code_text += line + os.linesep + self.code_processor = WikiProcessor(self.env, 'default') + else: + self.code_text += line + os.linesep + + def close_code_blocks(self): + while self.in_code_block > 0: + self.handle_code_block(Formatter.ENDBLOCK) + + # -- Wiki engine + + def handle_match(self, fullmatch): + for itype, match in fullmatch.groupdict().items(): + if match and not itype in self.wiki.helper_patterns: + # Check for preceding escape character '!' + if match[0] == '!': + return escape(match[1:]) + if itype in self.wiki.external_handlers: + external_handler = self.wiki.external_handlers[itype] + return external_handler(self, match, fullmatch) + else: + internal_handler = getattr(self, '_%s_formatter' % itype) + return internal_handler(match, fullmatch) + + def replace(self, fullmatch): + """Replace one match with its corresponding expansion""" + replacement = self.handle_match(fullmatch) + if replacement: + return to_unicode(replacement) + + def reset(self, out=None): + class NullOut(object): + def write(self, data): pass + self.out = out or NullOut() + self._open_tags = [] + self._list_stack = [] + self._quote_stack = [] + self._tabstops = [] + + self.in_code_block = 0 + self.in_table = 0 + self.in_def_list = 0 + self.in_table_row = 0 + self.in_table_cell = 0 + self.paragraph_open = 0 + + def format(self, text, out=None, escape_newlines=False): + self.reset(out) + for line in text.splitlines(): + # Handle code block + if self.in_code_block or line.strip() == Formatter.STARTBLOCK: + self.handle_code_block(line) + continue + # Handle Horizontal ruler + elif line[0:4] == '----': + self.close_table() + self.close_paragraph() + self.close_indentation() + self.close_list() + self.close_def_list() + self.out.write('<hr />' + os.linesep) + continue + # Handle new paragraph + elif line == '': + self.close_paragraph() + self.close_indentation() + self.close_list() + self.close_def_list() + continue + + # Tab expansion and clear tabstops if no indent + line = line.replace('\t', ' '*8) + if not line.startswith(' '): + self._tabstops = [] + + if escape_newlines: + line += ' [[BR]]' + self.in_list_item = False + self.in_quote = False + # Throw a bunch of regexps on the problem + result = re.sub(self.wiki.rules, self.replace, line) + + if not self.in_list_item: + self.close_list() + + if not self.in_quote: + self.close_indentation() + + if self.in_def_list and not line.startswith(' '): + self.close_def_list() + + if self.in_table and line.strip()[0:2] != '||': + self.close_table() + + if len(result) and not self.in_list_item and not self.in_def_list \ + and not self.in_table: + self.open_paragraph() + self.out.write(result + os.linesep) + self.close_table_row() + + self.close_table() + self.close_paragraph() + self.close_indentation() + self.close_list() + self.close_def_list() + self.close_code_blocks() + + +class OneLinerFormatter(Formatter): + """ + A special version of the wiki formatter that only implement a + subset of the wiki formatting functions. This version is useful + for rendering short wiki-formatted messages on a single line + """ + flavor = 'oneliner' + + def __init__(self, env, absurls=False, db=None): + Formatter.__init__(self, env, None, absurls, db) + + # Override a few formatters to disable some wiki syntax in "oneliner"-mode + def _list_formatter(self, match, fullmatch): return match + def _indent_formatter(self, match, fullmatch): return match + def _citation_formatter(self, match, fullmatch): + return escape(match, False) + def _heading_formatter(self, match, fullmatch): + return escape(match, False) + def _definition_formatter(self, match, fullmatch): + return escape(match, False) + def _table_cell_formatter(self, match, fullmatch): return match + def _last_table_cell_formatter(self, match, fullmatch): return match + + def _macro_formatter(self, match, fullmatch): + name = fullmatch.group('macroname') + if name.lower() == 'br': + return ' ' + elif name == 'comment': + return '' + else: + args = fullmatch.group('macroargs') + return '[[%s%s]]' % (name, args and '(...)' or '') + + def format(self, text, out, shorten=False): + if not text: + return + self.out = out + self._open_tags = [] + + # Simplify code blocks + in_code_block = 0 + processor = None + buf = StringIO() + for line in text.strip().splitlines(): + if line.strip() == Formatter.STARTBLOCK: + in_code_block += 1 + elif line.strip() == Formatter.ENDBLOCK: + if in_code_block: + in_code_block -= 1 + if in_code_block == 0: + if processor != 'comment': + buf.write(' ![...]' + os.linesep) + processor = None + elif in_code_block: + if not processor: + if line.startswith('#!'): + processor = line[2:].strip() + else: + buf.write(line + os.linesep) + result = buf.getvalue()[:-1] + + if shorten: + result = shorten_line(result) + + result = re.sub(self.wiki.rules, self.replace, result) + result = result.replace('[...]', '[…]') + if result.endswith('...'): + result = result[:-3] + '…' + + # Close all open 'one line'-tags + result += self.close_tag(None) + # Flush unterminated code blocks + if in_code_block > 0: + result += '[…]' + out.write(result) + + +class OutlineFormatter(Formatter): + """Special formatter that generates an outline of all the headings.""" + flavor = 'outline' + + def __init__(self, env, absurls=False, db=None): + Formatter.__init__(self, env, None, absurls, db) + + # Avoid the possible side-effects of rendering WikiProcessors + + def _macro_formatter(self, match, fullmatch): + return '' + + def handle_code_block(self, line): + if line.strip() == Formatter.STARTBLOCK: + self.in_code_block += 1 + elif line.strip() == Formatter.ENDBLOCK: + self.in_code_block -= 1 + + def format(self, text, out, max_depth=6, min_depth=1): + self.outline = [] + Formatter.format(self, text) + + if min_depth > max_depth: + min_depth, max_depth = max_depth, min_depth + max_depth = min(6, max_depth) + min_depth = max(1, min_depth) + + curr_depth = min_depth - 1 + for depth, anchor, text in self.outline: + if depth < min_depth or depth > max_depth: + continue + if depth < curr_depth: + out.write('</li></ol><li>' * (curr_depth - depth)) + elif depth > curr_depth: + out.write('<ol><li>' * (depth - curr_depth)) + else: + out.write("</li><li>\n") + curr_depth = depth + out.write('<a href="#%s">%s</a>' % (anchor, text)) + out.write('</li></ol>' * curr_depth) + + def _heading_formatter(self, match, fullmatch): + depth, heading, anchor = self._parse_heading(match, fullmatch, True) + heading = re.sub(r'</?a(?: .*?)?>', '', heading) # Strip out link tags + self.outline.append((depth, anchor, heading)) + + +class LinkFormatter(OutlineFormatter): + """Special formatter that focuses on TracLinks.""" + flavor = 'outline' + + def __init__(self, env, absurls=False, db=None): + OutlineFormatter.__init__(self, env, absurls, db) + + def match(self, wikitext): + """Return the Wiki match found at the beginning of the `wikitext`""" + self.reset() + match = re.match(self.wiki.rules, wikitext) + if match: + return self.handle_match(match) + + +# -- wiki_to_* helper functions + +def wiki_to_html(wikitext, env, req, db=None, + absurls=False, escape_newlines=False): + if not wikitext: + return Markup() + out = StringIO() + Formatter(env, req, absurls, db).format(wikitext, out, escape_newlines) + return Markup(out.getvalue()) + +def wiki_to_oneliner(wikitext, env, db=None, shorten=False, absurls=False): + if not wikitext: + return Markup() + out = StringIO() + OneLinerFormatter(env, absurls, db).format(wikitext, out, shorten) + return Markup(out.getvalue()) + +def wiki_to_outline(wikitext, env, db=None, + absurls=False, max_depth=None, min_depth=None): + if not wikitext: + return Markup() + out = StringIO() + OutlineFormatter(env, absurls, db).format(wikitext, out, max_depth, + min_depth) + return Markup(out.getvalue()) + +def wiki_to_link(wikitext, env, req): + if not wikitext: + return '' + return LinkFormatter(env, False, None).match(wikitext) +