trac/wiki/formatter.py
# -*- coding: utf-8 -*-
#
# Copyright (C) 2003-2023 Edgewall Software
# Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
# Copyright (C) 2004-2005 Christopher Lenz <cmlenz@gmx.de>
# Copyright (C) 2005-2007 Christian Boos <cboos@edgewall.org>
# 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 https://trac.edgewall.org/wiki/TracLicense.
#
# This software consists of voluntary contributions made by many
# individuals. For the exact contribution history, see the revision
# history and logs, available at https://trac.edgewall.org/log/.
#
# Author: Jonas Borgström <jonas@edgewall.com>
# Christopher Lenz <cmlenz@gmx.de>
# Christian Boos <cboos@edgewall.org>
import io
import re
from trac.core import *
from trac.mimeview import *
from trac.resource import get_relative_resource, get_resource_url
from trac.util import arity, as_int
from trac.util.text import (
exception_to_unicode, shorten_line, to_unicode, unicode_quote,
unquote_label
)
from trac.util.html import (
Element, Fragment, Markup, TracHTMLSanitizer, escape, plaintext, tag,
to_fragment
)
from trac.util.translation import _, tag_
from trac.wiki.api import WikiSystem, parse_args
from trac.wiki.parser import WikiParser, parse_processor_args
__all__ = ['Formatter', 'MacroError', 'ProcessorError',
'concat_path_query_fragment', 'extract_link', 'format_to',
'format_to_html', 'format_to_oneliner',
'split_url_into_path_query_fragment', 'wiki_to_outline']
def _markup_to_unicode(markup):
if isinstance(markup, Fragment):
return Markup(markup)
else:
return to_unicode(markup)
def system_message(msg, text=None):
return tag.div(tag.strong(msg), text and tag.pre(text),
class_="system-message")
def split_url_into_path_query_fragment(target):
"""Split a target along `?` and `#` in `(path, query, fragment)`.
>>> split_url_into_path_query_fragment('http://path?a=1&b=2#frag?ment')
('http://path', '?a=1&b=2', '#frag?ment')
>>> split_url_into_path_query_fragment('http://path#frag?ment')
('http://path', '', '#frag?ment')
>>> split_url_into_path_query_fragment('http://path?a=1&b=2')
('http://path', '?a=1&b=2', '')
>>> split_url_into_path_query_fragment('http://path')
('http://path', '', '')
"""
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
def concat_path_query_fragment(path, query, fragment=None):
"""Assemble `path`, `query` and `fragment` into a proper URL.
Can be used to re-assemble an URL decomposed using
`split_url_into_path_query_fragment` after modification.
>>> concat_path_query_fragment('/wiki/page', '?version=1')
'/wiki/page?version=1'
>>> concat_path_query_fragment('/wiki/page#a', '?version=1', '#b')
'/wiki/page?version=1#b'
>>> concat_path_query_fragment('/wiki/page?version=1#a', '?format=txt')
'/wiki/page?version=1&format=txt#a'
>>> concat_path_query_fragment('/wiki/page?version=1', '&format=txt')
'/wiki/page?version=1&format=txt'
>>> concat_path_query_fragment('/wiki/page?version=1', 'format=txt')
'/wiki/page?version=1&format=txt'
>>> concat_path_query_fragment('/wiki/page?version=1#a', '?format=txt', '#')
'/wiki/page?version=1&format=txt'
"""
p, q, f = split_url_into_path_query_fragment(path)
if query:
q += ('&' if q else '?') + query.lstrip('?&')
if fragment:
f = fragment
return p + q + ('' if f == '#' else f)
class MacroError(TracError):
"""Exception raised on incorrect macro usage.
The exception is trapped by the wiki formatter and the message is
rendered in a `pre` tag, wrapped in a div with class `system-message`.
:since: 1.0.11
"""
pass
class ProcessorError(TracError):
"""Exception raised on incorrect processor usage.
The exception is trapped by the wiki formatter and the message is
rendered in a `pre` tag, wrapped in a div with class `system-message`.
:since: 0.12
"""
pass
class WikiProcessor(object):
_code_block_re = re.compile(r'^<div(?:\s+class="([^"]+)")?>(.*)</div>$')
_block_elem_re = re.compile(r'^\s*<(?:div|table)(?:\s+[^>]+)?>',
re.I | re.M)
def __init__(self, formatter, name, args=None):
"""Find the processor by name
:param formatter: the formatter embedding a call for this processor
:param name: the name of the processor
:param args: extra parameters for the processor
(''since 0.11'')
"""
self.formatter = formatter
self.env = formatter.env
self.name = name
self.args = args
self.error = None
self.macro_provider = None
# FIXME: move these tables outside of __init__
builtin_processors = {'html': self._html_processor,
'htmlcomment': self._htmlcomment_processor,
'default': self._default_processor,
'comment': self._comment_processor,
'div': self._div_processor,
'rtl': self._rtl_processor,
'span': self._span_processor,
'Span': self._span_processor,
'td': self._td_processor,
'th': self._th_processor,
'tr': self._tr_processor,
'table': self._table_processor,
}
self.inline_check = {'html': self._html_is_inline,
'htmlcomment': True, 'comment': True,
'span': True, 'Span': True,
}.get(name)
self._sanitizer = TracHTMLSanitizer(
safe_schemes=formatter.wiki.safe_schemes,
safe_origins=formatter.wiki.safe_origins)
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() or []:
if self.name == macro_name:
if hasattr(macro_provider, 'expand_macro'):
self.processor = self._macro_processor
else:
raise TracError(
tag_("Pre-0.11 macros with the %(method)s "
"method are no longer supported.",
method=tag.code("render_macro")))
self.macro_provider = macro_provider
self.inline_check = getattr(macro_provider, 'is_inline',
False)
break
if not self.processor:
# Find a matching mimeview renderer
mimeview = Mimeview(formatter.env)
for renderer in mimeview.renderers:
if renderer.get_quality_ratio(self.name) > 1:
self.processor = self._mimeview_processor
break
if not self.processor:
mimetype = mimeview.get_mimetype(self.name)
if mimetype:
self.name = mimetype
self.processor = self._mimeview_processor
if not self.processor:
self.processor = self._default_processor
self.error = _("No macro or processor named '%(name)s' found",
name=name)
# inline checks
def _html_is_inline(self, text):
if text:
tag = text[1:].lstrip()
idx = tag.find(' ')
if idx > -1:
tag = tag[:idx]
return tag.lower() in ('a', 'span', 'bdo', 'img',
'big', 'small', 'font',
'tt', 'i', 'b', 'u', 's', 'strike',
'em', 'strong', 'dfn', 'code', 'q',
'samp', 'kbd', 'var', 'cite', 'abbr',
'acronym', 'sub', 'sup')
# builtin processors
def _comment_processor(self, text):
return ''
def _default_processor(self, text):
if self.args and 'lineno' in self.args:
self.name = \
Mimeview(self.formatter.env).get_mimetype('text/plain')
return self._mimeview_processor(text)
else:
return tag.pre(text, class_="wiki")
def _html_processor(self, text):
if WikiSystem(self.env).render_unsafe_content:
return Markup(text)
return self._sanitizer.sanitize(text)
def _htmlcomment_processor(self, text):
if "--" in text:
return system_message(_('Error: Forbidden character sequence '
'"--" in htmlcomment wiki code block'))
return Markup('<!--\n%s-->\n' % text)
def _elt_processor(self, eltname, format_to, text):
# Note: as long as _processor_param_re is not re.UNICODE, **args is OK.
# Also, parse_args is using strict mode when processing [[span(...)]].
elt = getattr(tag, eltname)(**(self.args or {}))
if not WikiSystem(self.env).render_unsafe_content:
sanitized_elt = getattr(tag, eltname)
sanitized_elt.attrib = self._sanitizer.sanitize_attrs(eltname,
elt.attrib)
elt = sanitized_elt
elt.append(format_to(self.env, self.formatter.context, text))
return elt
def _div_processor(self, text):
if not self.args:
self.args = {}
self.args.setdefault('class', 'wikipage')
return self._elt_processor('div', format_to_html, text)
def _rtl_processor(self, text):
if not self.args:
self.args = {}
self.args['class'] = ('rtl ' + self.args.get('class', '')).rstrip()
return self._elt_processor('div', format_to_html, text)
def _span_processor(self, text):
if self.args is None:
args, self.args = parse_args(text, strict=True)
text = ', '.join(args)
return self._elt_processor('span', format_to_oneliner, text)
def _td_processor(self, text):
return self._tablecell_processor('td', text)
def _th_processor(self, text):
return self._tablecell_processor('th', text)
def _tr_processor(self, text):
try:
elt = self._elt_processor('tr', self._format_row, text)
self.formatter.open_table()
return elt
except ProcessorError as e:
return system_message(e)
def _table_processor(self, text):
if not self.args:
self.args = {}
self.args.setdefault('class', 'wiki')
try:
return self._elt_processor('table', self._format_table, text)
except ProcessorError as e:
return system_message(e)
def _tablecell_processor(self, eltname, text):
self.formatter.open_table_row()
return self._elt_processor(eltname, format_to_html, text)
_has_multiple_tables_re = re.compile(r"</table>.*?<table",
re.MULTILINE | re.DOTALL)
_inner_table_re = re.compile(r"""\s*
<table[^>]*>\s*
((?:<tr[^>]*>)?
(.*?)
(?:</tr>)?)\s*
</table>\s*$
""", re.MULTILINE | re.DOTALL | re.VERBOSE)
# Note: the need for "parsing" that crude way the formatted content
# will go away as soon as we have a WikiDOM to manipulate...
def _parse_inner_table(self, text):
if self._has_multiple_tables_re.search(text):
raise ProcessorError(_("!#%(name)s must contain at most one table",
name=self.name))
match = self._inner_table_re.match(text)
if not match:
raise ProcessorError(_("!#%(name)s must contain at least one table"
" cell (and table cells only)",
name=self.name))
return Markup(match.group(1 if self.name == 'table' else 2))
def _format_row(self, env, context, text):
if text:
out = io.StringIO()
Formatter(env, context).format(text, out)
text = self._parse_inner_table(out.getvalue())
return text
def _format_table(self, env, context, text):
if text:
out = io.StringIO()
Formatter(env, context).format(text, out)
text = self._parse_inner_table(out.getvalue())
return text
# generic processors
def _macro_processor(self, text):
self.env.log.debug('Executing Wiki macro %s by provider %s',
self.name, self.macro_provider)
if arity(self.macro_provider.expand_macro) == 4:
return self.macro_provider.expand_macro(self.formatter, self.name,
text, self.args)
else:
return self.macro_provider.expand_macro(self.formatter, self.name,
text)
def _mimeview_processor(self, text):
annotations = []
context = self.formatter.context.child()
args = self.args.copy() if self.args else self.args
if args and 'lineno' in args:
lineno = as_int(args.pop('lineno'), 1, min=1)
context.set_hints(lineno=lineno)
id = str(args.pop('id', '')) or \
self.formatter._unique_anchor('a')
context.set_hints(id=id + '-')
if 'marks' in args:
context.set_hints(marks=args.pop('marks'))
annotations.append('lineno')
if args: # Remaining args are assumed to be lexer options
context.set_hints(lexer_options=args)
return tag.div(class_='wiki-code')(
Mimeview(self.env).render(context, self.name, text,
annotations=annotations))
# TODO: use convert('text/html') instead of render
def process(self, text, in_paragraph=False):
if self.error:
text = system_message(tag_("Error: Failed to load processor "
"%(name)s", name=tag.code(self.name)),
self.error)
else:
text = self.processor(text)
return text or ''
def is_inline(self, text):
if callable(self.inline_check):
return self.inline_check(text)
else:
return self.inline_check
def ensure_inline(self, text, in_paragraph=True):
content_for_span = None
interrupt_paragraph = False
if isinstance(text, Element):
tagname = text.tag.lower()
if tagname == 'div':
class_ = text.attrib.get('class', '')
if class_ and 'code' in class_:
content_for_span = text.children
else:
interrupt_paragraph = True
elif tagname == 'table':
interrupt_paragraph = True
else:
# FIXME: do something smarter for Streams
text = _markup_to_unicode(text)
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 re.match(self._block_elem_re, text):
interrupt_paragraph = True
if content_for_span:
text = tag.span(class_='code-block')(*content_for_span)
elif interrupt_paragraph and in_paragraph:
text = "</p>%s<p>" % _markup_to_unicode(text)
return text
class Formatter(object):
"""Base Wiki formatter.
Parses and formats wiki text, in a given `RenderingContext`.
"""
flavor = 'default'
def __init__(self, env, context):
self.env = env
self.context = context.child()
self.context.set_hints(disable_warnings=True)
self.req = context.req
self.href = context.href
self.resource = context.resource
self.perm = context.perm
self.wiki = WikiSystem(self.env)
self.wikiparser = WikiParser(self.env)
self._anchors = {}
self._open_tags = []
self._safe_schemes = None
if not self.wiki.render_unsafe_content:
self._safe_schemes = set(self.wiki.safe_schemes)
def split_link(self, target):
return split_url_into_path_query_fragment(target)
# -- Pre- IWikiSyntaxProvider rules (Font styles)
_indirect_tags = {
'MM_BOLD': ('<strong>', '</strong>'),
'WC_BOLD': ('<strong>', '</strong>'),
'MM_ITALIC': ('<em>', '</em>'),
'WC_ITALIC': ('<em>', '</em>'),
'MM_UNDERLINE': ('<span class="underline">', '</span>'),
'MM_STRIKE': ('<del>', '</del>'),
'MM_SUBSCRIPT': ('<sub>', '</sub>'),
'MM_SUPERSCRIPT': ('<sup>', '</sup>'),
}
def _get_open_tag(self, tag):
"""Retrieve opening tag for direct or indirect `tag`."""
if not isinstance(tag, tuple):
tag = self._indirect_tags[tag]
return tag[0]
def _get_close_tag(self, tag):
"""Retrieve closing tag for direct or indirect `tag`."""
if not isinstance(tag, tuple):
tag = self._indirect_tags[tag]
return tag[1]
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 pop_tags(self):
while self._open_tags:
yield self._open_tags.pop()
def flush_tags(self):
for tag in self.pop_tags():
self.out.write(self._get_close_tag(tag))
def open_tag(self, tag_open, tag_close=None):
"""Open an inline style tag.
If `tag_close` is not specified, `tag_open` is an indirect tag (0.12)
"""
if tag_close:
self._open_tags.append((tag_open, tag_close))
else:
self._open_tags.append(tag_open)
tag_open = self._get_open_tag(tag_open)
return tag_open
def close_tag(self, open_tag, close_tag=None):
"""Open a inline style tag.
If `close_tag` is not specified, it's an indirect tag (0.12)
"""
tmp = ''
for i in range(len(self._open_tags) - 1, -1, -1):
tag = self._open_tags[i]
tmp += self._get_close_tag(tag)
if (open_tag == tag,
(open_tag, close_tag) == tag)[bool(close_tag)]:
del self._open_tags[i]
for j in range(i, len(self._open_tags)):
tmp += self._get_open_tag(self._open_tags[j])
break
return tmp
def _indirect_tag_handler(self, match, tag):
"""Handle binary inline style tags (indirect way, 0.12)"""
if self._list_stack and not self.in_list_item:
self.close_list()
if self.tag_open_p(tag):
return self.close_tag(tag)
else:
return self.open_tag(tag)
def _bolditalic_formatter(self, match, fullmatch):
if self._list_stack and not self.in_list_item:
self.close_list()
bold_open = self.tag_open_p('MM_BOLD')
italic_open = self.tag_open_p('MM_ITALIC')
if bold_open and italic_open:
bold_idx = self._open_tags.index('MM_BOLD')
italic_idx = self._open_tags.index('MM_ITALIC')
if italic_idx < bold_idx:
close_tags = ('MM_BOLD', 'MM_ITALIC')
else:
close_tags = ('MM_ITALIC', 'MM_BOLD')
open_tags = ()
elif bold_open:
close_tags = ('MM_BOLD',)
open_tags = ('MM_ITALIC',)
elif italic_open:
close_tags = ('MM_ITALIC',)
open_tags = ('MM_BOLD',)
else:
close_tags = ()
open_tags = ('MM_BOLD', 'MM_ITALIC')
tmp = []
tmp.extend(self.close_tag(tag) for tag in close_tags)
tmp.extend(self.open_tag(tag) for tag in open_tags)
return ''.join(tmp)
def _bold_formatter(self, match, fullmatch):
return self._indirect_tag_handler(match, 'MM_BOLD')
def _bold_wc_formatter(self, match, fullmatch):
return self._indirect_tag_handler(match, 'WC_BOLD')
def _italic_formatter(self, match, fullmatch):
return self._indirect_tag_handler(match, 'MM_ITALIC')
def _italic_wc_formatter(self, match, fullmatch):
return self._indirect_tag_handler(match, 'WC_ITALIC')
def _underline_formatter(self, match, fullmatch):
return self._indirect_tag_handler(match, 'MM_UNDERLINE')
def _strike_formatter(self, match, fullmatch):
return self._indirect_tag_handler(match, 'MM_STRIKE')
def _subscript_formatter(self, match, fullmatch):
return self._indirect_tag_handler(match, 'MM_SUBSCRIPT')
def _superscript_formatter(self, match, fullmatch):
return self._indirect_tag_handler(match, 'MM_SUPERSCRIPT')
def _inlinecode_formatter(self, match, fullmatch):
return tag.code(fullmatch.group('inline'))
def _inlinecode2_formatter(self, match, fullmatch):
return tag.code(fullmatch.group('inline2'))
# pre-0.12 public API (no longer used by Trac itself but kept for plugins)
def simple_tag_handler(self, match, open_tag, close_tag):
"""Generic handler for simple binary style tags"""
if self.tag_open_p((open_tag, close_tag)):
return self.close_tag(open_tag, close_tag)
else:
self.open_tag(open_tag, close_tag)
return open_tag
# -- Post- IWikiSyntaxProvider rules
# WikiCreole line breaks
def _linebreak_wc_formatter(self, match, fullmatch):
return '<br />'
# E-mails
def _email_formatter(self, match, fullmatch):
from trac.web.chrome import Chrome
omatch = Chrome(self.env).format_emails(self.context, match)
if omatch == match: # not obfuscated, make a link
return self._make_mail_link('mailto:'+match, match)
else:
return omatch
# HTML escape of &, < and >
def _htmlescape_formatter(self, match, fullmatch):
return "&" if match == "&" \
else "<" if match == "<" else ">"
# Short form (shref) and long form (lhref) of TracLinks
def _shrefbr_formatter(self, match, fullmatch):
ns = fullmatch.group('snsbr')
target = unquote_label(fullmatch.group('stgtbr'))
match = match[1:-1]
return '<%s>' % \
self._make_link(ns, target, match, match, fullmatch)
def _shref_formatter(self, match, fullmatch):
ns = fullmatch.group('sns')
target = unquote_label(fullmatch.group('stgt'))
return self._make_link(ns, target, match, match, fullmatch)
def _lhref_formatter(self, match, fullmatch):
rel = fullmatch.group('rel')
ns = fullmatch.group('lns')
target = unquote_label(fullmatch.group('ltgt'))
label = fullmatch.group('label')
return self._make_lhref_link(match, fullmatch, rel, ns, target, label)
def _make_lhref_link(self, match, fullmatch, rel, ns, target, label):
if not label: # e.g. `[http://target]` or `[wiki:target]`
if target:
if ns and target.startswith('//'): # for `[http://target]`
label = ns + ':' + target # use `http://target`
else: # for `wiki:target`
label = target.lstrip('/') # use only `target`
else: # e.g. `[search:]`
label = ns
else:
label = unquote_label(label)
if rel:
if not label:
label = self.wiki.make_label_from_target(rel)
path, query, fragment = self.split_link(rel)
if path.startswith('//'):
path = '/' + path.lstrip('/')
elif path.startswith('/'):
path = self.href + path
else:
resource = get_relative_resource(self.resource, path)
path = get_resource_url(self.env, resource, self.href)
if resource.id:
target = concat_path_query_fragment(str(resource.id),
query, fragment)
if resource.realm == 'wiki':
target = '/' + target # Avoid wiki page scoping
return self._make_link(resource.realm, target, match,
label, fullmatch)
return tag.a(label,
href=concat_path_query_fragment(path, query, fragment))
else:
return self._make_link(ns or 'wiki', target or '', match, label,
fullmatch)
def _make_link(self, ns, target, match, label, fullmatch):
# first check for an alias defined in trac.ini
ns = self.env.config['intertrac'].get(ns, ns)
if ns in self.wikiparser.link_resolvers:
resolver = self.wikiparser.link_resolvers[ns]
if arity(resolver) == 5:
return resolver(self, ns, target, escape(label, False),
fullmatch)
else:
return resolver(self, ns, target, escape(label, False))
elif ns == "mailto":
from trac.web.chrome import Chrome
chrome = Chrome(self.env)
if chrome.never_obfuscate_mailto:
otarget, olabel = target, label
else:
otarget = chrome.format_emails(self.context, target)
olabel = chrome.format_emails(self.context, label)
if (otarget, olabel) == (target, label):
return self._make_mail_link('mailto:'+target, label)
else:
return olabel or otarget
elif target.startswith('//'):
if self._safe_schemes is None or ns in self._safe_schemes:
return self._make_ext_link(ns + ':' + target, label)
else:
return escape(match)
else:
return self._make_intertrac_link(ns, target, label) or \
self._make_interwiki_link(ns, target, label) or \
escape(match)
def _make_intertrac_link(self, ns, target, label):
res = self.get_intertrac_url(ns, target)
if res:
return self._make_ext_link(res[0], label, res[1])
def get_intertrac_url(self, ns, target):
intertrac = self.env.config['intertrac']
url = intertrac.get(ns + '.url')
name = _("Trac project %(name)s", name=ns)
if not url and ns.lower() == 'trac':
url = 'https://trac.edgewall.org'
name = _("The Trac Project")
if url:
name = intertrac.get(ns + '.title', name)
url = '%s/intertrac/%s' % (url, unicode_quote(target))
if target:
title = _("%(target)s in %(name)s", target=target, name=name)
else:
title = name
return url, title
def shorthand_intertrac_helper(self, ns, target, label, fullmatch):
if fullmatch: # short form
it_group = fullmatch.groupdict().get('it_' + ns)
if it_group:
alias = it_group.strip()
intertrac = self.env.config['intertrac']
target = '%s:%s' % (ns, target[len(it_group):])
return self._make_intertrac_link(intertrac.get(alias, alias),
target, label) or label
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)
if url:
return self._make_ext_link(url, label, title)
def _make_ext_link(self, url, text, title=''):
local_url = self.env.project_url or self.env.abs_href.base
if not url.startswith(local_url):
return tag.a(tag.span('\u200b', class_="icon"), text,
class_="ext-link", href=url, title=title or None)
else:
return tag.a(text, href=url, title=title or None)
def _make_mail_link(self, url, text, title=''):
return tag.a(tag.span('\u200b', class_="icon"), text,
class_="mail-link", href=url, title=title or None)
# Anchors
def _anchor_formatter(self, match, fullmatch):
anchor = fullmatch.group('anchorname')
label = fullmatch.group('anchorlabel') or ''
if label:
label = format_to_oneliner(self.env, self.context, label)
return '<span class="wikianchor" id="%s">%s</span>' % (anchor, label)
def _unique_anchor(self, anchor):
i = 1
anchor_base = anchor
while anchor in self._anchors:
anchor = anchor_base + str(i)
i += 1
self._anchors[anchor] = True
return anchor
# WikiMacros or WikiCreole links
def _macrolink_formatter(self, match, fullmatch):
# check for a known [[macro]]
macro_or_link = match[2:-2]
if macro_or_link.startswith('=#'):
fullmatch = WikiParser._set_anchor_wc_re.match(macro_or_link)
if fullmatch:
return self._anchor_formatter(macro_or_link, fullmatch)
fullmatch = WikiParser._macro_re.match(macro_or_link)
if fullmatch:
name = fullmatch.group('macroname')
args = fullmatch.group('macroargs')
macro = False # not a macro
macrolist = name[-1] == '?'
if name.lower() == 'br' or name == '?':
macro = None
else:
macro = WikiProcessor(self, (name, name[:-1])[macrolist])
if macro.error:
macro = False
if macro is not False:
if macrolist:
macro = WikiProcessor(self, 'MacroList')
return self._macro_formatter(match, fullmatch, macro)
fullmatch = WikiParser._creolelink_re.match(macro_or_link)
return self._lhref_formatter(match, fullmatch)
def _macro_formatter(self, match, fullmatch, macro, only_inline=False):
name = fullmatch.group('macroname')
if name and name[-1] == '?': # Macro?() shortcut for MacroList(Macro)
args = name[:-1] or '*'
else:
args = fullmatch.group('macroargs')
if name.lower() == 'br':
return self.emit_linebreak(args)
in_paragraph = not (getattr(self, 'in_list_item', True) or
getattr(self, 'in_table', True) or
getattr(self, 'in_def_list', True))
try:
return macro.ensure_inline(macro.process(args), in_paragraph)
except MacroError as e:
return system_message(_("Macro %(name)s(%(args)s) failed",
name=name, args=args), to_fragment(e))
except Exception as e:
self.env.log.error("Macro %s(%s) failed for %s:%s", name,
args, self.resource,
exception_to_unicode(e, traceback=True))
return system_message(_("Error: Macro %(name)s(%(args)s) failed",
name=name, args=args), to_fragment(e))
def emit_linebreak(self, args):
if args:
sep = ':' if ':' in args else '='
kv = args.split(sep, 1)
if kv[0] == 'clear':
clear = kv[-1] if kv[-1] in ['left', 'right'] else 'both'
return '<br style="clear: {0}" />'.format(clear)
return '<br />'
# Headings
def _parse_heading(self, match, fullmatch, shorten):
match = match.strip()
hdepth = fullmatch.group('hdepth')
depth = len(hdepth)
anchor = fullmatch.group('hanchor') or ''
htext = fullmatch.group('htext').strip()
if htext.endswith(hdepth):
htext = htext[:-depth]
heading = format_to_oneliner(self.env, self.context, htext, False)
if anchor:
anchor = anchor[1:]
else:
sans_markup = plaintext(heading, keeplinebreaks=False)
anchor = WikiParser._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
anchor = self._unique_anchor(anchor)
if shorten:
heading = format_to_oneliner(self.env, self.context, htext, 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 class="section" 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 WikiParser.BULLET_CHARS:
type_ = 'ul'
else:
type_ = 'ol'
lstart = fullmatch.group('lstart')
if listid == 'i':
class_ = 'lowerroman'
elif listid == 'I':
class_ = 'upperroman'
elif listid.isdigit() and lstart != '1':
start = int(lstart)
elif listid.islower():
class_ = 'loweralpha'
if len(lstart) == 1 and lstart != 'a':
start = ord(lstart) - ord('a') + 1
elif listid.isupper():
class_ = 'upperalpha'
if len(lstart) == 1 and lstart != 'A':
start = ord(lstart) - ord('A') + 1
self._set_list_depth(ldepth, type_, class_, start)
return ''
def _get_list_depth(self):
"""Return the space offset associated to the deepest opened list."""
if self._list_stack:
return self._list_stack[-1][1]
return -1
def _set_list_depth(self, depth, new_type=None, lclass=None, start=None):
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 = ' class="%s"' % lclass if lclass else ''
start_attr = ' start="%s"' % start if start is not None else ''
self.out.write('<' + new_type + class_attr + start_attr + '><li>')
def close_item():
self.flush_tags()
self.out.write('</li>')
def close_list(tp):
self._list_stack.pop()
close_item()
self.out.write('</%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 new_type and 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)
close_item()
self.out.write('<li>')
else:
open_list()
def close_list(self, depth=-1):
self._set_list_depth(depth)
# Definition Lists
def _definition_formatter(self, match, fullmatch):
if self.in_def_list:
tmp = '</dd>'
else:
self.close_paragraph()
tmp = '<dl class="wiki">'
definition = match[:match.find('::')]
tmp += '<dt>%s</dt><dd>' % format_to_oneliner(self.env, self.context,
definition)
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)
return ''
elif idepth <= ldepth + (3 if ltype == 'ol' else 2):
self.in_list_item = True
return ''
if not self.in_def_list:
self._set_quote_depth(idepth)
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[-1] if self._quote_stack else 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 = ' class="citation"' if citation else ''
self.out.write('<blockquote%s>\n' % class_attr)
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>\n')
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 _table_cell_formatter(self, match, fullmatch):
self.open_table()
self.open_table_row()
self.continue_table = 1
separator = fullmatch.group('table_cell_sep')
is_last = fullmatch.group('table_cell_last')
numpipes = len(separator)
cell = 'td'
if separator[0] == '=':
numpipes -= 1
if separator[-1] == '=':
numpipes -= 1
cell = 'th'
colspan = numpipes // 2
if is_last is not None:
if is_last and is_last[-1] == '\\':
self.continue_table_row = 1
colspan -= 1
if not colspan:
return ''
attrs = ''
if colspan > 1:
attrs = ' colspan="%d"' % int(colspan)
# alignment: ||left || right||default|| default || center ||
after_sep = fullmatch.end('table_cell_sep')
alignleft = after_sep < len(self.line) and self.line[after_sep] != ' '
# lookahead next || (FIXME: this fails on ` || ` inside the cell)
next_sep = re.search(r'([^!])=?\|\|', self.line[after_sep:])
alignright = next_sep and next_sep.group(1) != ' '
textalign = None
if alignleft:
if not alignright:
textalign = 'left'
elif alignright:
textalign = 'right'
elif next_sep: # check for the extra spaces specifying a center align
first_extra = after_sep + 1
last_extra = after_sep + next_sep.start() - 1
if first_extra < last_extra and \
self.line[first_extra] == self.line[last_extra] == ' ':
textalign = 'center'
if textalign:
attrs += ' style="text-align: %s"' % textalign
td = '<%s%s>' % (cell, attrs)
if self.in_table_cell:
close_tags = ''.join(self._get_close_tag(tag)
for tag in self.pop_tags())
td = '%s</%s>%s' % (close_tags, self.in_table_cell, td)
self.in_table_cell = cell
return td
def _table_row_sep_formatter(self, match, fullmatch):
self.open_table()
self.close_table_row(force=True)
params = fullmatch.group('table_row_params')
if params:
tr = WikiProcessor(self, 'tr', self.parse_processor_args(params))
processed = _markup_to_unicode(tr.process(''))
params = processed[3:processed.find('>')]
self.open_table_row(params or '')
self.continue_table = 1
self.continue_table_row = 1
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">\n')
def open_table_row(self, params=''):
if not self.in_table_row:
self.open_table()
self.in_table_row = 1
self.out.write('<tr%s>' % params)
def close_table_row(self, force=False):
if self.in_table_row and (not self.continue_table_row or force):
self.in_table_row = 0
if self.in_table_cell:
self.flush_tags()
self.out.write('</%s>' % self.in_table_cell)
self.in_table_cell = ''
self.out.write('</tr>')
self.continue_table_row = 0
def close_table(self):
if self.in_table:
self.close_table_row(force=True)
self.out.write('</table>\n')
self.in_table = 0
# Paragraphs
def open_paragraph(self):
if not self.paragraph_open:
self.out.write('<p>\n')
self.paragraph_open = 1
def close_paragraph(self):
self.flush_tags()
if self.paragraph_open:
self.out.write('</p>\n')
self.paragraph_open = 0
# Code blocks
def parse_processor_args(self, line):
return parse_processor_args(line)
def handle_code_block(self, line, startmatch=None):
if startmatch:
self.in_code_block += 1
if self.in_code_block == 1:
name = startmatch.group(2)
if name:
args = parse_processor_args(line[startmatch.end():])
self.code_processor = WikiProcessor(self, name, args)
else:
self.code_processor = None
self.code_buf = []
self.code_prefix = line[:line.find(WikiParser.STARTBLOCK)]
else:
self.code_buf.append(line)
if not self.code_processor:
self.code_processor = WikiProcessor(self, 'default')
elif line.strip() == WikiParser.ENDBLOCK:
self.in_code_block -= 1
if self.in_code_block == 0 and self.code_processor:
if self.code_processor.name not in ('th', 'td', 'tr'):
self.close_table()
self.close_paragraph()
if self.code_buf:
if self.code_prefix and all(not l or
l.startswith(self.code_prefix)
for l in self.code_buf):
code_indent = len(self.code_prefix)
self.code_buf = [l[code_indent:]
for l in self.code_buf]
self.code_buf.append('')
code_text = '\n'.join(self.code_buf)
processed = self._exec_processor(self.code_processor,
code_text)
self.out.write(_markup_to_unicode(processed))
else:
self.code_buf.append(line)
elif not self.code_processor:
match = WikiParser._processor_re.match(line)
if match:
self.code_prefix = match.group(1)
name = match.group(2)
args = parse_processor_args(line[match.end():])
self.code_processor = WikiProcessor(self, name, args)
else:
self.code_buf.append(line)
self.code_processor = WikiProcessor(self, 'default')
else:
self.code_buf.append(line)
def close_code_blocks(self):
while self.in_code_block > 0:
self.handle_code_block(WikiParser.ENDBLOCK)
def _exec_processor(self, processor, text):
try:
return processor.process(text)
except ProcessorError as e:
return system_message(_("Processor %(name)s failed",
name=processor.name), to_fragment(e))
except Exception as e:
self.env.log.error("Processor %s failed for %s:%s",
processor.name, self.resource,
exception_to_unicode(e, traceback=True))
return system_message(_("Error: Processor %(name)s failed",
name=processor.name), to_fragment(e))
# > quotes
def handle_quote_block(self, line):
self.close_paragraph()
depth = line.find('>')
# Close lists up to current level:
#
# - first level item
# - second level item
# > citation part of first level item
#
# (depth == 3, _list_stack == [1, 3])
if not self._quote_buffer and depth < self._get_list_depth():
self.close_list(depth)
self._quote_buffer.append(line[depth + 1:])
def close_quote_block(self, escape_newlines):
if self._quote_buffer:
# avoid an extra <blockquote> when there's consistently one space
# after the '>'
if all(not line or line[0] in '> ' for line in self._quote_buffer):
self._quote_buffer = [line[bool(line and line[0] == ' '):]
for line in self._quote_buffer]
self.out.write('<blockquote class="citation">\n')
Formatter(self.env, self.context).format(self._quote_buffer,
self.out, escape_newlines)
self.out.write('</blockquote>\n')
self._quote_buffer = []
# -- Wiki engine
def handle_match(self, fullmatch):
for itype, match in fullmatch.groupdict().items():
if match and itype not in self.wikiparser.helper_patterns:
# Check for preceding escape character '!'
if match[0] == '!':
return escape(match[1:])
if itype in self.wikiparser.external_handlers:
external_handler = self.wikiparser.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 _markup_to_unicode(replacement)
_normalize_re = re.compile(r'[\v\f]', re.UNICODE)
def reset(self, source, out=None):
if isinstance(source, str):
source = re.sub(self._normalize_re, ' ', source)
self.source = source
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._quote_buffer = []
self.in_code_block = 0
self.in_table = 0
self.in_def_list = 0
self.in_table_row = 0
self.continue_table = 0
self.continue_table_row = 0
self.in_table_cell = ''
self.paragraph_open = 0
return source
def format(self, text, out=None, escape_newlines=False):
text = self.reset(text, out)
if isinstance(text, str):
text = text.splitlines()
for line in text:
# Detect start of code block (new block or embedded block)
block_start_match = None
if WikiParser.ENDBLOCK not in line:
block_start_match = WikiParser._startblock_re.match(line)
# Handle content or end of code block
if self.in_code_block:
self.handle_code_block(line, block_start_match)
continue
# Handle citation quotes '> ...'
if line.strip().startswith('>'):
self.handle_quote_block(line)
continue
# Handle end of citation quotes
self.close_quote_block(escape_newlines)
# Handle start of a new block
if block_start_match:
self.handle_code_block(line, block_start_match)
continue
# Handle Horizontal ruler
if line[0:4] == '----':
self.close_table()
self.close_paragraph()
self.close_indentation()
self.close_list()
self.close_def_list()
self.out.write('<hr />\n')
continue
# Handle new paragraph
if line == '':
self.close_table()
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 = []
# Handle end of indentation
if not line.startswith(' ') and self._quote_stack:
self.close_indentation()
self.in_list_item = False
self.in_quote = False
# Throw a bunch of regexps on the problem
self.line = line
result = re.sub(self.wikiparser.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 not self.continue_table:
self.close_table()
self.continue_table = 0
sep = '\n'
if not(self.in_list_item or self.in_def_list or self.in_table):
if len(result):
self.open_paragraph()
if escape_newlines and self.paragraph_open and \
not result.rstrip().endswith('<br />'):
sep = '<br />' + sep
self.out.write(result + sep)
self.close_table_row()
self.close_code_blocks()
self.close_quote_block(escape_newlines)
self.close_table()
self.close_paragraph()
self.close_indentation()
self.close_list()
self.close_def_list()
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'
# 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 _table_row_sep_formatter(self, match, fullmatch):
return ''
def _linebreak_wc_formatter(self, match, fullmatch):
return ' '
def _macro_formatter(self, match, fullmatch, macro):
name = fullmatch.group('macroname')
if name.lower() == 'br':
return ' '
args = fullmatch.group('macroargs')
if macro.is_inline(args):
return Formatter._macro_formatter(self, match, fullmatch, macro)
else:
return '[[%s%s]]' % (name, '(...)' if args else '')
def format(self, text, out, shorten=False):
if not text:
return
text = self.reset(text, out)
# Simplify code blocks
in_code_block = 0
processor = None
buf = io.StringIO()
for line in text.strip().splitlines():
if WikiParser.ENDBLOCK not in line and \
WikiParser._startblock_re.match(line):
in_code_block += 1
elif line.strip() == WikiParser.ENDBLOCK:
if in_code_block:
in_code_block -= 1
if in_code_block == 0:
if processor != 'comment':
buf.write(' [...]\n')
processor = None
elif in_code_block:
if not processor:
if line.startswith('#!'):
processor = line[2:].strip()
else:
buf.write(line + '\n')
result = buf.getvalue()[:-len('\n')]
if shorten:
result = shorten_line(result)
result = re.sub(self.wikiparser.rules, self.replace, result)
result = result.replace('[...]', '[\u2026]')
if result.endswith('...'):
result = result[:-3] + '\u2026'
self.out.write(result)
# Close all open 'one line'-tags
self.flush_tags()
# Flush unterminated code blocks
if in_code_block > 0:
self.out.write('[\u2026]')
class OutlineFormatter(Formatter):
"""Special formatter that generates an outline of all the headings."""
flavor = 'outline'
# Avoid the possible side-effects of rendering WikiProcessors
def _macro_formatter(self, match, fullmatch, macro):
name = fullmatch.group('macroname')
if name.lower() == 'br':
return ' '
args = fullmatch.group('macroargs')
if macro.is_inline(args):
return Formatter._macro_formatter(self, match, fullmatch, macro)
return ''
def handle_code_block(self, line, startmatch=None):
if WikiParser.ENDBLOCK not in line and \
WikiParser._startblock_re.match(line):
self.in_code_block += 1
elif line.strip() == WikiParser.ENDBLOCK:
self.in_code_block -= 1
def format(self, text, out, max_depth=6, min_depth=1, shorten=True):
self.shorten = shorten
whitespace_indent = ' '
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
out.write('\n')
for depth, anchor, text in self.outline:
if depth < min_depth or depth > max_depth:
continue
if depth > curr_depth: # Deeper indent
for i in range(curr_depth, depth):
out.write(whitespace_indent * (2*i) + '<ol>\n' +
whitespace_indent * (2*i+1) + '<li>\n')
elif depth < curr_depth: # Shallower indent
for i in range(curr_depth-1, depth-1, -1):
out.write(whitespace_indent * (2*i+1) + '</li>\n' +
whitespace_indent * (2*i) + '</ol>\n')
out.write(whitespace_indent * (2*depth-1) + '</li>\n' +
whitespace_indent * (2*depth-1) + '<li>\n')
else: # Same indent
out.write( whitespace_indent * (2*depth-1) + '</li>\n' +
whitespace_indent * (2*depth-1) + '<li>\n')
curr_depth = depth
out.write(whitespace_indent * (2*depth) +
'<a href="#%s">%s</a>\n' % (anchor, text))
# Close out all indentation
for i in range(curr_depth-1, min_depth-2, -1):
out.write(whitespace_indent * (2*i+1) + '</li>\n' +
whitespace_indent * (2*i) + '</ol>\n')
def _heading_formatter(self, match, fullmatch):
depth, heading, anchor = self._parse_heading(match, fullmatch,
self.shorten)
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 = 'link'
def _heading_formatter(self, match, fullmatch):
return ''
def match(self, wikitext):
"""Return the Wiki match found at the beginning of the `wikitext`"""
wikitext = self.reset(wikitext)
self.line = wikitext
match = re.match(self.wikiparser.rules, wikitext)
if match:
return self.handle_match(match)
# Pure Wiki Formatter
class HtmlFormatter(object):
"""Format parsed wiki text to HTML"""
flavor = 'default'
def __init__(self, env, context, wikidom):
self.env = env
self.context = context
if isinstance(wikidom, str):
wikidom = WikiParser(env).parse(wikidom)
self.wikidom = wikidom
def generate(self, escape_newlines=False):
"""Generate HTML elements.
newlines in the wikidom will be preserved if `escape_newlines` is set.
"""
# FIXME: compatibility code only for now
out = io.StringIO()
Formatter(self.env, self.context).format(self.wikidom, out,
escape_newlines)
return Markup(out.getvalue())
class InlineHtmlFormatter(object):
"""Format parsed wiki text to inline elements HTML.
Block level content will be discarded or compacted.
"""
flavor = 'oneliner'
def __init__(self, env, context, wikidom):
self.env = env
self.context = context
if isinstance(wikidom, str):
wikidom = WikiParser(env).parse(wikidom)
self.wikidom = wikidom
def generate(self, shorten=False):
"""Generate HTML inline elements.
If `shorten` is set, the generation will stop once enough characters
have been emitted.
"""
# FIXME: compatibility code only for now
out = io.StringIO()
OneLinerFormatter(self.env, self.context).format(self.wikidom, out,
shorten)
return Markup(out.getvalue())
def format_to(env, flavor, context, wikidom, **options):
if flavor is None:
flavor = context.get_hint('wiki_flavor', 'html')
if flavor == 'oneliner':
return format_to_oneliner(env, context, wikidom, **options)
else:
return format_to_html(env, context, wikidom, **options)
def format_to_html(env, context, wikidom, escape_newlines=None):
if not wikidom:
return Markup()
if escape_newlines is None:
escape_newlines = context.get_hint('preserve_newlines', False)
return HtmlFormatter(env, context, wikidom).generate(escape_newlines)
def format_to_oneliner(env, context, wikidom, shorten=None):
if not wikidom:
return Markup()
if shorten is None:
shorten = context.get_hint('shorten_lines', False)
return InlineHtmlFormatter(env, context, wikidom).generate(shorten)
def extract_link(env, context, wikidom):
if not wikidom:
return Markup()
return LinkFormatter(env, context).match(wikidom)
# pre-0.11 wiki text to Markup compatibility methods
def wiki_to_outline(wikitext, env, db=None,
absurls=False, max_depth=None, min_depth=None, req=None):
""":deprecated: will be removed in 1.0 and replaced by something else"""
if not wikitext:
return Markup()
abs_ref, href = (req or env).abs_href, (req or env).href
from trac.web.chrome import web_context
context = web_context(req, absurls=absurls)
out = io.StringIO()
OutlineFormatter(env, context).format(wikitext, out, max_depth, min_depth)
return Markup(out.getvalue())