markup_renderer.py
195 lines
| 6.6 KiB
| text/x-python
|
PythonLexer
r1604 | # -*- coding: utf-8 -*- | |||
""" | ||||
rhodecode.lib.markup_renderer | ||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||
r1818 | ||||
r1604 | Renderer for markup languages with ability to parse using rst or markdown | |||
r1818 | ||||
r1604 | :created_on: Oct 27, 2011 | |||
:author: marcink | ||||
r1824 | :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com> | |||
r1604 | :license: GPLv3, see COPYING for more details. | |||
""" | ||||
# This program is free software: you can redistribute it and/or modify | ||||
# it under the terms of the GNU General Public License as published by | ||||
# the Free Software Foundation, either version 3 of the License, or | ||||
# (at your option) any later version. | ||||
# | ||||
# This program is distributed in the hope that it will be useful, | ||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
# GNU General Public License for more details. | ||||
# | ||||
# You should have received a copy of the GNU General Public License | ||||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
import re | ||||
import logging | ||||
r2747 | import traceback | |||
r1604 | ||||
r2201 | from rhodecode.lib.utils2 import safe_unicode, MENTIONS_REGEX | |||
r1604 | ||||
log = logging.getLogger(__name__) | ||||
r1996 | ||||
r1604 | class MarkupRenderer(object): | |||
RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES = ['include', 'meta', 'raw'] | ||||
r1818 | ||||
r1996 | MARKDOWN_PAT = re.compile(r'md|mkdn?|mdown|markdown', re.IGNORECASE) | |||
RST_PAT = re.compile(r're?st', re.IGNORECASE) | ||||
PLAIN_PAT = re.compile(r'readme', re.IGNORECASE) | ||||
r1818 | ||||
r4009 | def _detect_renderer(self, source, filename=None): | |||
r1604 | """ | |||
runs detection of what renderer should be used for generating html | ||||
from a markup language | ||||
r1818 | ||||
r1604 | filename can be also explicitly a renderer name | |||
r1818 | ||||
r1604 | :param source: | |||
:param filename: | ||||
""" | ||||
if MarkupRenderer.MARKDOWN_PAT.findall(filename): | ||||
detected_renderer = 'markdown' | ||||
elif MarkupRenderer.RST_PAT.findall(filename): | ||||
detected_renderer = 'rst' | ||||
elif MarkupRenderer.PLAIN_PAT.findall(filename): | ||||
detected_renderer = 'rst' | ||||
else: | ||||
detected_renderer = 'plain' | ||||
return getattr(MarkupRenderer, detected_renderer) | ||||
r4009 | @classmethod | |||
def _flavored_markdown(cls, text): | ||||
""" | ||||
Github style flavored markdown | ||||
:param text: | ||||
""" | ||||
from hashlib import md5 | ||||
# Extract pre blocks. | ||||
extractions = {} | ||||
def pre_extraction_callback(matchobj): | ||||
digest = md5(matchobj.group(0)).hexdigest() | ||||
extractions[digest] = matchobj.group(0) | ||||
return "{gfm-extraction-%s}" % digest | ||||
pattern = re.compile(r'<pre>.*?</pre>', re.MULTILINE | re.DOTALL) | ||||
text = re.sub(pattern, pre_extraction_callback, text) | ||||
# Prevent foo_bar_baz from ending up with an italic word in the middle. | ||||
def italic_callback(matchobj): | ||||
s = matchobj.group(0) | ||||
if list(s).count('_') >= 2: | ||||
return s.replace('_', '\_') | ||||
return s | ||||
text = re.sub(r'^(?! {4}|\t)\w+_\w+_\w[\w_]*', italic_callback, text) | ||||
# In very clear cases, let newlines become <br /> tags. | ||||
def newline_callback(matchobj): | ||||
if len(matchobj.group(1)) == 1: | ||||
return matchobj.group(0).rstrip() + ' \n' | ||||
else: | ||||
return matchobj.group(0) | ||||
pattern = re.compile(r'^[\w\<][^\n]*(\n+)', re.MULTILINE) | ||||
text = re.sub(pattern, newline_callback, text) | ||||
# Insert pre block extractions. | ||||
def pre_insert_callback(matchobj): | ||||
return '\n\n' + extractions[matchobj.group(1)] | ||||
text = re.sub(r'{gfm-extraction-([0-9a-f]{32})\}', | ||||
pre_insert_callback, text) | ||||
return text | ||||
r1604 | def render(self, source, filename=None): | |||
""" | ||||
Renders a given filename using detected renderer | ||||
it detects renderers based on file extension or mimetype. | ||||
At last it will just do a simple html replacing new lines with <br/> | ||||
r1818 | ||||
r1604 | :param file_name: | |||
:param source: | ||||
""" | ||||
r4009 | renderer = self._detect_renderer(source, filename) | |||
r1604 | readme_data = renderer(source) | |||
return readme_data | ||||
@classmethod | ||||
def plain(cls, source): | ||||
source = safe_unicode(source) | ||||
r1996 | ||||
r1604 | def urlify_text(text): | |||
url_pat = re.compile(r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]' | ||||
'|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)') | ||||
def url_func(match_obj): | ||||
url_full = match_obj.groups()[0] | ||||
r1996 | return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full}) | |||
r1604 | ||||
return url_pat.sub(url_func, text) | ||||
source = urlify_text(source) | ||||
return '<br />' + source.replace("\n", '<br />') | ||||
@classmethod | ||||
r4009 | def markdown(cls, source, safe=True, flavored=False): | |||
r1604 | source = safe_unicode(source) | |||
try: | ||||
import markdown as __markdown | ||||
r4009 | if flavored: | |||
source = cls._flavored_markdown(source) | ||||
xpol
|
r3164 | return __markdown.markdown(source, ['codehilite', 'extra']) | ||
r1604 | except ImportError: | |||
log.warning('Install markdown to use this function') | ||||
return cls.plain(source) | ||||
r2747 | except Exception: | |||
log.error(traceback.format_exc()) | ||||
if safe: | ||||
return source | ||||
else: | ||||
raise | ||||
r1604 | ||||
@classmethod | ||||
r2747 | def rst(cls, source, safe=True): | |||
r1604 | source = safe_unicode(source) | |||
try: | ||||
from docutils.core import publish_parts | ||||
from docutils.parsers.rst import directives | ||||
docutils_settings = dict([(alias, None) for alias in | ||||
cls.RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES]) | ||||
docutils_settings.update({'input_encoding': 'unicode', | ||||
r1996 | 'report_level': 4}) | |||
r1604 | ||||
for k, v in docutils_settings.iteritems(): | ||||
directives.register_directive(k, v) | ||||
parts = publish_parts(source=source, | ||||
writer_name="html4css1", | ||||
settings_overrides=docutils_settings) | ||||
return parts['html_title'] + parts["fragment"] | ||||
except ImportError: | ||||
log.warning('Install docutils to use this function') | ||||
return cls.plain(source) | ||||
r2747 | except Exception: | |||
log.error(traceback.format_exc()) | ||||
if safe: | ||||
return source | ||||
else: | ||||
raise | ||||
r1604 | ||||
r1769 | @classmethod | |||
def rst_with_mentions(cls, source): | ||||
r2201 | mention_pat = re.compile(MENTIONS_REGEX) | |||
r1818 | ||||
r1769 | def wrapp(match_obj): | |||
uname = match_obj.groups()[0] | ||||
r2201 | return ' **@%(uname)s** ' % {'uname': uname} | |||
r1769 | mention_hl = mention_pat.sub(wrapp, source).strip() | |||
return cls.rst(mention_hl) | ||||