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