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