##// END OF EJS Templates
renderer: fixed the helper funtion to original version. This...
marcink -
r401:40cb52d4 default
parent child Browse files
Show More
@@ -1,305 +1,306 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 import re
26 import re
27 import os
27 import os
28 import logging
28 import logging
29 import itertools
29 import itertools
30
30
31 from mako.lookup import TemplateLookup
31 from mako.lookup import TemplateLookup
32
32
33 from docutils.core import publish_parts
33 from docutils.core import publish_parts
34 from docutils.parsers.rst import directives
34 from docutils.parsers.rst import directives
35 import markdown
35 import markdown
36
36
37 from rhodecode.lib.markdown_ext import (
37 from rhodecode.lib.markdown_ext import (
38 UrlizeExtension, GithubFlavoredMarkdownExtension)
38 UrlizeExtension, GithubFlavoredMarkdownExtension)
39 from rhodecode.lib.utils2 import safe_unicode, md5_safe, MENTIONS_REGEX
39 from rhodecode.lib.utils2 import safe_unicode, md5_safe, MENTIONS_REGEX
40
40
41 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
42
42
43 # default renderer used to generate automated comments
43 # default renderer used to generate automated comments
44 DEFAULT_COMMENTS_RENDERER = 'rst'
44 DEFAULT_COMMENTS_RENDERER = 'rst'
45
45
46
46
47 class MarkupRenderer(object):
47 class MarkupRenderer(object):
48 RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES = ['include', 'meta', 'raw']
48 RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES = ['include', 'meta', 'raw']
49
49
50 MARKDOWN_PAT = re.compile(r'\.(md|mkdn?|mdown|markdown)$', re.IGNORECASE)
50 MARKDOWN_PAT = re.compile(r'\.(md|mkdn?|mdown|markdown)$', re.IGNORECASE)
51 RST_PAT = re.compile(r'\.re?st$', re.IGNORECASE)
51 RST_PAT = re.compile(r'\.re?st$', re.IGNORECASE)
52 PLAIN_PAT = re.compile(r'^readme$', re.IGNORECASE)
52 PLAIN_PAT = re.compile(r'^readme$', re.IGNORECASE)
53
53
54 # list of readme files to search in file tree and display in summary
54 # list of readme files to search in file tree and display in summary
55 # attached weights defines the search order lower is first
55 # attached weights defines the search order lower is first
56 ALL_READMES = [
56 ALL_READMES = [
57 ('readme', 0), ('README', 0), ('Readme', 0),
57 ('readme', 0), ('README', 0), ('Readme', 0),
58 ('doc/readme', 1), ('doc/README', 1), ('doc/Readme', 1),
58 ('doc/readme', 1), ('doc/README', 1), ('doc/Readme', 1),
59 ('Docs/readme', 2), ('Docs/README', 2), ('Docs/Readme', 2),
59 ('Docs/readme', 2), ('Docs/README', 2), ('Docs/Readme', 2),
60 ('DOCS/readme', 2), ('DOCS/README', 2), ('DOCS/Readme', 2),
60 ('DOCS/readme', 2), ('DOCS/README', 2), ('DOCS/Readme', 2),
61 ('docs/readme', 2), ('docs/README', 2), ('docs/Readme', 2),
61 ('docs/readme', 2), ('docs/README', 2), ('docs/Readme', 2),
62 ]
62 ]
63 # extension together with weights. Lower is first means we control how
63 # extension together with weights. Lower is first means we control how
64 # extensions are attached to readme names with those.
64 # extensions are attached to readme names with those.
65 PLAIN_EXTS = [
65 PLAIN_EXTS = [
66 ('', 0), # special case that renders READMES names without extension
66 ('', 0), # special case that renders READMES names without extension
67 ('.text', 2), ('.TEXT', 2),
67 ('.text', 2), ('.TEXT', 2),
68 ('.txt', 3), ('.TXT', 3)
68 ('.txt', 3), ('.TXT', 3)
69 ]
69 ]
70
70
71 RST_EXTS = [
71 RST_EXTS = [
72 ('.rst', 1), ('.rest', 1),
72 ('.rst', 1), ('.rest', 1),
73 ('.RST', 2), ('.REST', 2)
73 ('.RST', 2), ('.REST', 2)
74 ]
74 ]
75
75
76 MARKDOWN_EXTS = [
76 MARKDOWN_EXTS = [
77 ('.md', 1), ('.MD', 1),
77 ('.md', 1), ('.MD', 1),
78 ('.mkdn', 2), ('.MKDN', 2),
78 ('.mkdn', 2), ('.MKDN', 2),
79 ('.mdown', 3), ('.MDOWN', 3),
79 ('.mdown', 3), ('.MDOWN', 3),
80 ('.markdown', 4), ('.MARKDOWN', 4)
80 ('.markdown', 4), ('.MARKDOWN', 4)
81 ]
81 ]
82
82
83 ALL_EXTS = PLAIN_EXTS + MARKDOWN_EXTS + RST_EXTS
83 ALL_EXTS = PLAIN_EXTS + MARKDOWN_EXTS + RST_EXTS
84
84
85 def _detect_renderer(self, source, filename=None):
85 def _detect_renderer(self, source, filename=None):
86 """
86 """
87 runs detection of what renderer should be used for generating html
87 runs detection of what renderer should be used for generating html
88 from a markup language
88 from a markup language
89
89
90 filename can be also explicitly a renderer name
90 filename can be also explicitly a renderer name
91
91
92 :param source:
92 :param source:
93 :param filename:
93 :param filename:
94 """
94 """
95
95
96 if MarkupRenderer.MARKDOWN_PAT.findall(filename):
96 if MarkupRenderer.MARKDOWN_PAT.findall(filename):
97 detected_renderer = 'markdown'
97 detected_renderer = 'markdown'
98 elif MarkupRenderer.RST_PAT.findall(filename):
98 elif MarkupRenderer.RST_PAT.findall(filename):
99 detected_renderer = 'rst'
99 detected_renderer = 'rst'
100 elif MarkupRenderer.PLAIN_PAT.findall(filename):
100 elif MarkupRenderer.PLAIN_PAT.findall(filename):
101 detected_renderer = 'rst'
101 detected_renderer = 'rst'
102 else:
102 else:
103 detected_renderer = 'plain'
103 detected_renderer = 'plain'
104
104
105 return getattr(MarkupRenderer, detected_renderer)
105 return getattr(MarkupRenderer, detected_renderer)
106
106
107 @classmethod
107 @classmethod
108 def renderer_from_filename(cls, filename, exclude):
108 def renderer_from_filename(cls, filename, exclude):
109 """
109 """
110 Detect renderer from filename and optionally use exlcude list to
110 Detect renderer markdown/rst from filename and optionally use exclude
111 remove some options. This is mostly used in helpers
111 list to remove some options. This is mostly used in helpers.
112 Returns None when no renderer can be detected.
112 """
113 """
113 def _filter(elements):
114 def _filter(elements):
114 if isinstance(exclude, (list, tuple)):
115 if isinstance(exclude, (list, tuple)):
115 return [x for x in elements if x not in exclude]
116 return [x for x in elements if x not in exclude]
116 return elements
117 return elements
117
118
118 if filename.endswith(
119 if filename.endswith(
119 tuple(_filter([x[0] for x in cls.MARKDOWN_EXTS if x[0]]))):
120 tuple(_filter([x[0] for x in cls.MARKDOWN_EXTS if x[0]]))):
120 return 'markdown'
121 return 'markdown'
121 if filename.endswith(tuple(_filter([x[0] for x in cls.RST_EXTS if x[0]]))):
122 if filename.endswith(tuple(_filter([x[0] for x in cls.RST_EXTS if x[0]]))):
122 return 'rst'
123 return 'rst'
123
124
124 return 'plain'
125 return None
125
126
126 @classmethod
127 @classmethod
127 def generate_readmes(cls, all_readmes, extensions):
128 def generate_readmes(cls, all_readmes, extensions):
128 combined = itertools.product(all_readmes, extensions)
129 combined = itertools.product(all_readmes, extensions)
129 # sort by filename weight(y[0][1]) + extensions weight(y[1][1])
130 # sort by filename weight(y[0][1]) + extensions weight(y[1][1])
130 prioritized_readmes = sorted(combined, key=lambda y: y[0][1] + y[1][1])
131 prioritized_readmes = sorted(combined, key=lambda y: y[0][1] + y[1][1])
131 # filename, extension
132 # filename, extension
132 return [''.join([x[0][0], x[1][0]]) for x in prioritized_readmes]
133 return [''.join([x[0][0], x[1][0]]) for x in prioritized_readmes]
133
134
134 def pick_readme_order(self, default_renderer):
135 def pick_readme_order(self, default_renderer):
135
136
136 if default_renderer == 'markdown':
137 if default_renderer == 'markdown':
137 markdown = self.generate_readmes(self.ALL_READMES, self.MARKDOWN_EXTS)
138 markdown = self.generate_readmes(self.ALL_READMES, self.MARKDOWN_EXTS)
138 readme_order = markdown + self.generate_readmes(
139 readme_order = markdown + self.generate_readmes(
139 self.ALL_READMES, self.RST_EXTS + self.PLAIN_EXTS)
140 self.ALL_READMES, self.RST_EXTS + self.PLAIN_EXTS)
140 elif default_renderer == 'rst':
141 elif default_renderer == 'rst':
141 markdown = self.generate_readmes(self.ALL_READMES, self.RST_EXTS)
142 markdown = self.generate_readmes(self.ALL_READMES, self.RST_EXTS)
142 readme_order = markdown + self.generate_readmes(
143 readme_order = markdown + self.generate_readmes(
143 self.ALL_READMES, self.MARKDOWN_EXTS + self.PLAIN_EXTS)
144 self.ALL_READMES, self.MARKDOWN_EXTS + self.PLAIN_EXTS)
144 else:
145 else:
145 readme_order = self.generate_readmes(self.ALL_READMES, self.ALL_EXTS)
146 readme_order = self.generate_readmes(self.ALL_READMES, self.ALL_EXTS)
146
147
147 return readme_order
148 return readme_order
148
149
149 def render(self, source, filename=None):
150 def render(self, source, filename=None):
150 """
151 """
151 Renders a given filename using detected renderer
152 Renders a given filename using detected renderer
152 it detects renderers based on file extension or mimetype.
153 it detects renderers based on file extension or mimetype.
153 At last it will just do a simple html replacing new lines with <br/>
154 At last it will just do a simple html replacing new lines with <br/>
154
155
155 :param file_name:
156 :param file_name:
156 :param source:
157 :param source:
157 """
158 """
158
159
159 renderer = self._detect_renderer(source, filename)
160 renderer = self._detect_renderer(source, filename)
160 readme_data = renderer(source)
161 readme_data = renderer(source)
161 return readme_data
162 return readme_data
162
163
163 @classmethod
164 @classmethod
164 def _flavored_markdown(cls, text):
165 def _flavored_markdown(cls, text):
165 """
166 """
166 Github style flavored markdown
167 Github style flavored markdown
167
168
168 :param text:
169 :param text:
169 """
170 """
170
171
171 # Extract pre blocks.
172 # Extract pre blocks.
172 extractions = {}
173 extractions = {}
173
174
174 def pre_extraction_callback(matchobj):
175 def pre_extraction_callback(matchobj):
175 digest = md5_safe(matchobj.group(0))
176 digest = md5_safe(matchobj.group(0))
176 extractions[digest] = matchobj.group(0)
177 extractions[digest] = matchobj.group(0)
177 return "{gfm-extraction-%s}" % digest
178 return "{gfm-extraction-%s}" % digest
178 pattern = re.compile(r'<pre>.*?</pre>', re.MULTILINE | re.DOTALL)
179 pattern = re.compile(r'<pre>.*?</pre>', re.MULTILINE | re.DOTALL)
179 text = re.sub(pattern, pre_extraction_callback, text)
180 text = re.sub(pattern, pre_extraction_callback, text)
180
181
181 # Prevent foo_bar_baz from ending up with an italic word in the middle.
182 # Prevent foo_bar_baz from ending up with an italic word in the middle.
182 def italic_callback(matchobj):
183 def italic_callback(matchobj):
183 s = matchobj.group(0)
184 s = matchobj.group(0)
184 if list(s).count('_') >= 2:
185 if list(s).count('_') >= 2:
185 return s.replace('_', r'\_')
186 return s.replace('_', r'\_')
186 return s
187 return s
187 text = re.sub(r'^(?! {4}|\t)\w+_\w+_\w[\w_]*', italic_callback, text)
188 text = re.sub(r'^(?! {4}|\t)\w+_\w+_\w[\w_]*', italic_callback, text)
188
189
189 # Insert pre block extractions.
190 # Insert pre block extractions.
190 def pre_insert_callback(matchobj):
191 def pre_insert_callback(matchobj):
191 return '\n\n' + extractions[matchobj.group(1)]
192 return '\n\n' + extractions[matchobj.group(1)]
192 text = re.sub(r'\{gfm-extraction-([0-9a-f]{32})\}',
193 text = re.sub(r'\{gfm-extraction-([0-9a-f]{32})\}',
193 pre_insert_callback, text)
194 pre_insert_callback, text)
194
195
195 return text
196 return text
196
197
197 @classmethod
198 @classmethod
198 def urlify_text(cls, text):
199 def urlify_text(cls, text):
199 url_pat = re.compile(r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'
200 url_pat = re.compile(r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'
200 r'|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)')
201 r'|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)')
201
202
202 def url_func(match_obj):
203 def url_func(match_obj):
203 url_full = match_obj.groups()[0]
204 url_full = match_obj.groups()[0]
204 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
205 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
205
206
206 return url_pat.sub(url_func, text)
207 return url_pat.sub(url_func, text)
207
208
208 @classmethod
209 @classmethod
209 def plain(cls, source, universal_newline=True):
210 def plain(cls, source, universal_newline=True):
210 source = safe_unicode(source)
211 source = safe_unicode(source)
211 if universal_newline:
212 if universal_newline:
212 newline = '\n'
213 newline = '\n'
213 source = newline.join(source.splitlines())
214 source = newline.join(source.splitlines())
214
215
215 source = cls.urlify_text(source)
216 source = cls.urlify_text(source)
216 return '<br />' + source.replace("\n", '<br />')
217 return '<br />' + source.replace("\n", '<br />')
217
218
218 @classmethod
219 @classmethod
219 def markdown(cls, source, safe=True, flavored=True, mentions=False):
220 def markdown(cls, source, safe=True, flavored=True, mentions=False):
220 # It does not allow to insert inline HTML. In presence of HTML tags, it
221 # It does not allow to insert inline HTML. In presence of HTML tags, it
221 # will replace them instead with [HTML_REMOVED]. This is controlled by
222 # will replace them instead with [HTML_REMOVED]. This is controlled by
222 # the safe_mode=True parameter of the markdown method.
223 # the safe_mode=True parameter of the markdown method.
223 extensions = ['codehilite', 'extra', 'def_list', 'sane_lists']
224 extensions = ['codehilite', 'extra', 'def_list', 'sane_lists']
224 if flavored:
225 if flavored:
225 extensions.append(GithubFlavoredMarkdownExtension())
226 extensions.append(GithubFlavoredMarkdownExtension())
226
227
227 if mentions:
228 if mentions:
228 mention_pat = re.compile(MENTIONS_REGEX)
229 mention_pat = re.compile(MENTIONS_REGEX)
229
230
230 def wrapp(match_obj):
231 def wrapp(match_obj):
231 uname = match_obj.groups()[0]
232 uname = match_obj.groups()[0]
232 return ' **@%(uname)s** ' % {'uname': uname}
233 return ' **@%(uname)s** ' % {'uname': uname}
233 mention_hl = mention_pat.sub(wrapp, source).strip()
234 mention_hl = mention_pat.sub(wrapp, source).strip()
234 # we extracted mentions render with this using Mentions false
235 # we extracted mentions render with this using Mentions false
235 return cls.markdown(mention_hl, safe=safe, flavored=flavored,
236 return cls.markdown(mention_hl, safe=safe, flavored=flavored,
236 mentions=False)
237 mentions=False)
237
238
238 source = safe_unicode(source)
239 source = safe_unicode(source)
239 try:
240 try:
240 if flavored:
241 if flavored:
241 source = cls._flavored_markdown(source)
242 source = cls._flavored_markdown(source)
242 return markdown.markdown(
243 return markdown.markdown(
243 source, extensions, safe_mode=True, enable_attributes=False)
244 source, extensions, safe_mode=True, enable_attributes=False)
244 except Exception:
245 except Exception:
245 log.exception('Error when rendering Markdown')
246 log.exception('Error when rendering Markdown')
246 if safe:
247 if safe:
247 log.debug('Fallback to render in plain mode')
248 log.debug('Fallback to render in plain mode')
248 return cls.plain(source)
249 return cls.plain(source)
249 else:
250 else:
250 raise
251 raise
251
252
252 @classmethod
253 @classmethod
253 def rst(cls, source, safe=True, mentions=False):
254 def rst(cls, source, safe=True, mentions=False):
254 if mentions:
255 if mentions:
255 mention_pat = re.compile(MENTIONS_REGEX)
256 mention_pat = re.compile(MENTIONS_REGEX)
256
257
257 def wrapp(match_obj):
258 def wrapp(match_obj):
258 uname = match_obj.groups()[0]
259 uname = match_obj.groups()[0]
259 return ' **@%(uname)s** ' % {'uname': uname}
260 return ' **@%(uname)s** ' % {'uname': uname}
260 mention_hl = mention_pat.sub(wrapp, source).strip()
261 mention_hl = mention_pat.sub(wrapp, source).strip()
261 # we extracted mentions render with this using Mentions false
262 # we extracted mentions render with this using Mentions false
262 return cls.rst(mention_hl, safe=safe, mentions=False)
263 return cls.rst(mention_hl, safe=safe, mentions=False)
263
264
264 source = safe_unicode(source)
265 source = safe_unicode(source)
265 try:
266 try:
266 docutils_settings = dict(
267 docutils_settings = dict(
267 [(alias, None) for alias in
268 [(alias, None) for alias in
268 cls.RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES])
269 cls.RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES])
269
270
270 docutils_settings.update({'input_encoding': 'unicode',
271 docutils_settings.update({'input_encoding': 'unicode',
271 'report_level': 4})
272 'report_level': 4})
272
273
273 for k, v in docutils_settings.iteritems():
274 for k, v in docutils_settings.iteritems():
274 directives.register_directive(k, v)
275 directives.register_directive(k, v)
275
276
276 parts = publish_parts(source=source,
277 parts = publish_parts(source=source,
277 writer_name="html4css1",
278 writer_name="html4css1",
278 settings_overrides=docutils_settings)
279 settings_overrides=docutils_settings)
279
280
280 return parts['html_title'] + parts["fragment"]
281 return parts['html_title'] + parts["fragment"]
281 except Exception:
282 except Exception:
282 log.exception('Error when rendering RST')
283 log.exception('Error when rendering RST')
283 if safe:
284 if safe:
284 log.debug('Fallbacking to render in plain mode')
285 log.debug('Fallbacking to render in plain mode')
285 return cls.plain(source)
286 return cls.plain(source)
286 else:
287 else:
287 raise
288 raise
288
289
289
290
290 class RstTemplateRenderer(object):
291 class RstTemplateRenderer(object):
291
292
292 def __init__(self):
293 def __init__(self):
293 base = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
294 base = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
294 rst_template_dirs = [os.path.join(base, 'templates', 'rst_templates')]
295 rst_template_dirs = [os.path.join(base, 'templates', 'rst_templates')]
295 self.template_store = TemplateLookup(
296 self.template_store = TemplateLookup(
296 directories=rst_template_dirs,
297 directories=rst_template_dirs,
297 input_encoding='utf-8',
298 input_encoding='utf-8',
298 imports=['from rhodecode.lib import helpers as h'])
299 imports=['from rhodecode.lib import helpers as h'])
299
300
300 def _get_template(self, templatename):
301 def _get_template(self, templatename):
301 return self.template_store.get_template(templatename)
302 return self.template_store.get_template(templatename)
302
303
303 def render(self, template_name, **kwargs):
304 def render(self, template_name, **kwargs):
304 template = self._get_template(template_name)
305 template = self._get_template(template_name)
305 return template.render(**kwargs)
306 return template.render(**kwargs)
General Comments 0
You need to be logged in to leave comments. Login now