##// END OF EJS Templates
markdown: enable full githubFlavoredMarkdown
marcink -
r317:2f438a3b default
parent child Browse files
Show More
@@ -1,105 +1,106 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import re
22 22
23 23 import markdown
24 24
25 from mdx_gfm import GithubFlavoredMarkdownExtension # noqa
26
25 27
26 28 class FlavoredCheckboxExtension(markdown.Extension):
27 29
28 30 def extendMarkdown(self, md, md_globals):
29 31 md.preprocessors.add('checklist',
30 32 FlavoredCheckboxPreprocessor(md), '<reference')
31 33 md.postprocessors.add('checklist',
32 34 FlavoredCheckboxPostprocessor(md), '>unescape')
33 35
34 36
35 37 class FlavoredCheckboxPreprocessor(markdown.preprocessors.Preprocessor):
36 38 """
37 39 Replaces occurrences of [ ] or [x] to checkbox input
38 40 """
39 41
40 42 pattern = re.compile(r'^([*-]) \[([ x])\]')
41 43
42 44 def run(self, lines):
43 45 return [self._transform_line(line) for line in lines]
44 46
45 47 def _transform_line(self, line):
46 48 return self.pattern.sub(self._replacer, line)
47 49
48 50 def _replacer(self, match):
49 51 list_prefix, state = match.groups()
50 52 checked = '' if state == ' ' else ' checked="checked"'
51 53 return '%s <input type="checkbox" disabled="disabled"%s>' % (list_prefix,
52 54 checked)
53 55
54 56
55 57 class FlavoredCheckboxPostprocessor(markdown.postprocessors.Postprocessor):
56 58 """
57 59 Adds `flavored_checkbox_list` class to list of checkboxes
58 60 """
59 61
60 62 pattern = re.compile(r'^([*-]) \[([ x])\]')
61 63
62 64 def run(self, html):
63 65 before = '<ul>\n<li><input type="checkbox"'
64 66 after = '<ul class="flavored_checkbox_list">\n<li><input type="checkbox"'
65 67 return html.replace(before, after)
66 68
67 69
68
69
70 70 # Global Vars
71 71 URLIZE_RE = '(%s)' % '|'.join([
72 72 r'<(?:f|ht)tps?://[^>]*>',
73 73 r'\b(?:f|ht)tps?://[^)<>\s]+[^.,)<>\s]',
74 74 r'\bwww\.[^)<>\s]+[^.,)<>\s]',
75 75 r'[^(<\s]+\.(?:com|net|org)\b',
76 76 ])
77 77
78
78 79 class UrlizePattern(markdown.inlinepatterns.Pattern):
79 80 """ Return a link Element given an autolink (`http://example/com`). """
80 81 def handleMatch(self, m):
81 82 url = m.group(2)
82 83
83 84 if url.startswith('<'):
84 85 url = url[1:-1]
85 86
86 87 text = url
87 88
88 89 if not url.split('://')[0] in ('http','https','ftp'):
89 90 if '@' in url and not '/' in url:
90 91 url = 'mailto:' + url
91 92 else:
92 93 url = 'http://' + url
93 94
94 95 el = markdown.util.etree.Element("a")
95 96 el.set('href', url)
96 97 el.text = markdown.util.AtomicString(text)
97 98 return el
98 99
99 100
100 101 class UrlizeExtension(markdown.Extension):
101 102 """ Urlize Extension for Python-Markdown. """
102 103
103 104 def extendMarkdown(self, md, md_globals):
104 105 """ Replace autolink with UrlizePattern """
105 106 md.inlinePatterns['autolink'] = UrlizePattern(URLIZE_RE, md)
@@ -1,231 +1,230 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 Renderer for markup languages with ability to parse using rst or markdown
24 24 """
25 25
26 26
27 27 import re
28 28 import os
29 29 import logging
30 30 from mako.lookup import TemplateLookup
31 31
32 32 from docutils.core import publish_parts
33 33 from docutils.parsers.rst import directives
34 34 import markdown
35 35
36 from rhodecode.lib.markdown_ext import FlavoredCheckboxExtension, UrlizeExtension
36 from rhodecode.lib.markdown_ext import (
37 UrlizeExtension, GithubFlavoredMarkdownExtension)
37 38 from rhodecode.lib.utils2 import safe_unicode, md5_safe, MENTIONS_REGEX
38 39
39 40 log = logging.getLogger(__name__)
40 41
41 42 # default renderer used to generate automated comments
42 43 DEFAULT_COMMENTS_RENDERER = 'rst'
43 44
44 45
45 46 class MarkupRenderer(object):
46 47 RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES = ['include', 'meta', 'raw']
47 48
48 49 MARKDOWN_PAT = re.compile(r'\.(md|mkdn?|mdown|markdown)$', re.IGNORECASE)
49 50 RST_PAT = re.compile(r'\.re?st$', re.IGNORECASE)
50 51 PLAIN_PAT = re.compile(r'^readme$', re.IGNORECASE)
51 52
52 53 def _detect_renderer(self, source, filename=None):
53 54 """
54 55 runs detection of what renderer should be used for generating html
55 56 from a markup language
56 57
57 58 filename can be also explicitly a renderer name
58 59
59 60 :param source:
60 61 :param filename:
61 62 """
62 63
63 64 if MarkupRenderer.MARKDOWN_PAT.findall(filename):
64 65 detected_renderer = 'markdown'
65 66 elif MarkupRenderer.RST_PAT.findall(filename):
66 67 detected_renderer = 'rst'
67 68 elif MarkupRenderer.PLAIN_PAT.findall(filename):
68 69 detected_renderer = 'rst'
69 70 else:
70 71 detected_renderer = 'plain'
71 72
72 73 return getattr(MarkupRenderer, detected_renderer)
73 74
74 75 def render(self, source, filename=None):
75 76 """
76 77 Renders a given filename using detected renderer
77 78 it detects renderers based on file extension or mimetype.
78 79 At last it will just do a simple html replacing new lines with <br/>
79 80
80 81 :param file_name:
81 82 :param source:
82 83 """
83 84
84 85 renderer = self._detect_renderer(source, filename)
85 86 readme_data = renderer(source)
86 87 return readme_data
87 88
88 89 @classmethod
89 90 def _flavored_markdown(cls, text):
90 91 """
91 92 Github style flavored markdown
92 93
93 94 :param text:
94 95 """
95 96
96 97 # Extract pre blocks.
97 98 extractions = {}
98 99
99 100 def pre_extraction_callback(matchobj):
100 101 digest = md5_safe(matchobj.group(0))
101 102 extractions[digest] = matchobj.group(0)
102 103 return "{gfm-extraction-%s}" % digest
103 104 pattern = re.compile(r'<pre>.*?</pre>', re.MULTILINE | re.DOTALL)
104 105 text = re.sub(pattern, pre_extraction_callback, text)
105 106
106 107 # Prevent foo_bar_baz from ending up with an italic word in the middle.
107 108 def italic_callback(matchobj):
108 109 s = matchobj.group(0)
109 110 if list(s).count('_') >= 2:
110 111 return s.replace('_', r'\_')
111 112 return s
112 113 text = re.sub(r'^(?! {4}|\t)\w+_\w+_\w[\w_]*', italic_callback, text)
113 114
114 115 # Insert pre block extractions.
115 116 def pre_insert_callback(matchobj):
116 117 return '\n\n' + extractions[matchobj.group(1)]
117 118 text = re.sub(r'\{gfm-extraction-([0-9a-f]{32})\}',
118 119 pre_insert_callback, text)
119 120
120 121 return text
121 122
122 123 @classmethod
123 124 def urlify_text(cls, text):
124 125 url_pat = re.compile(r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'
125 126 r'|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)')
126 127
127 128 def url_func(match_obj):
128 129 url_full = match_obj.groups()[0]
129 130 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
130 131
131 132 return url_pat.sub(url_func, text)
132 133
133 134 @classmethod
134 135 def plain(cls, source, universal_newline=True):
135 136 source = safe_unicode(source)
136 137 if universal_newline:
137 138 newline = '\n'
138 139 source = newline.join(source.splitlines())
139 140
140 141 source = cls.urlify_text(source)
141 142 return '<br />' + source.replace("\n", '<br />')
142 143
143 144 @classmethod
144 145 def markdown(cls, source, safe=True, flavored=False, mentions=False):
145 146 # It does not allow to insert inline HTML. In presence of HTML tags, it
146 147 # will replace them instead with [HTML_REMOVED]. This is controlled by
147 148 # the safe_mode=True parameter of the markdown method.
148 149 extensions = ['codehilite', 'extra', 'def_list', 'sane_lists']
149 150 if flavored:
150 extensions.append('nl2br')
151 extensions.append(FlavoredCheckboxExtension())
152 extensions.append(UrlizeExtension())
151 extensions.append(GithubFlavoredMarkdownExtension())
153 152
154 153 if mentions:
155 154 mention_pat = re.compile(MENTIONS_REGEX)
156 155
157 156 def wrapp(match_obj):
158 157 uname = match_obj.groups()[0]
159 158 return ' **@%(uname)s** ' % {'uname': uname}
160 159 mention_hl = mention_pat.sub(wrapp, source).strip()
161 160 # we extracted mentions render with this using Mentions false
162 161 return cls.markdown(mention_hl, safe=safe, flavored=flavored,
163 162 mentions=False)
164 163
165 164 source = safe_unicode(source)
166 165 try:
167 166 if flavored:
168 167 source = cls._flavored_markdown(source)
169 168 return markdown.markdown(
170 169 source, extensions, safe_mode=True, enable_attributes=False)
171 170 except Exception:
172 171 log.exception('Error when rendering Markdown')
173 172 if safe:
174 173 log.debug('Fallbacking to render in plain mode')
175 174 return cls.plain(source)
176 175 else:
177 176 raise
178 177
179 178 @classmethod
180 179 def rst(cls, source, safe=True, mentions=False):
181 180 if mentions:
182 181 mention_pat = re.compile(MENTIONS_REGEX)
183 182
184 183 def wrapp(match_obj):
185 184 uname = match_obj.groups()[0]
186 185 return ' **@%(uname)s** ' % {'uname': uname}
187 186 mention_hl = mention_pat.sub(wrapp, source).strip()
188 187 # we extracted mentions render with this using Mentions false
189 188 return cls.rst(mention_hl, safe=safe, mentions=False)
190 189
191 190 source = safe_unicode(source)
192 191 try:
193 192 docutils_settings = dict([(alias, None) for alias in
194 193 cls.RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES])
195 194
196 195 docutils_settings.update({'input_encoding': 'unicode',
197 196 'report_level': 4})
198 197
199 198 for k, v in docutils_settings.iteritems():
200 199 directives.register_directive(k, v)
201 200
202 201 parts = publish_parts(source=source,
203 202 writer_name="html4css1",
204 203 settings_overrides=docutils_settings)
205 204
206 205 return parts['html_title'] + parts["fragment"]
207 206 except Exception:
208 207 log.exception('Error when rendering RST')
209 208 if safe:
210 209 log.debug('Fallbacking to render in plain mode')
211 210 return cls.plain(source)
212 211 else:
213 212 raise
214 213
215 214
216 215 class RstTemplateRenderer(object):
217 216
218 217 def __init__(self):
219 218 base = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
220 219 rst_template_dirs = [os.path.join(base, 'templates', 'rst_templates')]
221 220 self.template_store = TemplateLookup(
222 221 directories=rst_template_dirs,
223 222 input_encoding='utf-8',
224 223 imports=['from rhodecode.lib import helpers as h'])
225 224
226 225 def _get_template(self, templatename):
227 226 return self.template_store.get_template(templatename)
228 227
229 228 def render(self, template_name, **kwargs):
230 229 template = self._get_template(template_name)
231 230 return template.render(**kwargs)
General Comments 0
You need to be logged in to leave comments. Login now