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