##// END OF EJS Templates
markup-renderers: fixed code highlite for rst
marcink -
r4117:81d225a3 default
parent child Browse files
Show More
@@ -1,559 +1,564 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2019 RhodeCode GmbH
3 # Copyright (C) 2011-2019 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 lxml
28 import lxml
29 import logging
29 import logging
30 import urlparse
30 import urlparse
31 import bleach
31 import bleach
32
32
33 from mako.lookup import TemplateLookup
33 from mako.lookup import TemplateLookup
34 from mako.template import Template as MakoTemplate
34 from mako.template import Template as MakoTemplate
35
35
36 from docutils.core import publish_parts
36 from docutils.core import publish_parts
37 from docutils.parsers.rst import directives
37 from docutils.parsers.rst import directives
38 from docutils import writers
38 from docutils import writers
39 from docutils.writers import html4css1
39 from docutils.writers import html4css1
40 import markdown
40 import markdown
41
41
42 from rhodecode.lib.markdown_ext import GithubFlavoredMarkdownExtension
42 from rhodecode.lib.markdown_ext import GithubFlavoredMarkdownExtension
43 from rhodecode.lib.utils2 import (safe_unicode, md5_safe, MENTIONS_REGEX)
43 from rhodecode.lib.utils2 import (safe_unicode, md5_safe, MENTIONS_REGEX)
44
44
45 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
46
46
47 # default renderer used to generate automated comments
47 # default renderer used to generate automated comments
48 DEFAULT_COMMENTS_RENDERER = 'rst'
48 DEFAULT_COMMENTS_RENDERER = 'rst'
49
49
50
50
51 class CustomHTMLTranslator(writers.html4css1.HTMLTranslator):
51 class CustomHTMLTranslator(writers.html4css1.HTMLTranslator):
52 """
52 """
53 Custom HTML Translator used for sandboxing potential
53 Custom HTML Translator used for sandboxing potential
54 JS injections in ref links
54 JS injections in ref links
55 """
55 """
56 def visit_literal_block(self, node):
57 self.body.append(self.starttag(node, 'pre', CLASS='codehilite literal-block'))
56
58
57 def visit_reference(self, node):
59 def visit_reference(self, node):
58 if 'refuri' in node.attributes:
60 if 'refuri' in node.attributes:
59 refuri = node['refuri']
61 refuri = node['refuri']
60 if ':' in refuri:
62 if ':' in refuri:
61 prefix, link = refuri.lstrip().split(':', 1)
63 prefix, link = refuri.lstrip().split(':', 1)
62 prefix = prefix or ''
64 prefix = prefix or ''
63
65
64 if prefix.lower() == 'javascript':
66 if prefix.lower() == 'javascript':
65 # we don't allow javascript type of refs...
67 # we don't allow javascript type of refs...
66 node['refuri'] = 'javascript:alert("SandBoxedJavascript")'
68 node['refuri'] = 'javascript:alert("SandBoxedJavascript")'
67
69
68 # old style class requires this...
70 # old style class requires this...
69 return html4css1.HTMLTranslator.visit_reference(self, node)
71 return html4css1.HTMLTranslator.visit_reference(self, node)
70
72
71
73
72 class RhodeCodeWriter(writers.html4css1.Writer):
74 class RhodeCodeWriter(writers.html4css1.Writer):
73 def __init__(self):
75 def __init__(self):
74 writers.Writer.__init__(self)
76 writers.Writer.__init__(self)
75 self.translator_class = CustomHTMLTranslator
77 self.translator_class = CustomHTMLTranslator
76
78
77
79
78 def relative_links(html_source, server_paths):
80 def relative_links(html_source, server_paths):
79 if not html_source:
81 if not html_source:
80 return html_source
82 return html_source
81
83
82 try:
84 try:
83 from lxml.html import fromstring
85 from lxml.html import fromstring
84 from lxml.html import tostring
86 from lxml.html import tostring
85 except ImportError:
87 except ImportError:
86 log.exception('Failed to import lxml')
88 log.exception('Failed to import lxml')
87 return html_source
89 return html_source
88
90
89 try:
91 try:
90 doc = lxml.html.fromstring(html_source)
92 doc = lxml.html.fromstring(html_source)
91 except Exception:
93 except Exception:
92 return html_source
94 return html_source
93
95
94 for el in doc.cssselect('img, video'):
96 for el in doc.cssselect('img, video'):
95 src = el.attrib.get('src')
97 src = el.attrib.get('src')
96 if src:
98 if src:
97 el.attrib['src'] = relative_path(src, server_paths['raw'])
99 el.attrib['src'] = relative_path(src, server_paths['raw'])
98
100
99 for el in doc.cssselect('a:not(.gfm)'):
101 for el in doc.cssselect('a:not(.gfm)'):
100 src = el.attrib.get('href')
102 src = el.attrib.get('href')
101 if src:
103 if src:
102 raw_mode = el.attrib['href'].endswith('?raw=1')
104 raw_mode = el.attrib['href'].endswith('?raw=1')
103 if raw_mode:
105 if raw_mode:
104 el.attrib['href'] = relative_path(src, server_paths['raw'])
106 el.attrib['href'] = relative_path(src, server_paths['raw'])
105 else:
107 else:
106 el.attrib['href'] = relative_path(src, server_paths['standard'])
108 el.attrib['href'] = relative_path(src, server_paths['standard'])
107
109
108 return lxml.html.tostring(doc)
110 return lxml.html.tostring(doc)
109
111
110
112
111 def relative_path(path, request_path, is_repo_file=None):
113 def relative_path(path, request_path, is_repo_file=None):
112 """
114 """
113 relative link support, path is a rel path, and request_path is current
115 relative link support, path is a rel path, and request_path is current
114 server path (not absolute)
116 server path (not absolute)
115
117
116 e.g.
118 e.g.
117
119
118 path = '../logo.png'
120 path = '../logo.png'
119 request_path= '/repo/files/path/file.md'
121 request_path= '/repo/files/path/file.md'
120 produces: '/repo/files/logo.png'
122 produces: '/repo/files/logo.png'
121 """
123 """
122 # TODO(marcink): unicode/str support ?
124 # TODO(marcink): unicode/str support ?
123 # maybe=> safe_unicode(urllib.quote(safe_str(final_path), '/:'))
125 # maybe=> safe_unicode(urllib.quote(safe_str(final_path), '/:'))
124
126
125 def dummy_check(p):
127 def dummy_check(p):
126 return True # assume default is a valid file path
128 return True # assume default is a valid file path
127
129
128 is_repo_file = is_repo_file or dummy_check
130 is_repo_file = is_repo_file or dummy_check
129 if not path:
131 if not path:
130 return request_path
132 return request_path
131
133
132 path = safe_unicode(path)
134 path = safe_unicode(path)
133 request_path = safe_unicode(request_path)
135 request_path = safe_unicode(request_path)
134
136
135 if path.startswith((u'data:', u'javascript:', u'#', u':')):
137 if path.startswith((u'data:', u'javascript:', u'#', u':')):
136 # skip data, anchor, invalid links
138 # skip data, anchor, invalid links
137 return path
139 return path
138
140
139 is_absolute = bool(urlparse.urlparse(path).netloc)
141 is_absolute = bool(urlparse.urlparse(path).netloc)
140 if is_absolute:
142 if is_absolute:
141 return path
143 return path
142
144
143 if not request_path:
145 if not request_path:
144 return path
146 return path
145
147
146 if path.startswith(u'/'):
148 if path.startswith(u'/'):
147 path = path[1:]
149 path = path[1:]
148
150
149 if path.startswith(u'./'):
151 if path.startswith(u'./'):
150 path = path[2:]
152 path = path[2:]
151
153
152 parts = request_path.split('/')
154 parts = request_path.split('/')
153 # compute how deep we need to traverse the request_path
155 # compute how deep we need to traverse the request_path
154 depth = 0
156 depth = 0
155
157
156 if is_repo_file(request_path):
158 if is_repo_file(request_path):
157 # if request path is a VALID file, we use a relative path with
159 # if request path is a VALID file, we use a relative path with
158 # one level up
160 # one level up
159 depth += 1
161 depth += 1
160
162
161 while path.startswith(u'../'):
163 while path.startswith(u'../'):
162 depth += 1
164 depth += 1
163 path = path[3:]
165 path = path[3:]
164
166
165 if depth > 0:
167 if depth > 0:
166 parts = parts[:-depth]
168 parts = parts[:-depth]
167
169
168 parts.append(path)
170 parts.append(path)
169 final_path = u'/'.join(parts).lstrip(u'/')
171 final_path = u'/'.join(parts).lstrip(u'/')
170
172
171 return u'/' + final_path
173 return u'/' + final_path
172
174
173
175
174 _cached_markdown_renderer = None
176 _cached_markdown_renderer = None
175
177
176
178
177 def get_markdown_renderer(extensions, output_format):
179 def get_markdown_renderer(extensions, output_format):
178 global _cached_markdown_renderer
180 global _cached_markdown_renderer
179
181
180 if _cached_markdown_renderer is None:
182 if _cached_markdown_renderer is None:
181 _cached_markdown_renderer = markdown.Markdown(
183 _cached_markdown_renderer = markdown.Markdown(
182 extensions=extensions,
184 extensions=extensions,
183 enable_attributes=False, output_format=output_format)
185 enable_attributes=False, output_format=output_format)
184 return _cached_markdown_renderer
186 return _cached_markdown_renderer
185
187
186
188
187 _cached_markdown_renderer_flavored = None
189 _cached_markdown_renderer_flavored = None
188
190
189
191
190 def get_markdown_renderer_flavored(extensions, output_format):
192 def get_markdown_renderer_flavored(extensions, output_format):
191 global _cached_markdown_renderer_flavored
193 global _cached_markdown_renderer_flavored
192
194
193 if _cached_markdown_renderer_flavored is None:
195 if _cached_markdown_renderer_flavored is None:
194 _cached_markdown_renderer_flavored = markdown.Markdown(
196 _cached_markdown_renderer_flavored = markdown.Markdown(
195 extensions=extensions + [GithubFlavoredMarkdownExtension()],
197 extensions=extensions + [GithubFlavoredMarkdownExtension()],
196 enable_attributes=False, output_format=output_format)
198 enable_attributes=False, output_format=output_format)
197 return _cached_markdown_renderer_flavored
199 return _cached_markdown_renderer_flavored
198
200
199
201
200 class MarkupRenderer(object):
202 class MarkupRenderer(object):
201 RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES = ['include', 'meta', 'raw']
203 RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES = ['include', 'meta', 'raw']
202
204
203 MARKDOWN_PAT = re.compile(r'\.(md|mkdn?|mdown|markdown)$', re.IGNORECASE)
205 MARKDOWN_PAT = re.compile(r'\.(md|mkdn?|mdown|markdown)$', re.IGNORECASE)
204 RST_PAT = re.compile(r'\.re?st$', re.IGNORECASE)
206 RST_PAT = re.compile(r'\.re?st$', re.IGNORECASE)
205 JUPYTER_PAT = re.compile(r'\.(ipynb)$', re.IGNORECASE)
207 JUPYTER_PAT = re.compile(r'\.(ipynb)$', re.IGNORECASE)
206 PLAIN_PAT = re.compile(r'^readme$', re.IGNORECASE)
208 PLAIN_PAT = re.compile(r'^readme$', re.IGNORECASE)
207
209
208 URL_PAT = re.compile(r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'
210 URL_PAT = re.compile(r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'
209 r'|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)')
211 r'|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)')
210
212
211 extensions = ['markdown.extensions.codehilite', 'markdown.extensions.extra',
213 extensions = ['markdown.extensions.codehilite', 'markdown.extensions.extra',
212 'markdown.extensions.def_list', 'markdown.extensions.sane_lists']
214 'markdown.extensions.def_list', 'markdown.extensions.sane_lists']
213
215
214 output_format = 'html4'
216 output_format = 'html4'
215
217
216 # extension together with weights. Lower is first means we control how
218 # extension together with weights. Lower is first means we control how
217 # extensions are attached to readme names with those.
219 # extensions are attached to readme names with those.
218 PLAIN_EXTS = [
220 PLAIN_EXTS = [
219 # prefer no extension
221 # prefer no extension
220 ('', 0), # special case that renders READMES names without extension
222 ('', 0), # special case that renders READMES names without extension
221 ('.text', 2), ('.TEXT', 2),
223 ('.text', 2), ('.TEXT', 2),
222 ('.txt', 3), ('.TXT', 3)
224 ('.txt', 3), ('.TXT', 3)
223 ]
225 ]
224
226
225 RST_EXTS = [
227 RST_EXTS = [
226 ('.rst', 1), ('.rest', 1),
228 ('.rst', 1), ('.rest', 1),
227 ('.RST', 2), ('.REST', 2)
229 ('.RST', 2), ('.REST', 2)
228 ]
230 ]
229
231
230 MARKDOWN_EXTS = [
232 MARKDOWN_EXTS = [
231 ('.md', 1), ('.MD', 1),
233 ('.md', 1), ('.MD', 1),
232 ('.mkdn', 2), ('.MKDN', 2),
234 ('.mkdn', 2), ('.MKDN', 2),
233 ('.mdown', 3), ('.MDOWN', 3),
235 ('.mdown', 3), ('.MDOWN', 3),
234 ('.markdown', 4), ('.MARKDOWN', 4)
236 ('.markdown', 4), ('.MARKDOWN', 4)
235 ]
237 ]
236
238
237 def _detect_renderer(self, source, filename=None):
239 def _detect_renderer(self, source, filename=None):
238 """
240 """
239 runs detection of what renderer should be used for generating html
241 runs detection of what renderer should be used for generating html
240 from a markup language
242 from a markup language
241
243
242 filename can be also explicitly a renderer name
244 filename can be also explicitly a renderer name
243
245
244 :param source:
246 :param source:
245 :param filename:
247 :param filename:
246 """
248 """
247
249
248 if MarkupRenderer.MARKDOWN_PAT.findall(filename):
250 if MarkupRenderer.MARKDOWN_PAT.findall(filename):
249 detected_renderer = 'markdown'
251 detected_renderer = 'markdown'
250 elif MarkupRenderer.RST_PAT.findall(filename):
252 elif MarkupRenderer.RST_PAT.findall(filename):
251 detected_renderer = 'rst'
253 detected_renderer = 'rst'
252 elif MarkupRenderer.JUPYTER_PAT.findall(filename):
254 elif MarkupRenderer.JUPYTER_PAT.findall(filename):
253 detected_renderer = 'jupyter'
255 detected_renderer = 'jupyter'
254 elif MarkupRenderer.PLAIN_PAT.findall(filename):
256 elif MarkupRenderer.PLAIN_PAT.findall(filename):
255 detected_renderer = 'plain'
257 detected_renderer = 'plain'
256 else:
258 else:
257 detected_renderer = 'plain'
259 detected_renderer = 'plain'
258
260
259 return getattr(MarkupRenderer, detected_renderer)
261 return getattr(MarkupRenderer, detected_renderer)
260
262
261 @classmethod
263 @classmethod
262 def bleach_clean(cls, text):
264 def bleach_clean(cls, text):
263 from .bleach_whitelist import markdown_attrs, markdown_tags
265 from .bleach_whitelist import markdown_attrs, markdown_tags
264 allowed_tags = markdown_tags
266 allowed_tags = markdown_tags
265 allowed_attrs = markdown_attrs
267 allowed_attrs = markdown_attrs
266
268
267 try:
269 try:
268 return bleach.clean(text, tags=allowed_tags, attributes=allowed_attrs)
270 return bleach.clean(text, tags=allowed_tags, attributes=allowed_attrs)
269 except Exception:
271 except Exception:
270 return 'UNPARSEABLE TEXT'
272 return 'UNPARSEABLE TEXT'
271
273
272 @classmethod
274 @classmethod
273 def renderer_from_filename(cls, filename, exclude):
275 def renderer_from_filename(cls, filename, exclude):
274 """
276 """
275 Detect renderer markdown/rst from filename and optionally use exclude
277 Detect renderer markdown/rst from filename and optionally use exclude
276 list to remove some options. This is mostly used in helpers.
278 list to remove some options. This is mostly used in helpers.
277 Returns None when no renderer can be detected.
279 Returns None when no renderer can be detected.
278 """
280 """
279 def _filter(elements):
281 def _filter(elements):
280 if isinstance(exclude, (list, tuple)):
282 if isinstance(exclude, (list, tuple)):
281 return [x for x in elements if x not in exclude]
283 return [x for x in elements if x not in exclude]
282 return elements
284 return elements
283
285
284 if filename.endswith(
286 if filename.endswith(
285 tuple(_filter([x[0] for x in cls.MARKDOWN_EXTS if x[0]]))):
287 tuple(_filter([x[0] for x in cls.MARKDOWN_EXTS if x[0]]))):
286 return 'markdown'
288 return 'markdown'
287 if filename.endswith(tuple(_filter([x[0] for x in cls.RST_EXTS if x[0]]))):
289 if filename.endswith(tuple(_filter([x[0] for x in cls.RST_EXTS if x[0]]))):
288 return 'rst'
290 return 'rst'
289
291
290 return None
292 return None
291
293
292 def render(self, source, filename=None):
294 def render(self, source, filename=None):
293 """
295 """
294 Renders a given filename using detected renderer
296 Renders a given filename using detected renderer
295 it detects renderers based on file extension or mimetype.
297 it detects renderers based on file extension or mimetype.
296 At last it will just do a simple html replacing new lines with <br/>
298 At last it will just do a simple html replacing new lines with <br/>
297
299
298 :param file_name:
300 :param file_name:
299 :param source:
301 :param source:
300 """
302 """
301
303
302 renderer = self._detect_renderer(source, filename)
304 renderer = self._detect_renderer(source, filename)
303 readme_data = renderer(source)
305 readme_data = renderer(source)
304 return readme_data
306 return readme_data
305
307
306 @classmethod
308 @classmethod
307 def _flavored_markdown(cls, text):
309 def _flavored_markdown(cls, text):
308 """
310 """
309 Github style flavored markdown
311 Github style flavored markdown
310
312
311 :param text:
313 :param text:
312 """
314 """
313
315
314 # Extract pre blocks.
316 # Extract pre blocks.
315 extractions = {}
317 extractions = {}
316
318
317 def pre_extraction_callback(matchobj):
319 def pre_extraction_callback(matchobj):
318 digest = md5_safe(matchobj.group(0))
320 digest = md5_safe(matchobj.group(0))
319 extractions[digest] = matchobj.group(0)
321 extractions[digest] = matchobj.group(0)
320 return "{gfm-extraction-%s}" % digest
322 return "{gfm-extraction-%s}" % digest
321 pattern = re.compile(r'<pre>.*?</pre>', re.MULTILINE | re.DOTALL)
323 pattern = re.compile(r'<pre>.*?</pre>', re.MULTILINE | re.DOTALL)
322 text = re.sub(pattern, pre_extraction_callback, text)
324 text = re.sub(pattern, pre_extraction_callback, text)
323
325
324 # Prevent foo_bar_baz from ending up with an italic word in the middle.
326 # Prevent foo_bar_baz from ending up with an italic word in the middle.
325 def italic_callback(matchobj):
327 def italic_callback(matchobj):
326 s = matchobj.group(0)
328 s = matchobj.group(0)
327 if list(s).count('_') >= 2:
329 if list(s).count('_') >= 2:
328 return s.replace('_', r'\_')
330 return s.replace('_', r'\_')
329 return s
331 return s
330 text = re.sub(r'^(?! {4}|\t)\w+_\w+_\w[\w_]*', italic_callback, text)
332 text = re.sub(r'^(?! {4}|\t)\w+_\w+_\w[\w_]*', italic_callback, text)
331
333
332 # Insert pre block extractions.
334 # Insert pre block extractions.
333 def pre_insert_callback(matchobj):
335 def pre_insert_callback(matchobj):
334 return '\n\n' + extractions[matchobj.group(1)]
336 return '\n\n' + extractions[matchobj.group(1)]
335 text = re.sub(r'\{gfm-extraction-([0-9a-f]{32})\}',
337 text = re.sub(r'\{gfm-extraction-([0-9a-f]{32})\}',
336 pre_insert_callback, text)
338 pre_insert_callback, text)
337
339
338 return text
340 return text
339
341
340 @classmethod
342 @classmethod
341 def urlify_text(cls, text):
343 def urlify_text(cls, text):
342 def url_func(match_obj):
344 def url_func(match_obj):
343 url_full = match_obj.groups()[0]
345 url_full = match_obj.groups()[0]
344 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
346 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
345
347
346 return cls.URL_PAT.sub(url_func, text)
348 return cls.URL_PAT.sub(url_func, text)
347
349
348 @classmethod
350 @classmethod
349 def plain(cls, source, universal_newline=True, leading_newline=True):
351 def plain(cls, source, universal_newline=True, leading_newline=True):
350 source = safe_unicode(source)
352 source = safe_unicode(source)
351 if universal_newline:
353 if universal_newline:
352 newline = '\n'
354 newline = '\n'
353 source = newline.join(source.splitlines())
355 source = newline.join(source.splitlines())
354
356
355 rendered_source = cls.urlify_text(source)
357 rendered_source = cls.urlify_text(source)
356 source = ''
358 source = ''
357 if leading_newline:
359 if leading_newline:
358 source += '<br />'
360 source += '<br />'
359 source += rendered_source.replace("\n", '<br />')
361 source += rendered_source.replace("\n", '<br />')
360
362
361 rendered = cls.bleach_clean(source)
363 rendered = cls.bleach_clean(source)
362 return rendered
364 return rendered
363
365
364 @classmethod
366 @classmethod
365 def markdown(cls, source, safe=True, flavored=True, mentions=False,
367 def markdown(cls, source, safe=True, flavored=True, mentions=False,
366 clean_html=True):
368 clean_html=True):
367 """
369 """
368 returns markdown rendered code cleaned by the bleach library
370 returns markdown rendered code cleaned by the bleach library
369 """
371 """
370
372
371 if flavored:
373 if flavored:
372 markdown_renderer = get_markdown_renderer_flavored(
374 markdown_renderer = get_markdown_renderer_flavored(
373 cls.extensions, cls.output_format)
375 cls.extensions, cls.output_format)
374 else:
376 else:
375 markdown_renderer = get_markdown_renderer(
377 markdown_renderer = get_markdown_renderer(
376 cls.extensions, cls.output_format)
378 cls.extensions, cls.output_format)
377
379
378 if mentions:
380 if mentions:
379 mention_pat = re.compile(MENTIONS_REGEX)
381 mention_pat = re.compile(MENTIONS_REGEX)
380
382
381 def wrapp(match_obj):
383 def wrapp(match_obj):
382 uname = match_obj.groups()[0]
384 uname = match_obj.groups()[0]
383 return ' **@%(uname)s** ' % {'uname': uname}
385 return ' **@%(uname)s** ' % {'uname': uname}
384 mention_hl = mention_pat.sub(wrapp, source).strip()
386 mention_hl = mention_pat.sub(wrapp, source).strip()
385 # we extracted mentions render with this using Mentions false
387 # we extracted mentions render with this using Mentions false
386 return cls.markdown(mention_hl, safe=safe, flavored=flavored,
388 return cls.markdown(mention_hl, safe=safe, flavored=flavored,
387 mentions=False)
389 mentions=False)
388
390
389 source = safe_unicode(source)
391 source = safe_unicode(source)
390
392
391 try:
393 try:
392 if flavored:
394 if flavored:
393 source = cls._flavored_markdown(source)
395 source = cls._flavored_markdown(source)
394 rendered = markdown_renderer.convert(source)
396 rendered = markdown_renderer.convert(source)
395 except Exception:
397 except Exception:
396 log.exception('Error when rendering Markdown')
398 log.exception('Error when rendering Markdown')
397 if safe:
399 if safe:
398 log.debug('Fallback to render in plain mode')
400 log.debug('Fallback to render in plain mode')
399 rendered = cls.plain(source)
401 rendered = cls.plain(source)
400 else:
402 else:
401 raise
403 raise
402
404
403 if clean_html:
405 if clean_html:
404 rendered = cls.bleach_clean(rendered)
406 rendered = cls.bleach_clean(rendered)
405 return rendered
407 return rendered
406
408
407 @classmethod
409 @classmethod
408 def rst(cls, source, safe=True, mentions=False, clean_html=False):
410 def rst(cls, source, safe=True, mentions=False, clean_html=False):
409 if mentions:
411 if mentions:
410 mention_pat = re.compile(MENTIONS_REGEX)
412 mention_pat = re.compile(MENTIONS_REGEX)
411
413
412 def wrapp(match_obj):
414 def wrapp(match_obj):
413 uname = match_obj.groups()[0]
415 uname = match_obj.groups()[0]
414 return ' **@%(uname)s** ' % {'uname': uname}
416 return ' **@%(uname)s** ' % {'uname': uname}
415 mention_hl = mention_pat.sub(wrapp, source).strip()
417 mention_hl = mention_pat.sub(wrapp, source).strip()
416 # we extracted mentions render with this using Mentions false
418 # we extracted mentions render with this using Mentions false
417 return cls.rst(mention_hl, safe=safe, mentions=False)
419 return cls.rst(mention_hl, safe=safe, mentions=False)
418
420
419 source = safe_unicode(source)
421 source = safe_unicode(source)
420 try:
422 try:
421 docutils_settings = dict(
423 docutils_settings = dict(
422 [(alias, None) for alias in
424 [(alias, None) for alias in
423 cls.RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES])
425 cls.RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES])
424
426
425 docutils_settings.update({
427 docutils_settings.update({
426 'input_encoding': 'unicode', 'report_level': 4})
428 'input_encoding': 'unicode',
429 'report_level': 4,
430 'syntax_highlight': 'short',
431 })
427
432
428 for k, v in docutils_settings.iteritems():
433 for k, v in docutils_settings.iteritems():
429 directives.register_directive(k, v)
434 directives.register_directive(k, v)
430
435
431 parts = publish_parts(source=source,
436 parts = publish_parts(source=source,
432 writer=RhodeCodeWriter(),
437 writer=RhodeCodeWriter(),
433 settings_overrides=docutils_settings)
438 settings_overrides=docutils_settings)
434 rendered = parts["fragment"]
439 rendered = parts["fragment"]
435 if clean_html:
440 if clean_html:
436 rendered = cls.bleach_clean(rendered)
441 rendered = cls.bleach_clean(rendered)
437 return parts['html_title'] + rendered
442 return parts['html_title'] + rendered
438 except Exception:
443 except Exception:
439 log.exception('Error when rendering RST')
444 log.exception('Error when rendering RST')
440 if safe:
445 if safe:
441 log.debug('Fallbacking to render in plain mode')
446 log.debug('Fallbacking to render in plain mode')
442 return cls.plain(source)
447 return cls.plain(source)
443 else:
448 else:
444 raise
449 raise
445
450
446 @classmethod
451 @classmethod
447 def jupyter(cls, source, safe=True):
452 def jupyter(cls, source, safe=True):
448 from rhodecode.lib import helpers
453 from rhodecode.lib import helpers
449
454
450 from traitlets.config import Config
455 from traitlets.config import Config
451 import nbformat
456 import nbformat
452 from nbconvert import HTMLExporter
457 from nbconvert import HTMLExporter
453 from nbconvert.preprocessors import Preprocessor
458 from nbconvert.preprocessors import Preprocessor
454
459
455 class CustomHTMLExporter(HTMLExporter):
460 class CustomHTMLExporter(HTMLExporter):
456 def _template_file_default(self):
461 def _template_file_default(self):
457 return 'basic'
462 return 'basic'
458
463
459 class Sandbox(Preprocessor):
464 class Sandbox(Preprocessor):
460
465
461 def preprocess(self, nb, resources):
466 def preprocess(self, nb, resources):
462 sandbox_text = 'SandBoxed(IPython.core.display.Javascript object)'
467 sandbox_text = 'SandBoxed(IPython.core.display.Javascript object)'
463 for cell in nb['cells']:
468 for cell in nb['cells']:
464 if not safe:
469 if not safe:
465 continue
470 continue
466
471
467 if 'outputs' in cell:
472 if 'outputs' in cell:
468 for cell_output in cell['outputs']:
473 for cell_output in cell['outputs']:
469 if 'data' in cell_output:
474 if 'data' in cell_output:
470 if 'application/javascript' in cell_output['data']:
475 if 'application/javascript' in cell_output['data']:
471 cell_output['data']['text/plain'] = sandbox_text
476 cell_output['data']['text/plain'] = sandbox_text
472 cell_output['data'].pop('application/javascript', None)
477 cell_output['data'].pop('application/javascript', None)
473
478
474 if 'source' in cell and cell['cell_type'] == 'markdown':
479 if 'source' in cell and cell['cell_type'] == 'markdown':
475 # sanitize similar like in markdown
480 # sanitize similar like in markdown
476 cell['source'] = cls.bleach_clean(cell['source'])
481 cell['source'] = cls.bleach_clean(cell['source'])
477
482
478 return nb, resources
483 return nb, resources
479
484
480 def _sanitize_resources(input_resources):
485 def _sanitize_resources(input_resources):
481 """
486 """
482 Skip/sanitize some of the CSS generated and included in jupyter
487 Skip/sanitize some of the CSS generated and included in jupyter
483 so it doesn't messes up UI so much
488 so it doesn't messes up UI so much
484 """
489 """
485
490
486 # TODO(marcink): probably we should replace this with whole custom
491 # TODO(marcink): probably we should replace this with whole custom
487 # CSS set that doesn't screw up, but jupyter generated html has some
492 # CSS set that doesn't screw up, but jupyter generated html has some
488 # special markers, so it requires Custom HTML exporter template with
493 # special markers, so it requires Custom HTML exporter template with
489 # _default_template_path_default, to achieve that
494 # _default_template_path_default, to achieve that
490
495
491 # strip the reset CSS
496 # strip the reset CSS
492 input_resources[0] = input_resources[0][input_resources[0].find('/*! Source'):]
497 input_resources[0] = input_resources[0][input_resources[0].find('/*! Source'):]
493 return input_resources
498 return input_resources
494
499
495 def as_html(notebook):
500 def as_html(notebook):
496 conf = Config()
501 conf = Config()
497 conf.CustomHTMLExporter.preprocessors = [Sandbox]
502 conf.CustomHTMLExporter.preprocessors = [Sandbox]
498 html_exporter = CustomHTMLExporter(config=conf)
503 html_exporter = CustomHTMLExporter(config=conf)
499
504
500 (body, resources) = html_exporter.from_notebook_node(notebook)
505 (body, resources) = html_exporter.from_notebook_node(notebook)
501 header = '<!-- ## IPYTHON NOTEBOOK RENDERING ## -->'
506 header = '<!-- ## IPYTHON NOTEBOOK RENDERING ## -->'
502 js = MakoTemplate(r'''
507 js = MakoTemplate(r'''
503 <!-- MathJax configuration -->
508 <!-- MathJax configuration -->
504 <script type="text/x-mathjax-config">
509 <script type="text/x-mathjax-config">
505 MathJax.Hub.Config({
510 MathJax.Hub.Config({
506 jax: ["input/TeX","output/HTML-CSS", "output/PreviewHTML"],
511 jax: ["input/TeX","output/HTML-CSS", "output/PreviewHTML"],
507 extensions: ["tex2jax.js","MathMenu.js","MathZoom.js", "fast-preview.js", "AssistiveMML.js", "[Contrib]/a11y/accessibility-menu.js"],
512 extensions: ["tex2jax.js","MathMenu.js","MathZoom.js", "fast-preview.js", "AssistiveMML.js", "[Contrib]/a11y/accessibility-menu.js"],
508 TeX: {
513 TeX: {
509 extensions: ["AMSmath.js","AMSsymbols.js","noErrors.js","noUndefined.js"]
514 extensions: ["AMSmath.js","AMSsymbols.js","noErrors.js","noUndefined.js"]
510 },
515 },
511 tex2jax: {
516 tex2jax: {
512 inlineMath: [ ['$','$'], ["\\(","\\)"] ],
517 inlineMath: [ ['$','$'], ["\\(","\\)"] ],
513 displayMath: [ ['$$','$$'], ["\\[","\\]"] ],
518 displayMath: [ ['$$','$$'], ["\\[","\\]"] ],
514 processEscapes: true,
519 processEscapes: true,
515 processEnvironments: true
520 processEnvironments: true
516 },
521 },
517 // Center justify equations in code and markdown cells. Elsewhere
522 // Center justify equations in code and markdown cells. Elsewhere
518 // we use CSS to left justify single line equations in code cells.
523 // we use CSS to left justify single line equations in code cells.
519 displayAlign: 'center',
524 displayAlign: 'center',
520 "HTML-CSS": {
525 "HTML-CSS": {
521 styles: {'.MathJax_Display': {"margin": 0}},
526 styles: {'.MathJax_Display': {"margin": 0}},
522 linebreaks: { automatic: true },
527 linebreaks: { automatic: true },
523 availableFonts: ["STIX", "TeX"]
528 availableFonts: ["STIX", "TeX"]
524 },
529 },
525 showMathMenu: false
530 showMathMenu: false
526 });
531 });
527 </script>
532 </script>
528 <!-- End of MathJax configuration -->
533 <!-- End of MathJax configuration -->
529 <script src="${h.asset('js/src/math_jax/MathJax.js')}"></script>
534 <script src="${h.asset('js/src/math_jax/MathJax.js')}"></script>
530 ''').render(h=helpers)
535 ''').render(h=helpers)
531
536
532 css = MakoTemplate(r'''
537 css = MakoTemplate(r'''
533 <link rel="stylesheet" type="text/css" href="${h.asset('css/style-ipython.css', ver=ver)}" media="screen"/>
538 <link rel="stylesheet" type="text/css" href="${h.asset('css/style-ipython.css', ver=ver)}" media="screen"/>
534 ''').render(h=helpers, ver='ver1')
539 ''').render(h=helpers, ver='ver1')
535
540
536 body = '\n'.join([header, css, js, body])
541 body = '\n'.join([header, css, js, body])
537 return body, resources
542 return body, resources
538
543
539 notebook = nbformat.reads(source, as_version=4)
544 notebook = nbformat.reads(source, as_version=4)
540 (body, resources) = as_html(notebook)
545 (body, resources) = as_html(notebook)
541 return body
546 return body
542
547
543
548
544 class RstTemplateRenderer(object):
549 class RstTemplateRenderer(object):
545
550
546 def __init__(self):
551 def __init__(self):
547 base = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
552 base = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
548 rst_template_dirs = [os.path.join(base, 'templates', 'rst_templates')]
553 rst_template_dirs = [os.path.join(base, 'templates', 'rst_templates')]
549 self.template_store = TemplateLookup(
554 self.template_store = TemplateLookup(
550 directories=rst_template_dirs,
555 directories=rst_template_dirs,
551 input_encoding='utf-8',
556 input_encoding='utf-8',
552 imports=['from rhodecode.lib import helpers as h'])
557 imports=['from rhodecode.lib import helpers as h'])
553
558
554 def _get_template(self, templatename):
559 def _get_template(self, templatename):
555 return self.template_store.get_template(templatename)
560 return self.template_store.get_template(templatename)
556
561
557 def render(self, template_name, **kwargs):
562 def render(self, template_name, **kwargs):
558 template = self._get_template(template_name)
563 template = self._get_template(template_name)
559 return template.render(**kwargs)
564 return template.render(**kwargs)
General Comments 0
You need to be logged in to leave comments. Login now