##// END OF EJS Templates
fix(jupyter): fixing rendering of legacy jupyter notebooks. Fixes|References: RCCE-10
ilin.s -
r5249:3bbf2d7d default
parent child Browse files
Show More
@@ -1,541 +1,543 b''
1
1
2
2
3 # Copyright (C) 2011-2023 RhodeCode GmbH
3 # Copyright (C) 2011-2023 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 urllib.parse
30 import urllib.parse
31 import pycmarkgfm
31 import pycmarkgfm
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.utils2 import safe_str, MENTIONS_REGEX
42 from rhodecode.lib.utils2 import safe_str, MENTIONS_REGEX
43
43
44 log = logging.getLogger(__name__)
44 log = logging.getLogger(__name__)
45
45
46 # default renderer used to generate automated comments
46 # default renderer used to generate automated comments
47 DEFAULT_COMMENTS_RENDERER = 'rst'
47 DEFAULT_COMMENTS_RENDERER = 'rst'
48
48
49 try:
49 try:
50 from lxml.html import fromstring
50 from lxml.html import fromstring
51 from lxml.html import tostring
51 from lxml.html import tostring
52 except ImportError:
52 except ImportError:
53 log.exception('Failed to import lxml')
53 log.exception('Failed to import lxml')
54 fromstring = None
54 fromstring = None
55 tostring = None
55 tostring = None
56
56
57
57
58 class CustomHTMLTranslator(writers.html4css1.HTMLTranslator):
58 class CustomHTMLTranslator(writers.html4css1.HTMLTranslator):
59 """
59 """
60 Custom HTML Translator used for sandboxing potential
60 Custom HTML Translator used for sandboxing potential
61 JS injections in ref links
61 JS injections in ref links
62 """
62 """
63 def visit_literal_block(self, node):
63 def visit_literal_block(self, node):
64 self.body.append(self.starttag(node, 'pre', CLASS='codehilite literal-block'))
64 self.body.append(self.starttag(node, 'pre', CLASS='codehilite literal-block'))
65
65
66 def visit_reference(self, node):
66 def visit_reference(self, node):
67 if 'refuri' in node.attributes:
67 if 'refuri' in node.attributes:
68 refuri = node['refuri']
68 refuri = node['refuri']
69 if ':' in refuri:
69 if ':' in refuri:
70 prefix, link = refuri.lstrip().split(':', 1)
70 prefix, link = refuri.lstrip().split(':', 1)
71 prefix = prefix or ''
71 prefix = prefix or ''
72
72
73 if prefix.lower() == 'javascript':
73 if prefix.lower() == 'javascript':
74 # we don't allow javascript type of refs...
74 # we don't allow javascript type of refs...
75 node['refuri'] = 'javascript:alert("SandBoxedJavascript")'
75 node['refuri'] = 'javascript:alert("SandBoxedJavascript")'
76
76
77 # old style class requires this...
77 # old style class requires this...
78 return html4css1.HTMLTranslator.visit_reference(self, node)
78 return html4css1.HTMLTranslator.visit_reference(self, node)
79
79
80
80
81 class RhodeCodeWriter(writers.html4css1.Writer):
81 class RhodeCodeWriter(writers.html4css1.Writer):
82 def __init__(self):
82 def __init__(self):
83 super(RhodeCodeWriter, self).__init__()
83 super(RhodeCodeWriter, self).__init__()
84 self.translator_class = CustomHTMLTranslator
84 self.translator_class = CustomHTMLTranslator
85
85
86
86
87 def relative_links(html_source, server_paths):
87 def relative_links(html_source, server_paths):
88 if not html_source:
88 if not html_source:
89 return html_source
89 return html_source
90
90
91 if not fromstring and tostring:
91 if not fromstring and tostring:
92 return html_source
92 return html_source
93
93
94 try:
94 try:
95 doc = lxml.html.fromstring(html_source)
95 doc = lxml.html.fromstring(html_source)
96 except Exception:
96 except Exception:
97 return html_source
97 return html_source
98
98
99 for el in doc.cssselect('img, video'):
99 for el in doc.cssselect('img, video'):
100 src = el.attrib.get('src')
100 src = el.attrib.get('src')
101 if src:
101 if src:
102 el.attrib['src'] = relative_path(src, server_paths['raw'])
102 el.attrib['src'] = relative_path(src, server_paths['raw'])
103
103
104 for el in doc.cssselect('a:not(.gfm)'):
104 for el in doc.cssselect('a:not(.gfm)'):
105 src = el.attrib.get('href')
105 src = el.attrib.get('href')
106 if src:
106 if src:
107 raw_mode = el.attrib['href'].endswith('?raw=1')
107 raw_mode = el.attrib['href'].endswith('?raw=1')
108 if raw_mode:
108 if raw_mode:
109 el.attrib['href'] = relative_path(src, server_paths['raw'])
109 el.attrib['href'] = relative_path(src, server_paths['raw'])
110 else:
110 else:
111 el.attrib['href'] = relative_path(src, server_paths['standard'])
111 el.attrib['href'] = relative_path(src, server_paths['standard'])
112
112
113 return lxml.html.tostring(doc, encoding='unicode')
113 return lxml.html.tostring(doc, encoding='unicode')
114
114
115
115
116 def relative_path(path, request_path, is_repo_file=None):
116 def relative_path(path, request_path, is_repo_file=None):
117 """
117 """
118 relative link support, path is a rel path, and request_path is current
118 relative link support, path is a rel path, and request_path is current
119 server path (not absolute)
119 server path (not absolute)
120
120
121 e.g.
121 e.g.
122
122
123 path = '../logo.png'
123 path = '../logo.png'
124 request_path= '/repo/files/path/file.md'
124 request_path= '/repo/files/path/file.md'
125 produces: '/repo/files/logo.png'
125 produces: '/repo/files/logo.png'
126 """
126 """
127 # TODO(marcink): unicode/str support ?
127 # TODO(marcink): unicode/str support ?
128 # maybe=> safe_str(urllib.quote(safe_str(final_path), '/:'))
128 # maybe=> safe_str(urllib.quote(safe_str(final_path), '/:'))
129
129
130 def dummy_check(p):
130 def dummy_check(p):
131 return True # assume default is a valid file path
131 return True # assume default is a valid file path
132
132
133 is_repo_file = is_repo_file or dummy_check
133 is_repo_file = is_repo_file or dummy_check
134 if not path:
134 if not path:
135 return request_path
135 return request_path
136
136
137 path = safe_str(path)
137 path = safe_str(path)
138 request_path = safe_str(request_path)
138 request_path = safe_str(request_path)
139
139
140 if path.startswith(('data:', 'javascript:', '#', ':')):
140 if path.startswith(('data:', 'javascript:', '#', ':')):
141 # skip data, anchor, invalid links
141 # skip data, anchor, invalid links
142 return path
142 return path
143
143
144 is_absolute = bool(urllib.parse.urlparse(path).netloc)
144 is_absolute = bool(urllib.parse.urlparse(path).netloc)
145 if is_absolute:
145 if is_absolute:
146 return path
146 return path
147
147
148 if not request_path:
148 if not request_path:
149 return path
149 return path
150
150
151 if path.startswith('/'):
151 if path.startswith('/'):
152 path = path[1:]
152 path = path[1:]
153
153
154 if path.startswith('./'):
154 if path.startswith('./'):
155 path = path[2:]
155 path = path[2:]
156
156
157 parts = request_path.split('/')
157 parts = request_path.split('/')
158 # compute how deep we need to traverse the request_path
158 # compute how deep we need to traverse the request_path
159 depth = 0
159 depth = 0
160
160
161 if is_repo_file(request_path):
161 if is_repo_file(request_path):
162 # if request path is a VALID file, we use a relative path with
162 # if request path is a VALID file, we use a relative path with
163 # one level up
163 # one level up
164 depth += 1
164 depth += 1
165
165
166 while path.startswith('../'):
166 while path.startswith('../'):
167 depth += 1
167 depth += 1
168 path = path[3:]
168 path = path[3:]
169
169
170 if depth > 0:
170 if depth > 0:
171 parts = parts[:-depth]
171 parts = parts[:-depth]
172
172
173 parts.append(path)
173 parts.append(path)
174 final_path = '/'.join(parts).lstrip('/')
174 final_path = '/'.join(parts).lstrip('/')
175
175
176 return '/' + final_path
176 return '/' + final_path
177
177
178
178
179 _cached_markdown_renderer = None
179 _cached_markdown_renderer = None
180
180
181
181
182 def get_markdown_renderer(extensions, output_format):
182 def get_markdown_renderer(extensions, output_format):
183 global _cached_markdown_renderer
183 global _cached_markdown_renderer
184
184
185 if _cached_markdown_renderer is None:
185 if _cached_markdown_renderer is None:
186 _cached_markdown_renderer = markdown.Markdown(
186 _cached_markdown_renderer = markdown.Markdown(
187 extensions=extensions + ['legacy_attrs'],
187 extensions=extensions + ['legacy_attrs'],
188 output_format=output_format)
188 output_format=output_format)
189 return _cached_markdown_renderer
189 return _cached_markdown_renderer
190
190
191
191
192 def get_markdown_renderer_flavored(extensions, output_format):
192 def get_markdown_renderer_flavored(extensions, output_format):
193 """
193 """
194 Dummy wrapper to mimic markdown API and render github HTML rendered
194 Dummy wrapper to mimic markdown API and render github HTML rendered
195
195
196 """
196 """
197 md = get_markdown_renderer(extensions, output_format)
197 md = get_markdown_renderer(extensions, output_format)
198
198
199 class GFM(object):
199 class GFM(object):
200 def convert(self, source):
200 def convert(self, source):
201 with pycmarkgfm.parse_gfm(source, options=pycmarkgfm.options.hardbreaks) as document:
201 with pycmarkgfm.parse_gfm(source, options=pycmarkgfm.options.hardbreaks) as document:
202 parsed_md = document.to_commonmark()
202 parsed_md = document.to_commonmark()
203 return md.convert(parsed_md)
203 return md.convert(parsed_md)
204
204
205 return GFM()
205 return GFM()
206
206
207
207
208 class MarkupRenderer(object):
208 class MarkupRenderer(object):
209 RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES = ['include', 'meta', 'raw']
209 RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES = ['include', 'meta', 'raw']
210
210
211 MARKDOWN_PAT = re.compile(r'\.(md|mkdn?|mdown|markdown)$', re.IGNORECASE)
211 MARKDOWN_PAT = re.compile(r'\.(md|mkdn?|mdown|markdown)$', re.IGNORECASE)
212 RST_PAT = re.compile(r'\.re?st$', re.IGNORECASE)
212 RST_PAT = re.compile(r'\.re?st$', re.IGNORECASE)
213 JUPYTER_PAT = re.compile(r'\.(ipynb)$', re.IGNORECASE)
213 JUPYTER_PAT = re.compile(r'\.(ipynb)$', re.IGNORECASE)
214 PLAIN_PAT = re.compile(r'^readme$', re.IGNORECASE)
214 PLAIN_PAT = re.compile(r'^readme$', re.IGNORECASE)
215
215
216 URL_PAT = re.compile(r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'
216 URL_PAT = re.compile(r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'
217 r'|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)')
217 r'|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)')
218
218
219 MENTION_PAT = re.compile(MENTIONS_REGEX)
219 MENTION_PAT = re.compile(MENTIONS_REGEX)
220
220
221 extensions = ['markdown.extensions.codehilite', 'markdown.extensions.extra',
221 extensions = ['markdown.extensions.codehilite', 'markdown.extensions.extra',
222 'markdown.extensions.def_list', 'markdown.extensions.sane_lists']
222 'markdown.extensions.def_list', 'markdown.extensions.sane_lists']
223
223
224 output_format = 'html4'
224 output_format = 'html4'
225
225
226 # extension together with weights. Lower is first means we control how
226 # extension together with weights. Lower is first means we control how
227 # extensions are attached to readme names with those.
227 # extensions are attached to readme names with those.
228 PLAIN_EXTS = [
228 PLAIN_EXTS = [
229 # prefer no extension
229 # prefer no extension
230 ('', 0), # special case that renders READMES names without extension
230 ('', 0), # special case that renders READMES names without extension
231 ('.text', 2), ('.TEXT', 2),
231 ('.text', 2), ('.TEXT', 2),
232 ('.txt', 3), ('.TXT', 3)
232 ('.txt', 3), ('.TXT', 3)
233 ]
233 ]
234
234
235 RST_EXTS = [
235 RST_EXTS = [
236 ('.rst', 1), ('.rest', 1),
236 ('.rst', 1), ('.rest', 1),
237 ('.RST', 2), ('.REST', 2)
237 ('.RST', 2), ('.REST', 2)
238 ]
238 ]
239
239
240 MARKDOWN_EXTS = [
240 MARKDOWN_EXTS = [
241 ('.md', 1), ('.MD', 1),
241 ('.md', 1), ('.MD', 1),
242 ('.mkdn', 2), ('.MKDN', 2),
242 ('.mkdn', 2), ('.MKDN', 2),
243 ('.mdown', 3), ('.MDOWN', 3),
243 ('.mdown', 3), ('.MDOWN', 3),
244 ('.markdown', 4), ('.MARKDOWN', 4)
244 ('.markdown', 4), ('.MARKDOWN', 4)
245 ]
245 ]
246
246
247 def _detect_renderer(self, source, filename=None):
247 def _detect_renderer(self, source, filename=None):
248 """
248 """
249 runs detection of what renderer should be used for generating html
249 runs detection of what renderer should be used for generating html
250 from a markup language
250 from a markup language
251
251
252 filename can be also explicitly a renderer name
252 filename can be also explicitly a renderer name
253
253
254 :param source:
254 :param source:
255 :param filename:
255 :param filename:
256 """
256 """
257
257
258 if MarkupRenderer.MARKDOWN_PAT.findall(filename):
258 if MarkupRenderer.MARKDOWN_PAT.findall(filename):
259 detected_renderer = 'markdown'
259 detected_renderer = 'markdown'
260 elif MarkupRenderer.RST_PAT.findall(filename):
260 elif MarkupRenderer.RST_PAT.findall(filename):
261 detected_renderer = 'rst'
261 detected_renderer = 'rst'
262 elif MarkupRenderer.JUPYTER_PAT.findall(filename):
262 elif MarkupRenderer.JUPYTER_PAT.findall(filename):
263 detected_renderer = 'jupyter'
263 detected_renderer = 'jupyter'
264 elif MarkupRenderer.PLAIN_PAT.findall(filename):
264 elif MarkupRenderer.PLAIN_PAT.findall(filename):
265 detected_renderer = 'plain'
265 detected_renderer = 'plain'
266 else:
266 else:
267 detected_renderer = 'plain'
267 detected_renderer = 'plain'
268
268
269 return getattr(MarkupRenderer, detected_renderer)
269 return getattr(MarkupRenderer, detected_renderer)
270
270
271 @classmethod
271 @classmethod
272 def sanitize_html(cls, text):
272 def sanitize_html(cls, text):
273 from .html_filters import sanitize_html
273 from .html_filters import sanitize_html
274 return sanitize_html(text, markdown=True)
274 return sanitize_html(text, markdown=True)
275
275
276 @classmethod
276 @classmethod
277 def renderer_from_filename(cls, filename, exclude):
277 def renderer_from_filename(cls, filename, exclude):
278 """
278 """
279 Detect renderer markdown/rst from filename and optionally use exclude
279 Detect renderer markdown/rst from filename and optionally use exclude
280 list to remove some options. This is mostly used in helpers.
280 list to remove some options. This is mostly used in helpers.
281 Returns None when no renderer can be detected.
281 Returns None when no renderer can be detected.
282 """
282 """
283 def _filter(elements):
283 def _filter(elements):
284 if isinstance(exclude, (list, tuple)):
284 if isinstance(exclude, (list, tuple)):
285 return [x for x in elements if x not in exclude]
285 return [x for x in elements if x not in exclude]
286 return elements
286 return elements
287
287
288 if filename.endswith(
288 if filename.endswith(
289 tuple(_filter([x[0] for x in cls.MARKDOWN_EXTS if x[0]]))):
289 tuple(_filter([x[0] for x in cls.MARKDOWN_EXTS if x[0]]))):
290 return 'markdown'
290 return 'markdown'
291 if filename.endswith(tuple(_filter([x[0] for x in cls.RST_EXTS if x[0]]))):
291 if filename.endswith(tuple(_filter([x[0] for x in cls.RST_EXTS if x[0]]))):
292 return 'rst'
292 return 'rst'
293
293
294 return None
294 return None
295
295
296 def render(self, source, filename=None):
296 def render(self, source, filename=None):
297 """
297 """
298 Renders a given filename using detected renderer
298 Renders a given filename using detected renderer
299 it detects renderers based on file extension or mimetype.
299 it detects renderers based on file extension or mimetype.
300 At last it will just do a simple html replacing new lines with <br/>
300 At last it will just do a simple html replacing new lines with <br/>
301 """
301 """
302
302
303 renderer = self._detect_renderer(source, filename)
303 renderer = self._detect_renderer(source, filename)
304 readme_data = renderer(source)
304 readme_data = renderer(source)
305 return readme_data
305 return readme_data
306
306
307 @classmethod
307 @classmethod
308 def urlify_text(cls, text):
308 def urlify_text(cls, text):
309 def url_func(match_obj):
309 def url_func(match_obj):
310 url_full = match_obj.groups()[0]
310 url_full = match_obj.groups()[0]
311 return f'<a href="{url_full}">{url_full}</a>'
311 return f'<a href="{url_full}">{url_full}</a>'
312
312
313 return cls.URL_PAT.sub(url_func, text)
313 return cls.URL_PAT.sub(url_func, text)
314
314
315 @classmethod
315 @classmethod
316 def convert_mentions(cls, text, mode):
316 def convert_mentions(cls, text, mode):
317 mention_pat = cls.MENTION_PAT
317 mention_pat = cls.MENTION_PAT
318
318
319 def wrapp(match_obj):
319 def wrapp(match_obj):
320 uname = match_obj.groups()[0]
320 uname = match_obj.groups()[0]
321 hovercard_url = "pyroutes.url('hovercard_username', {'username': '%s'});" % uname
321 hovercard_url = "pyroutes.url('hovercard_username', {'username': '%s'});" % uname
322
322
323 if mode == 'markdown':
323 if mode == 'markdown':
324 tmpl = '<strong class="tooltip-hovercard" data-hovercard-alt="{uname}" data-hovercard-url="{hovercard_url}">@{uname}</strong>'
324 tmpl = '<strong class="tooltip-hovercard" data-hovercard-alt="{uname}" data-hovercard-url="{hovercard_url}">@{uname}</strong>'
325 elif mode == 'rst':
325 elif mode == 'rst':
326 tmpl = ' **@{uname}** '
326 tmpl = ' **@{uname}** '
327 else:
327 else:
328 raise ValueError('mode must be rst or markdown')
328 raise ValueError('mode must be rst or markdown')
329
329
330 return tmpl.format(**{'uname': uname,
330 return tmpl.format(**{'uname': uname,
331 'hovercard_url': hovercard_url})
331 'hovercard_url': hovercard_url})
332
332
333 return mention_pat.sub(wrapp, text).strip()
333 return mention_pat.sub(wrapp, text).strip()
334
334
335 @classmethod
335 @classmethod
336 def plain(cls, source, universal_newline=True, leading_newline=True):
336 def plain(cls, source, universal_newline=True, leading_newline=True):
337 source = safe_str(source)
337 source = safe_str(source)
338 if universal_newline:
338 if universal_newline:
339 newline = '\n'
339 newline = '\n'
340 source = newline.join(source.splitlines())
340 source = newline.join(source.splitlines())
341
341
342 rendered_source = cls.urlify_text(source)
342 rendered_source = cls.urlify_text(source)
343 source = ''
343 source = ''
344 if leading_newline:
344 if leading_newline:
345 source += '<br />'
345 source += '<br />'
346 source += rendered_source.replace("\n", '<br />')
346 source += rendered_source.replace("\n", '<br />')
347
347
348 rendered = cls.sanitize_html(source)
348 rendered = cls.sanitize_html(source)
349 return rendered
349 return rendered
350
350
351 @classmethod
351 @classmethod
352 def markdown(cls, source, safe=True, flavored=True, mentions=False,
352 def markdown(cls, source, safe=True, flavored=True, mentions=False,
353 clean_html=True):
353 clean_html=True):
354 """
354 """
355 returns markdown rendered code cleaned by the bleach library
355 returns markdown rendered code cleaned by the bleach library
356 """
356 """
357
357
358 if flavored:
358 if flavored:
359 markdown_renderer = get_markdown_renderer_flavored(
359 markdown_renderer = get_markdown_renderer_flavored(
360 cls.extensions, cls.output_format)
360 cls.extensions, cls.output_format)
361 else:
361 else:
362 markdown_renderer = get_markdown_renderer(
362 markdown_renderer = get_markdown_renderer(
363 cls.extensions, cls.output_format)
363 cls.extensions, cls.output_format)
364
364
365 if mentions:
365 if mentions:
366 mention_hl = cls.convert_mentions(source, mode='markdown')
366 mention_hl = cls.convert_mentions(source, mode='markdown')
367 # we extracted mentions render with this using Mentions false
367 # we extracted mentions render with this using Mentions false
368 return cls.markdown(mention_hl, safe=safe, flavored=flavored,
368 return cls.markdown(mention_hl, safe=safe, flavored=flavored,
369 mentions=False)
369 mentions=False)
370
370
371 try:
371 try:
372 rendered = markdown_renderer.convert(source)
372 rendered = markdown_renderer.convert(source)
373
373
374 except Exception:
374 except Exception:
375 log.exception('Error when rendering Markdown')
375 log.exception('Error when rendering Markdown')
376 if safe:
376 if safe:
377 log.debug('Fallback to render in plain mode')
377 log.debug('Fallback to render in plain mode')
378 rendered = cls.plain(source)
378 rendered = cls.plain(source)
379 else:
379 else:
380 raise
380 raise
381
381
382 if clean_html:
382 if clean_html:
383 rendered = cls.sanitize_html(rendered)
383 rendered = cls.sanitize_html(rendered)
384 return rendered
384 return rendered
385
385
386 @classmethod
386 @classmethod
387 def rst(cls, source, safe=True, mentions=False, clean_html=False):
387 def rst(cls, source, safe=True, mentions=False, clean_html=False):
388
388
389 if mentions:
389 if mentions:
390 mention_hl = cls.convert_mentions(source, mode='rst')
390 mention_hl = cls.convert_mentions(source, mode='rst')
391 # we extracted mentions render with this using Mentions false
391 # we extracted mentions render with this using Mentions false
392 return cls.rst(mention_hl, safe=safe, mentions=False)
392 return cls.rst(mention_hl, safe=safe, mentions=False)
393
393
394 source = safe_str(source)
394 source = safe_str(source)
395 try:
395 try:
396 docutils_settings = dict(
396 docutils_settings = dict(
397 [(alias, None) for alias in
397 [(alias, None) for alias in
398 cls.RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES])
398 cls.RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES])
399
399
400 docutils_settings.update({
400 docutils_settings.update({
401 'input_encoding': 'unicode',
401 'input_encoding': 'unicode',
402 'report_level': 4,
402 'report_level': 4,
403 'syntax_highlight': 'short',
403 'syntax_highlight': 'short',
404 })
404 })
405
405
406 for k, v in list(docutils_settings.items()):
406 for k, v in list(docutils_settings.items()):
407 directives.register_directive(k, v)
407 directives.register_directive(k, v)
408
408
409 parts = publish_parts(source=source,
409 parts = publish_parts(source=source,
410 writer=RhodeCodeWriter(),
410 writer=RhodeCodeWriter(),
411 settings_overrides=docutils_settings)
411 settings_overrides=docutils_settings)
412 rendered = parts["fragment"]
412 rendered = parts["fragment"]
413 if clean_html:
413 if clean_html:
414 rendered = cls.sanitize_html(rendered)
414 rendered = cls.sanitize_html(rendered)
415 return parts['html_title'] + rendered
415 return parts['html_title'] + rendered
416 except Exception:
416 except Exception:
417 log.exception('Error when rendering RST')
417 log.exception('Error when rendering RST')
418 if safe:
418 if safe:
419 log.debug('Fallback to render in plain mode')
419 log.debug('Fallback to render in plain mode')
420 return cls.plain(source)
420 return cls.plain(source)
421 else:
421 else:
422 raise
422 raise
423
423
424 @classmethod
424 @classmethod
425 def jupyter(cls, source, safe=True):
425 def jupyter(cls, source, safe=True):
426 from rhodecode.lib import helpers
426 from rhodecode.lib import helpers
427
427
428 from traitlets import default, config
428 from traitlets import default, config
429 import nbformat
429 import nbformat
430 from nbconvert import HTMLExporter
430 from nbconvert import HTMLExporter
431 from nbconvert.preprocessors import Preprocessor
431 from nbconvert.preprocessors import Preprocessor
432
432
433 class CustomHTMLExporter(HTMLExporter):
433 class CustomHTMLExporter(HTMLExporter):
434
434
435 @default("template_file")
435 @default("template_file")
436 def _template_file_default(self):
436 def _template_file_default(self):
437 if self.template_extension:
437 if self.template_extension:
438 return "basic/index" + self.template_extension
438 return "basic/index" + self.template_extension
439
439
440 class Sandbox(Preprocessor):
440 class Sandbox(Preprocessor):
441
441
442 def preprocess(self, nb, resources):
442 def preprocess(self, nb, resources):
443 sandbox_text = 'SandBoxed(IPython.core.display.Javascript object)'
443 sandbox_text = 'SandBoxed(IPython.core.display.Javascript object)'
444 for cell in nb['cells']:
444 for cell in nb['cells']:
445 if not safe:
445 if not safe:
446 continue
446 continue
447
447
448 if 'outputs' in cell:
448 if 'outputs' in cell:
449 for cell_output in cell['outputs']:
449 for cell_output in cell['outputs']:
450 if 'data' in cell_output:
450 if 'data' in cell_output:
451 if 'application/javascript' in cell_output['data']:
451 if 'application/javascript' in cell_output['data']:
452 cell_output['data']['text/plain'] = sandbox_text
452 cell_output['data']['text/plain'] = sandbox_text
453 cell_output['data'].pop('application/javascript', None)
453 cell_output['data'].pop('application/javascript', None)
454
454
455 if 'source' in cell and cell['cell_type'] == 'markdown':
455 if 'source' in cell and cell['cell_type'] == 'markdown':
456 # sanitize similar like in markdown
456 # sanitize similar like in markdown
457 cell['source'] = cls.sanitize_html(cell['source'])
457 cell['source'] = cls.sanitize_html(cell['source'])
458
458
459 return nb, resources
459 return nb, resources
460
460
461 def _sanitize_resources(input_resources):
461 def _sanitize_resources(input_resources):
462 """
462 """
463 Skip/sanitize some of the CSS generated and included in jupyter
463 Skip/sanitize some of the CSS generated and included in jupyter
464 so it doesn't mess up UI so much
464 so it doesn't mess up UI so much
465 """
465 """
466
466
467 # TODO(marcink): probably we should replace this with whole custom
467 # TODO(marcink): probably we should replace this with whole custom
468 # CSS set that doesn't screw up, but jupyter generated html has some
468 # CSS set that doesn't screw up, but jupyter generated html has some
469 # special markers, so it requires Custom HTML exporter template with
469 # special markers, so it requires Custom HTML exporter template with
470 # _default_template_path_default, to achieve that
470 # _default_template_path_default, to achieve that
471
471
472 # strip the reset CSS
472 # strip the reset CSS
473 input_resources[0] = input_resources[0][input_resources[0].find('/*! Source'):]
473 input_resources[0] = input_resources[0][input_resources[0].find('/*! Source'):]
474 return input_resources
474 return input_resources
475
475
476 def as_html(notebook):
476 def as_html(notebook):
477 conf = config.Config()
477 conf = config.Config()
478 conf.CustomHTMLExporter.preprocessors = [Sandbox]
478 conf.CustomHTMLExporter.default_preprocessors = [Sandbox]
479 conf.Sandbox.enabled = True
479 html_exporter = CustomHTMLExporter(config=conf)
480 html_exporter = CustomHTMLExporter(config=conf)
480
481
481 (body, resources) = html_exporter.from_notebook_node(notebook)
482 (body, resources) = html_exporter.from_notebook_node(notebook)
482
483
483 header = '<!-- ## IPYTHON NOTEBOOK RENDERING ## -->'
484 header = '<!-- ## IPYTHON NOTEBOOK RENDERING ## -->'
484 js = MakoTemplate(r'''
485 js = MakoTemplate(r'''
485 <!-- MathJax configuration -->
486 <!-- MathJax configuration -->
486 <script type="text/x-mathjax-config">
487 <script type="text/x-mathjax-config">
487 MathJax.Hub.Config({
488 MathJax.Hub.Config({
488 jax: ["input/TeX","output/HTML-CSS", "output/PreviewHTML"],
489 jax: ["input/TeX","output/HTML-CSS", "output/PreviewHTML"],
489 extensions: ["tex2jax.js","MathMenu.js","MathZoom.js", "fast-preview.js", "AssistiveMML.js", "[Contrib]/a11y/accessibility-menu.js"],
490 extensions: ["tex2jax.js","MathMenu.js","MathZoom.js", "fast-preview.js", "AssistiveMML.js", "[Contrib]/a11y/accessibility-menu.js"],
490 TeX: {
491 TeX: {
491 extensions: ["AMSmath.js","AMSsymbols.js","noErrors.js","noUndefined.js"]
492 extensions: ["AMSmath.js","AMSsymbols.js","noErrors.js","noUndefined.js"]
492 },
493 },
493 tex2jax: {
494 tex2jax: {
494 inlineMath: [ ['$','$'], ["\\(","\\)"] ],
495 inlineMath: [ ['$','$'], ["\\(","\\)"] ],
495 displayMath: [ ['$$','$$'], ["\\[","\\]"] ],
496 displayMath: [ ['$$','$$'], ["\\[","\\]"] ],
496 processEscapes: true,
497 processEscapes: true,
497 processEnvironments: true
498 processEnvironments: true
498 },
499 },
499 // Center justify equations in code and markdown cells. Elsewhere
500 // Center justify equations in code and markdown cells. Elsewhere
500 // we use CSS to left justify single line equations in code cells.
501 // we use CSS to left justify single line equations in code cells.
501 displayAlign: 'center',
502 displayAlign: 'center',
502 "HTML-CSS": {
503 "HTML-CSS": {
503 styles: {'.MathJax_Display': {"margin": 0}},
504 styles: {'.MathJax_Display': {"margin": 0}},
504 linebreaks: { automatic: true },
505 linebreaks: { automatic: true },
505 availableFonts: ["STIX", "TeX"]
506 availableFonts: ["STIX", "TeX"]
506 },
507 },
507 showMathMenu: false
508 showMathMenu: false
508 });
509 });
509 </script>
510 </script>
510 <!-- End of MathJax configuration -->
511 <!-- End of MathJax configuration -->
511 <script src="${h.asset('js/src/math_jax/MathJax.js')}"></script>
512 <script src="${h.asset('js/src/math_jax/MathJax.js')}"></script>
512 ''').render(h=helpers)
513 ''').render(h=helpers)
513
514
514 css = MakoTemplate(r'''
515 css = MakoTemplate(r'''
515 <link rel="stylesheet" type="text/css" href="${h.asset('css/style-ipython.css', ver=ver)}" media="screen"/>
516 <link rel="stylesheet" type="text/css" href="${h.asset('css/style-ipython.css', ver=ver)}" media="screen"/>
516 ''').render(h=helpers, ver='ver1')
517 ''').render(h=helpers, ver='ver1')
517
518
518 body = '\n'.join([header, css, js, body])
519 body = '\n'.join([header, css, js, body])
519 return body, resources
520 return body, resources
520
521
521 notebook = nbformat.reads(source, as_version=nbformat.NO_CONVERT)
522 # TODO: In the event of a newer jupyter notebook version, consider increasing the as_version parameter
523 notebook = nbformat.reads(source, as_version=4)
522 (body, resources) = as_html(notebook)
524 (body, resources) = as_html(notebook)
523 return body
525 return body
524
526
525
527
526 class RstTemplateRenderer(object):
528 class RstTemplateRenderer(object):
527
529
528 def __init__(self):
530 def __init__(self):
529 base = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
531 base = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
530 rst_template_dirs = [os.path.join(base, 'templates', 'rst_templates')]
532 rst_template_dirs = [os.path.join(base, 'templates', 'rst_templates')]
531 self.template_store = TemplateLookup(
533 self.template_store = TemplateLookup(
532 directories=rst_template_dirs,
534 directories=rst_template_dirs,
533 input_encoding='utf-8',
535 input_encoding='utf-8',
534 imports=['from rhodecode.lib import helpers as h'])
536 imports=['from rhodecode.lib import helpers as h'])
535
537
536 def _get_template(self, templatename):
538 def _get_template(self, templatename):
537 return self.template_store.get_template(templatename)
539 return self.template_store.get_template(templatename)
538
540
539 def render(self, template_name, **kwargs):
541 def render(self, template_name, **kwargs):
540 template = self._get_template(template_name)
542 template = self._get_template(template_name)
541 return template.render(**kwargs)
543 return template.render(**kwargs)
@@ -1,685 +1,785 b''
1
1
2 # Copyright (C) 2010-2023 RhodeCode GmbH
2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
6 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
15 #
16 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
17 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19
19
20 import pytest
20 import pytest
21
21
22 from rhodecode.lib.markup_renderer import (
22 from rhodecode.lib.markup_renderer import (
23 MarkupRenderer, RstTemplateRenderer, relative_path, relative_links)
23 MarkupRenderer, RstTemplateRenderer, relative_path, relative_links)
24
24
25
25
26 @pytest.mark.parametrize(
26 @pytest.mark.parametrize(
27 "filename, expected_renderer",
27 "filename, expected_renderer",
28 [
28 [
29 ('readme.md', 'markdown'),
29 ('readme.md', 'markdown'),
30 ('readme.Md', 'markdown'),
30 ('readme.Md', 'markdown'),
31 ('readme.MdoWn', 'markdown'),
31 ('readme.MdoWn', 'markdown'),
32 ('readme.rst', 'rst'),
32 ('readme.rst', 'rst'),
33 ('readme.Rst', 'rst'),
33 ('readme.Rst', 'rst'),
34 ('readme.rest', 'rst'),
34 ('readme.rest', 'rst'),
35 ('readme.rest', 'rst'),
35 ('readme.rest', 'rst'),
36
36
37 ('markdown.xml', 'plain'),
37 ('markdown.xml', 'plain'),
38 ('rest.xml', 'plain'),
38 ('rest.xml', 'plain'),
39 ('readme.xml', 'plain'),
39 ('readme.xml', 'plain'),
40
40
41 ('readme', 'plain'),
41 ('readme', 'plain'),
42 ('README', 'plain'),
42 ('README', 'plain'),
43 ('readme.mdx', 'plain'),
43 ('readme.mdx', 'plain'),
44 ('readme.rstx', 'plain'),
44 ('readme.rstx', 'plain'),
45 ('readmex', 'plain'),
45 ('readmex', 'plain'),
46 ])
46 ])
47 def test_detect_renderer(filename, expected_renderer):
47 def test_detect_renderer(filename, expected_renderer):
48 detected_renderer = MarkupRenderer()._detect_renderer(
48 detected_renderer = MarkupRenderer()._detect_renderer(
49 '', filename=filename).__name__
49 '', filename=filename).__name__
50 assert expected_renderer == detected_renderer
50 assert expected_renderer == detected_renderer
51
51
52
52
53 def test_markdown_xss_link():
53 def test_markdown_xss_link():
54 xss_md = "[link](javascript:alert('XSS: pwned!'))"
54 xss_md = "[link](javascript:alert('XSS: pwned!'))"
55 rendered_html = MarkupRenderer.markdown(xss_md)
55 rendered_html = MarkupRenderer.markdown(xss_md)
56 assert 'href="javascript:alert(\'XSS: pwned!\')"' not in rendered_html
56 assert 'href="javascript:alert(\'XSS: pwned!\')"' not in rendered_html
57
57
58
58
59 def test_markdown_xss_inline_html():
59 def test_markdown_xss_inline_html():
60 xss_md = '\n'.join([
60 xss_md = '\n'.join([
61 '> <a name="n"',
61 '> <a name="n"',
62 '> href="javascript:alert(\'XSS: pwned!\')">link</a>'])
62 '> href="javascript:alert(\'XSS: pwned!\')">link</a>'])
63 rendered_html = MarkupRenderer.markdown(xss_md)
63 rendered_html = MarkupRenderer.markdown(xss_md)
64 assert 'href="javascript:alert(\'XSS: pwned!\')">' not in rendered_html
64 assert 'href="javascript:alert(\'XSS: pwned!\')">' not in rendered_html
65
65
66
66
67 def test_markdown_inline_html():
67 def test_markdown_inline_html():
68 xss_md = '\n'.join(['> <a name="n"',
68 xss_md = '\n'.join(['> <a name="n"',
69 '> onload="javascript:alert()" href="https://rhodecode.com">link</a>'])
69 '> onload="javascript:alert()" href="https://rhodecode.com">link</a>'])
70 rendered_html = MarkupRenderer.markdown(xss_md)
70 rendered_html = MarkupRenderer.markdown(xss_md)
71 assert '<a name="n" href="https://rhodecode.com">link</a>' in rendered_html
71 assert '<a name="n" href="https://rhodecode.com">link</a>' in rendered_html
72
72
73
73
74 def test_markdown_bleach_renders_correct():
74 def test_markdown_bleach_renders_correct():
75 test_md = """
75 test_md = """
76 This is intended as a quick reference and showcase. For more complete info, see [John Gruber's original spec](http://daringfireball.net/projects/markdown/) and the [Github-flavored Markdown info page](http://github.github.com/github-flavored-markdown/).
76 This is intended as a quick reference and showcase. For more complete info, see [John Gruber's original spec](http://daringfireball.net/projects/markdown/) and the [Github-flavored Markdown info page](http://github.github.com/github-flavored-markdown/).
77
77
78 Note that there is also a [Cheatsheet specific to Markdown Here](./Markdown-Here-Cheatsheet) if that's what you're looking for. You can also check out [more Markdown tools](./Other-Markdown-Tools).
78 Note that there is also a [Cheatsheet specific to Markdown Here](./Markdown-Here-Cheatsheet) if that's what you're looking for. You can also check out [more Markdown tools](./Other-Markdown-Tools).
79
79
80 ##### Table of Contents
80 ##### Table of Contents
81 [Headers](#headers)
81 [Headers](#headers)
82 [Emphasis](#emphasis)
82 [Emphasis](#emphasis)
83 [Lists](#lists)
83 [Lists](#lists)
84 [Links](#links)
84 [Links](#links)
85 [Images](#images)
85 [Images](#images)
86 [Code and Syntax Highlighting](#code)
86 [Code and Syntax Highlighting](#code)
87 [Tables](#tables)
87 [Tables](#tables)
88 [Blockquotes](#blockquotes)
88 [Blockquotes](#blockquotes)
89 [Inline HTML](#html)
89 [Inline HTML](#html)
90 [Horizontal Rule](#hr)
90 [Horizontal Rule](#hr)
91 [Line Breaks](#lines)
91 [Line Breaks](#lines)
92 [Youtube videos](#videos)
92 [Youtube videos](#videos)
93
93
94
94
95 ## Headers
95 ## Headers
96
96
97 ```no-highlight
97 ```no-highlight
98 # H1
98 # H1
99 ## H2
99 ## H2
100 ### H3
100 ### H3
101 #### H4
101 #### H4
102 ##### H5
102 ##### H5
103 ###### H6
103 ###### H6
104
104
105 Alternatively, for H1 and H2, an underline-ish style:
105 Alternatively, for H1 and H2, an underline-ish style:
106
106
107 Alt-H1
107 Alt-H1
108 ======
108 ======
109
109
110 Alt-H2
110 Alt-H2
111 ------
111 ------
112 ```
112 ```
113
113
114 # H1
114 # H1
115 ## H2
115 ## H2
116 ### H3
116 ### H3
117 #### H4
117 #### H4
118 ##### H5
118 ##### H5
119 ###### H6
119 ###### H6
120
120
121 Alternatively, for H1 and H2, an underline-ish style:
121 Alternatively, for H1 and H2, an underline-ish style:
122
122
123 Alt-H1
123 Alt-H1
124 ======
124 ======
125
125
126 Alt-H2
126 Alt-H2
127 ------
127 ------
128
128
129 ## Emphasis
129 ## Emphasis
130
130
131 ```no-highlight
131 ```no-highlight
132 Emphasis, aka italics, with *asterisks* or _underscores_.
132 Emphasis, aka italics, with *asterisks* or _underscores_.
133
133
134 Strong emphasis, aka bold, with **asterisks** or __underscores__.
134 Strong emphasis, aka bold, with **asterisks** or __underscores__.
135
135
136 Combined emphasis with **asterisks and _underscores_**.
136 Combined emphasis with **asterisks and _underscores_**.
137
137
138 Strikethrough uses two tildes. ~~Scratch this.~~
138 Strikethrough uses two tildes. ~~Scratch this.~~
139 ```
139 ```
140
140
141 Emphasis, aka italics, with *asterisks* or _underscores_.
141 Emphasis, aka italics, with *asterisks* or _underscores_.
142
142
143 Strong emphasis, aka bold, with **asterisks** or __underscores__.
143 Strong emphasis, aka bold, with **asterisks** or __underscores__.
144
144
145 Combined emphasis with **asterisks and _underscores_**.
145 Combined emphasis with **asterisks and _underscores_**.
146
146
147 Strikethrough uses two tildes. ~~Scratch this.~~
147 Strikethrough uses two tildes. ~~Scratch this.~~
148
148
149
149
150 ## Lists
150 ## Lists
151
151
152 (In this example, leading and trailing spaces are shown with with dots: β‹…)
152 (In this example, leading and trailing spaces are shown with with dots: β‹…)
153
153
154 ```no-highlight
154 ```no-highlight
155 1. First ordered list item
155 1. First ordered list item
156 2. Another item
156 2. Another item
157 β‹…β‹…* Unordered sub-list.
157 β‹…β‹…* Unordered sub-list.
158 1. Actual numbers don't matter, just that it's a number
158 1. Actual numbers don't matter, just that it's a number
159 β‹…β‹…1. Ordered sub-list
159 β‹…β‹…1. Ordered sub-list
160 4. And another item.
160 4. And another item.
161
161
162 β‹…β‹…β‹…You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown).
162 β‹…β‹…β‹…You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown).
163
163
164 β‹…β‹…β‹…To have a line break without a paragraph, you will need to use two trailing spaces.β‹…β‹…
164 β‹…β‹…β‹…To have a line break without a paragraph, you will need to use two trailing spaces.β‹…β‹…
165 β‹…β‹…β‹…Note that this line is separate, but within the same paragraph.β‹…β‹…
165 β‹…β‹…β‹…Note that this line is separate, but within the same paragraph.β‹…β‹…
166 β‹…β‹…β‹…(This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.)
166 β‹…β‹…β‹…(This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.)
167
167
168 * Unordered list can use asterisks
168 * Unordered list can use asterisks
169 - Or minuses
169 - Or minuses
170 + Or pluses
170 + Or pluses
171 ```
171 ```
172
172
173 1. First ordered list item
173 1. First ordered list item
174 2. Another item
174 2. Another item
175 * Unordered sub-list.
175 * Unordered sub-list.
176 1. Actual numbers don't matter, just that it's a number
176 1. Actual numbers don't matter, just that it's a number
177 1. Ordered sub-list
177 1. Ordered sub-list
178 4. And another item.
178 4. And another item.
179
179
180 You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown).
180 You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown).
181
181
182 To have a line break without a paragraph, you will need to use two trailing spaces.
182 To have a line break without a paragraph, you will need to use two trailing spaces.
183 Note that this line is separate, but within the same paragraph.
183 Note that this line is separate, but within the same paragraph.
184 (This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.)
184 (This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.)
185
185
186 * Unordered list can use asterisks
186 * Unordered list can use asterisks
187 - Or minuses
187 - Or minuses
188 + Or pluses
188 + Or pluses
189
189
190
190
191 ## Links
191 ## Links
192
192
193 There are two ways to create links.
193 There are two ways to create links.
194
194
195 ```no-highlight
195 ```no-highlight
196 [I'm an inline-style link](https://www.google.com)
196 [I'm an inline-style link](https://www.google.com)
197
197
198 [I'm an inline-style link with title](https://www.google.com "Google's Homepage")
198 [I'm an inline-style link with title](https://www.google.com "Google's Homepage")
199
199
200 [I'm a reference-style link][Arbitrary case-insensitive reference text]
200 [I'm a reference-style link][Arbitrary case-insensitive reference text]
201
201
202 [I'm a relative reference to a repository file (LICENSE)](./LICENSE)
202 [I'm a relative reference to a repository file (LICENSE)](./LICENSE)
203
203
204 [I'm a relative reference to a repository file (IMAGE)](./img/logo.png)
204 [I'm a relative reference to a repository file (IMAGE)](./img/logo.png)
205
205
206 [I'm a relative reference to a repository file (IMAGE2)](img/logo.png)
206 [I'm a relative reference to a repository file (IMAGE2)](img/logo.png)
207
207
208 [You can use numbers for reference-style link definitions][1]
208 [You can use numbers for reference-style link definitions][1]
209
209
210 Or leave it empty and use the [link text itself].
210 Or leave it empty and use the [link text itself].
211
211
212 URLs and URLs in angle brackets will automatically get turned into links.
212 URLs and URLs in angle brackets will automatically get turned into links.
213 http://www.example.com or <http://www.example.com> and sometimes
213 http://www.example.com or <http://www.example.com> and sometimes
214 example.com (but not on Github, for example).
214 example.com (but not on Github, for example).
215
215
216 Some text to show that the reference links can follow later.
216 Some text to show that the reference links can follow later.
217
217
218 [arbitrary case-insensitive reference text]: https://www.mozilla.org
218 [arbitrary case-insensitive reference text]: https://www.mozilla.org
219 [1]: http://slashdot.org
219 [1]: http://slashdot.org
220 [link text itself]: http://www.reddit.com
220 [link text itself]: http://www.reddit.com
221 ```
221 ```
222
222
223 [I'm an inline-style link](https://www.google.com)
223 [I'm an inline-style link](https://www.google.com)
224
224
225 [I'm an inline-style link with title](https://www.google.com "Google's Homepage")
225 [I'm an inline-style link with title](https://www.google.com "Google's Homepage")
226
226
227 [I'm a reference-style link][Arbitrary case-insensitive reference text]
227 [I'm a reference-style link][Arbitrary case-insensitive reference text]
228
228
229 [I'm a relative reference to a repository file (LICENSE)](./LICENSE)
229 [I'm a relative reference to a repository file (LICENSE)](./LICENSE)
230
230
231 [I'm a relative reference to a repository file (IMAGE)](./img/logo.png)
231 [I'm a relative reference to a repository file (IMAGE)](./img/logo.png)
232
232
233 [I'm a relative reference to a repository file (IMAGE2)](img/logo.png)
233 [I'm a relative reference to a repository file (IMAGE2)](img/logo.png)
234
234
235 [You can use numbers for reference-style link definitions][1]
235 [You can use numbers for reference-style link definitions][1]
236
236
237 Or leave it empty and use the [link text itself].
237 Or leave it empty and use the [link text itself].
238
238
239 URLs and URLs in angle brackets will automatically get turned into links.
239 URLs and URLs in angle brackets will automatically get turned into links.
240 http://www.example.com or <http://www.example.com> and sometimes
240 http://www.example.com or <http://www.example.com> and sometimes
241 example.com (but not on Github, for example).
241 example.com (but not on Github, for example).
242
242
243 Some text to show that the reference links can follow later.
243 Some text to show that the reference links can follow later.
244
244
245 [arbitrary case-insensitive reference text]: https://www.mozilla.org
245 [arbitrary case-insensitive reference text]: https://www.mozilla.org
246 [1]: http://slashdot.org
246 [1]: http://slashdot.org
247 [link text itself]: http://www.reddit.com
247 [link text itself]: http://www.reddit.com
248
248
249
249
250 ## Images
250 ## Images
251
251
252 ```no-highlight
252 ```no-highlight
253 Here's our logo (hover to see the title text):
253 Here's our logo (hover to see the title text):
254
254
255 Inline-style:
255 Inline-style:
256 ![alt text](https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png "Logo Title Text 1")
256 ![alt text](https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png "Logo Title Text 1")
257
257
258 relative-src-style:
258 relative-src-style:
259 ![alt text](img/logo.png)
259 ![alt text](img/logo.png)
260
260
261 Reference-style:
261 Reference-style:
262 ![alt text][logo]
262 ![alt text][logo]
263
263
264 [logo]: https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png "Logo Title Text 2"
264 [logo]: https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png "Logo Title Text 2"
265 ```
265 ```
266
266
267 Here's our logo (hover to see the title text):
267 Here's our logo (hover to see the title text):
268
268
269 Inline-style:
269 Inline-style:
270 ![alt text](https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png "Logo Title Text 1")
270 ![alt text](https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png "Logo Title Text 1")
271
271
272 relative-src-style:
272 relative-src-style:
273 ![alt text](img/logo.png)
273 ![alt text](img/logo.png)
274
274
275 relative-src-style:
275 relative-src-style:
276 ![alt text](./img/logo.png)
276 ![alt text](./img/logo.png)
277
277
278 Reference-style:
278 Reference-style:
279 ![alt text][logo]
279 ![alt text][logo]
280
280
281 [logo]: https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png "Logo Title Text 2"
281 [logo]: https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png "Logo Title Text 2"
282
282
283
283
284 ## Code and Syntax Highlighting
284 ## Code and Syntax Highlighting
285
285
286 Code blocks are part of the Markdown spec, but syntax highlighting isn't. However, many renderers -- like Github's and *Markdown Here* -- support syntax highlighting. Which languages are supported and how those language names should be written will vary from renderer to renderer. *Markdown Here* supports highlighting for dozens of languages (and not-really-languages, like diffs and HTTP headers); to see the complete list, and how to write the language names, see the [highlight.js demo page](http://softwaremaniacs.org/media/soft/highlight/test.html).
286 Code blocks are part of the Markdown spec, but syntax highlighting isn't. However, many renderers -- like Github's and *Markdown Here* -- support syntax highlighting. Which languages are supported and how those language names should be written will vary from renderer to renderer. *Markdown Here* supports highlighting for dozens of languages (and not-really-languages, like diffs and HTTP headers); to see the complete list, and how to write the language names, see the [highlight.js demo page](http://softwaremaniacs.org/media/soft/highlight/test.html).
287
287
288 ```no-highlight
288 ```no-highlight
289 Inline `code` has `back-ticks around` it.
289 Inline `code` has `back-ticks around` it.
290 ```
290 ```
291
291
292 Inline `code` has `back-ticks around` it.
292 Inline `code` has `back-ticks around` it.
293
293
294 Blocks of code are either fenced by lines with three back-ticks <code>```</code>, or are indented with four spaces. I recommend only using the fenced code blocks -- they're easier and only they support syntax highlighting.
294 Blocks of code are either fenced by lines with three back-ticks <code>```</code>, or are indented with four spaces. I recommend only using the fenced code blocks -- they're easier and only they support syntax highlighting.
295
295
296 ```javascript
296 ```javascript
297 var s = "JavaScript syntax highlighting";
297 var s = "JavaScript syntax highlighting";
298 console.log(s);
298 console.log(s);
299 ```
299 ```
300
300
301 ```python
301 ```python
302 s = "Python syntax highlighting"
302 s = "Python syntax highlighting"
303 print(s)
303 print(s)
304
304
305 class Orm(object):
305 class Orm(object):
306 pass
306 pass
307 ```
307 ```
308
308
309 ```
309 ```
310 No language indicated, so no syntax highlighting.
310 No language indicated, so no syntax highlighting.
311 But let's throw in a &lt;b&gt;tag&lt;/b&gt;.
311 But let's throw in a &lt;b&gt;tag&lt;/b&gt;.
312 ```
312 ```
313
313
314
314
315 ```javascript
315 ```javascript
316 var s = "JavaScript syntax highlighting";
316 var s = "JavaScript syntax highlighting";
317 alert(s);
317 alert(s);
318 ```
318 ```
319
319
320 ```python
320 ```python
321 s = "Python syntax highlighting"
321 s = "Python syntax highlighting"
322 print(s)
322 print(s)
323
323
324 class Orm(object):
324 class Orm(object):
325 pass
325 pass
326 ```
326 ```
327
327
328 ```
328 ```
329 No language indicated, so no syntax highlighting in Markdown Here (varies on Github).
329 No language indicated, so no syntax highlighting in Markdown Here (varies on Github).
330 But let's throw in a <b>tag</b>.
330 But let's throw in a <b>tag</b>.
331 ```
331 ```
332
332
333
333
334 ## Tables
334 ## Tables
335
335
336 Tables aren't part of the core Markdown spec, but they are part of GFM and *Markdown Here* supports them. They are an easy way of adding tables to your email -- a task that would otherwise require copy-pasting from another application.
336 Tables aren't part of the core Markdown spec, but they are part of GFM and *Markdown Here* supports them. They are an easy way of adding tables to your email -- a task that would otherwise require copy-pasting from another application.
337
337
338 ```no-highlight
338 ```no-highlight
339 Colons can be used to align columns.
339 Colons can be used to align columns.
340
340
341 | Tables | Are | Cool |
341 | Tables | Are | Cool |
342 | ------------- |:-------------:| -----:|
342 | ------------- |:-------------:| -----:|
343 | col 3 is | right-aligned | $1600 |
343 | col 3 is | right-aligned | $1600 |
344 | col 2 is | centered | $12 |
344 | col 2 is | centered | $12 |
345 | zebra stripes | are neat | $1 |
345 | zebra stripes | are neat | $1 |
346
346
347 There must be at least 3 dashes separating each header cell.
347 There must be at least 3 dashes separating each header cell.
348 The outer pipes (|) are optional, and you don't need to make the
348 The outer pipes (|) are optional, and you don't need to make the
349 raw Markdown line up prettily. You can also use inline Markdown.
349 raw Markdown line up prettily. You can also use inline Markdown.
350
350
351 Markdown | Less | Pretty
351 Markdown | Less | Pretty
352 --- | --- | ---
352 --- | --- | ---
353 *Still* | `renders` | **nicely**
353 *Still* | `renders` | **nicely**
354 1 | 2 | 3
354 1 | 2 | 3
355 ```
355 ```
356
356
357 Colons can be used to align columns.
357 Colons can be used to align columns.
358
358
359 | Tables | Are | Cool |
359 | Tables | Are | Cool |
360 | ------------- |:-------------:| -----:|
360 | ------------- |:-------------:| -----:|
361 | col 3 is | right-aligned | $1600 |
361 | col 3 is | right-aligned | $1600 |
362 | col 2 is | centered | $12 |
362 | col 2 is | centered | $12 |
363 | zebra stripes | are neat | $1 |
363 | zebra stripes | are neat | $1 |
364
364
365 There must be at least 3 dashes separating each header cell. The outer pipes (|) are optional, and you don't need to make the raw Markdown line up prettily. You can also use inline Markdown.
365 There must be at least 3 dashes separating each header cell. The outer pipes (|) are optional, and you don't need to make the raw Markdown line up prettily. You can also use inline Markdown.
366
366
367 Markdown | Less | Pretty
367 Markdown | Less | Pretty
368 --- | --- | ---
368 --- | --- | ---
369 *Still* | `renders` | **nicely**
369 *Still* | `renders` | **nicely**
370 1 | 2 | 3
370 1 | 2 | 3
371
371
372
372
373 ## Blockquotes
373 ## Blockquotes
374
374
375 ```no-highlight
375 ```no-highlight
376 > Blockquotes are very handy in email to emulate reply text.
376 > Blockquotes are very handy in email to emulate reply text.
377 > This line is part of the same quote.
377 > This line is part of the same quote.
378
378
379 Quote break.
379 Quote break.
380
380
381 > This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.
381 > This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.
382 ```
382 ```
383
383
384 > Blockquotes are very handy in email to emulate reply text.
384 > Blockquotes are very handy in email to emulate reply text.
385 > This line is part of the same quote.
385 > This line is part of the same quote.
386
386
387 Quote break.
387 Quote break.
388
388
389 > This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.
389 > This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.
390
390
391
391
392 ## Inline HTML
392 ## Inline HTML
393
393
394 You can also use raw HTML in your Markdown, and it'll mostly work pretty well.
394 You can also use raw HTML in your Markdown, and it'll mostly work pretty well.
395
395
396 ```no-highlight
396 ```no-highlight
397 <dl>
397 <dl>
398 <dt>Definition list</dt>
398 <dt>Definition list</dt>
399 <dd>Is something people use sometimes.</dd>
399 <dd>Is something people use sometimes.</dd>
400
400
401 <dt>Markdown in HTML</dt>
401 <dt>Markdown in HTML</dt>
402 <dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd>
402 <dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd>
403 </dl>
403 </dl>
404 ```
404 ```
405
405
406 <dl>
406 <dl>
407 <dt>Definition list</dt>
407 <dt>Definition list</dt>
408 <dd>Is something people use sometimes.</dd>
408 <dd>Is something people use sometimes.</dd>
409
409
410 <dt>Markdown in HTML</dt>
410 <dt>Markdown in HTML</dt>
411 <dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd>
411 <dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd>
412 </dl>
412 </dl>
413
413
414
414
415 ## Horizontal Rule
415 ## Horizontal Rule
416
416
417 ```
417 ```
418 Three or more...
418 Three or more...
419
419
420 ---
420 ---
421
421
422 Hyphens
422 Hyphens
423
423
424 ***
424 ***
425
425
426 Asterisks
426 Asterisks
427
427
428 ___
428 ___
429
429
430 Underscores
430 Underscores
431 ```
431 ```
432
432
433 Three or more...
433 Three or more...
434
434
435 ---
435 ---
436
436
437 Hyphens
437 Hyphens
438
438
439 ***
439 ***
440
440
441 Asterisks
441 Asterisks
442
442
443 ___
443 ___
444
444
445 Underscores
445 Underscores
446
446
447
447
448 ## Line Breaks
448 ## Line Breaks
449
449
450 My basic recommendation for learning how line breaks work is to experiment and discover -- hit &lt;Enter&gt; once (i.e., insert one newline), then hit it twice (i.e., insert two newlines), see what happens. You'll soon learn to get what you want. "Markdown Toggle" is your friend.
450 My basic recommendation for learning how line breaks work is to experiment and discover -- hit &lt;Enter&gt; once (i.e., insert one newline), then hit it twice (i.e., insert two newlines), see what happens. You'll soon learn to get what you want. "Markdown Toggle" is your friend.
451
451
452 Here are some things to try out:
452 Here are some things to try out:
453
453
454 ```
454 ```
455 Here's a line for us to start with.
455 Here's a line for us to start with.
456
456
457 This line is separated from the one above by two newlines, so it will be a *separate paragraph*.
457 This line is separated from the one above by two newlines, so it will be a *separate paragraph*.
458
458
459 This line is also a separate paragraph, but...
459 This line is also a separate paragraph, but...
460 This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
460 This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
461 ```
461 ```
462
462
463 Here's a line for us to start with.
463 Here's a line for us to start with.
464
464
465 This line is separated from the one above by two newlines, so it will be a *separate paragraph*.
465 This line is separated from the one above by two newlines, so it will be a *separate paragraph*.
466
466
467 This line is also begins a separate paragraph, but...
467 This line is also begins a separate paragraph, but...
468 This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
468 This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
469
469
470 (Technical note: *Markdown Here* uses GFM line breaks, so there's no need to use MD's two-space line breaks.)
470 (Technical note: *Markdown Here* uses GFM line breaks, so there's no need to use MD's two-space line breaks.)
471
471
472
472
473 ## Youtube videos
473 ## Youtube videos
474
474
475 They can't be added directly but you can add an image with a link to the video like this:
475 They can't be added directly but you can add an image with a link to the video like this:
476
476
477 ```no-highlight
477 ```no-highlight
478 <a href="http://www.youtube.com/watch?feature=player_embedded&v=YOUTUBE_VIDEO_ID_HERE
478 <a href="http://www.youtube.com/watch?feature=player_embedded&v=YOUTUBE_VIDEO_ID_HERE
479 " target="_blank"><img src="http://img.youtube.com/vi/YOUTUBE_VIDEO_ID_HERE/0.jpg"
479 " target="_blank"><img src="http://img.youtube.com/vi/YOUTUBE_VIDEO_ID_HERE/0.jpg"
480 alt="IMAGE ALT TEXT HERE" width="240" height="180" border="10" /></a>
480 alt="IMAGE ALT TEXT HERE" width="240" height="180" border="10" /></a>
481 ```
481 ```
482
482
483 Or, in pure Markdown, but losing the image sizing and border:
483 Or, in pure Markdown, but losing the image sizing and border:
484
484
485 ```no-highlight
485 ```no-highlight
486 [![IMAGE ALT TEXT HERE](http://img.youtube.com/vi/YOUTUBE_VIDEO_ID_HERE/0.jpg)](http://www.youtube.com/watch?v=YOUTUBE_VIDEO_ID_HERE)
486 [![IMAGE ALT TEXT HERE](http://img.youtube.com/vi/YOUTUBE_VIDEO_ID_HERE/0.jpg)](http://www.youtube.com/watch?v=YOUTUBE_VIDEO_ID_HERE)
487 ```
487 ```
488
488
489 Referencing a bug by #bugID in your git commit links it to the slip. For example #1.
489 Referencing a bug by #bugID in your git commit links it to the slip. For example #1.
490
490
491 ---
491 ---
492
492
493 License: [CC-BY](https://creativecommons.org/licenses/by/3.0/)
493 License: [CC-BY](https://creativecommons.org/licenses/by/3.0/)
494 """
494 """
495 raw_rendered_html = MarkupRenderer.markdown(test_md, clean_html=False)
495 raw_rendered_html = MarkupRenderer.markdown(test_md, clean_html=False)
496 bleached_rendered_html = MarkupRenderer.markdown(test_md, clean_html=True)
496 bleached_rendered_html = MarkupRenderer.markdown(test_md, clean_html=True)
497 assert raw_rendered_html == bleached_rendered_html
497 assert raw_rendered_html == bleached_rendered_html
498
498
499
499
500 def test_rst_xss_link():
500 def test_rst_xss_link():
501 xss_rst = "`Link<javascript:alert('XSS: pwned!')>`_"
501 xss_rst = "`Link<javascript:alert('XSS: pwned!')>`_"
502 rendered_html = MarkupRenderer.rst(xss_rst)
502 rendered_html = MarkupRenderer.rst(xss_rst)
503 assert "href=javascript:alert('XSS: pwned!')" not in rendered_html
503 assert "href=javascript:alert('XSS: pwned!')" not in rendered_html
504
504
505
505
506 @pytest.mark.xfail(reason='Bug in docutils. Waiting answer from the author')
506 @pytest.mark.xfail(reason='Bug in docutils. Waiting answer from the author')
507 def test_rst_xss_inline_html():
507 def test_rst_xss_inline_html():
508 xss_rst = '<a href="javascript:alert(\'XSS: pwned!\')">link</a>'
508 xss_rst = '<a href="javascript:alert(\'XSS: pwned!\')">link</a>'
509 rendered_html = MarkupRenderer.rst(xss_rst)
509 rendered_html = MarkupRenderer.rst(xss_rst)
510 assert 'href="javascript:alert(' not in rendered_html
510 assert 'href="javascript:alert(' not in rendered_html
511
511
512
512
513 def test_rst_xss_raw_directive():
513 def test_rst_xss_raw_directive():
514 xss_rst = '\n'.join([
514 xss_rst = '\n'.join([
515 '.. raw:: html',
515 '.. raw:: html',
516 '',
516 '',
517 ' <a href="javascript:alert(\'XSS: pwned!\')">link</a>'])
517 ' <a href="javascript:alert(\'XSS: pwned!\')">link</a>'])
518 rendered_html = MarkupRenderer.rst(xss_rst)
518 rendered_html = MarkupRenderer.rst(xss_rst)
519 assert 'href="javascript:alert(' not in rendered_html
519 assert 'href="javascript:alert(' not in rendered_html
520
520
521
521
522 def test_render_rst_template_without_files():
522 def test_render_rst_template_without_files():
523 expected = u'''\
523 expected = u'''\
524 Pull request updated. Auto status change to |under_review|
524 Pull request updated. Auto status change to |under_review|
525
525
526 .. role:: added
526 .. role:: added
527 .. role:: removed
527 .. role:: removed
528 .. parsed-literal::
528 .. parsed-literal::
529
529
530 Changed commits:
530 Changed commits:
531 * :added:`2 added`
531 * :added:`2 added`
532 * :removed:`3 removed`
532 * :removed:`3 removed`
533
533
534 No file changes found
534 No file changes found
535
535
536 .. |under_review| replace:: *"NEW STATUS"*'''
536 .. |under_review| replace:: *"NEW STATUS"*'''
537
537
538 params = {
538 params = {
539 'under_review_label': 'NEW STATUS',
539 'under_review_label': 'NEW STATUS',
540 'added_commits': ['a', 'b'],
540 'added_commits': ['a', 'b'],
541 'removed_commits': ['a', 'b', 'c'],
541 'removed_commits': ['a', 'b', 'c'],
542 'changed_files': [],
542 'changed_files': [],
543 'added_files': [],
543 'added_files': [],
544 'modified_files': [],
544 'modified_files': [],
545 'removed_files': [],
545 'removed_files': [],
546 'ancestor_commit_id': 'aaabbbcccdddeee',
546 'ancestor_commit_id': 'aaabbbcccdddeee',
547 }
547 }
548 renderer = RstTemplateRenderer()
548 renderer = RstTemplateRenderer()
549 rendered = renderer.render('pull_request_update.mako', **params)
549 rendered = renderer.render('pull_request_update.mako', **params)
550 assert expected == rendered
550 assert expected == rendered
551
551
552
552
553 def test_render_rst_template_with_files():
553 def test_render_rst_template_with_files():
554 expected = u'''\
554 expected = u'''\
555 Pull request updated. Auto status change to |under_review|
555 Pull request updated. Auto status change to |under_review|
556
556
557 .. role:: added
557 .. role:: added
558 .. role:: removed
558 .. role:: removed
559 .. parsed-literal::
559 .. parsed-literal::
560
560
561 Changed commits:
561 Changed commits:
562 * :added:`1 added`
562 * :added:`1 added`
563 * :removed:`3 removed`
563 * :removed:`3 removed`
564
564
565 Changed files:
565 Changed files:
566 * `A /path/a.py <#a_c-aaabbbcccddd-68ed34923b68>`_
566 * `A /path/a.py <#a_c-aaabbbcccddd-68ed34923b68>`_
567 * `A /path/b.js <#a_c-aaabbbcccddd-64f90608b607>`_
567 * `A /path/b.js <#a_c-aaabbbcccddd-64f90608b607>`_
568 * `M /path/d.js <#a_c-aaabbbcccddd-85842bf30c6e>`_
568 * `M /path/d.js <#a_c-aaabbbcccddd-85842bf30c6e>`_
569 * `M /path/Δ™.py <#a_c-aaabbbcccddd-d713adf009cd>`_
569 * `M /path/Δ™.py <#a_c-aaabbbcccddd-d713adf009cd>`_
570 * `R /path/ΕΊ.py`
570 * `R /path/ΕΊ.py`
571
571
572 .. |under_review| replace:: *"NEW STATUS"*'''
572 .. |under_review| replace:: *"NEW STATUS"*'''
573
573
574 added = ['/path/a.py', '/path/b.js']
574 added = ['/path/a.py', '/path/b.js']
575 modified = ['/path/d.js', u'/path/Δ™.py']
575 modified = ['/path/d.js', u'/path/Δ™.py']
576 removed = [u'/path/ΕΊ.py']
576 removed = [u'/path/ΕΊ.py']
577
577
578 params = {
578 params = {
579 'under_review_label': 'NEW STATUS',
579 'under_review_label': 'NEW STATUS',
580 'added_commits': ['a'],
580 'added_commits': ['a'],
581 'removed_commits': ['a', 'b', 'c'],
581 'removed_commits': ['a', 'b', 'c'],
582 'changed_files': added + modified + removed,
582 'changed_files': added + modified + removed,
583 'added_files': added,
583 'added_files': added,
584 'modified_files': modified,
584 'modified_files': modified,
585 'removed_files': removed,
585 'removed_files': removed,
586 'ancestor_commit_id': 'aaabbbcccdddeee',
586 'ancestor_commit_id': 'aaabbbcccdddeee',
587 }
587 }
588 renderer = RstTemplateRenderer()
588 renderer = RstTemplateRenderer()
589 rendered = renderer.render('pull_request_update.mako', **params)
589 rendered = renderer.render('pull_request_update.mako', **params)
590
590
591 assert expected == rendered
591 assert expected == rendered
592
592
593
593
594 def test_render_rst_auto_status_template():
594 def test_render_rst_auto_status_template():
595 expected = u'''\
595 expected = u'''\
596 Auto status change to |new_status|
596 Auto status change to |new_status|
597
597
598 .. |new_status| replace:: *"NEW STATUS"*'''
598 .. |new_status| replace:: *"NEW STATUS"*'''
599
599
600 params = {
600 params = {
601 'new_status_label': 'NEW STATUS',
601 'new_status_label': 'NEW STATUS',
602 'pull_request': None,
602 'pull_request': None,
603 'commit_id': None,
603 'commit_id': None,
604 }
604 }
605 renderer = RstTemplateRenderer()
605 renderer = RstTemplateRenderer()
606 rendered = renderer.render('auto_status_change.mako', **params)
606 rendered = renderer.render('auto_status_change.mako', **params)
607 assert expected == rendered
607 assert expected == rendered
608
608
609
609
610 @pytest.mark.parametrize(
610 @pytest.mark.parametrize(
611 "src_path, server_path, is_path, expected",
611 "src_path, server_path, is_path, expected",
612 [
612 [
613 ('source.png', '/repo/files/path', lambda p: False,
613 ('source.png', '/repo/files/path', lambda p: False,
614 '/repo/files/path/source.png'),
614 '/repo/files/path/source.png'),
615
615
616 ('source.png', 'mk/git/blob/master/README.md', lambda p: True,
616 ('source.png', 'mk/git/blob/master/README.md', lambda p: True,
617 '/mk/git/blob/master/source.png'),
617 '/mk/git/blob/master/source.png'),
618
618
619 ('./source.png', 'mk/git/blob/master/README.md', lambda p: True,
619 ('./source.png', 'mk/git/blob/master/README.md', lambda p: True,
620 '/mk/git/blob/master/source.png'),
620 '/mk/git/blob/master/source.png'),
621
621
622 ('/source.png', 'mk/git/blob/master/README.md', lambda p: True,
622 ('/source.png', 'mk/git/blob/master/README.md', lambda p: True,
623 '/mk/git/blob/master/source.png'),
623 '/mk/git/blob/master/source.png'),
624
624
625 ('./source.png', 'repo/files/path/source.md', lambda p: True,
625 ('./source.png', 'repo/files/path/source.md', lambda p: True,
626 '/repo/files/path/source.png'),
626 '/repo/files/path/source.png'),
627
627
628 ('./source.png', '/repo/files/path/file.md', lambda p: True,
628 ('./source.png', '/repo/files/path/file.md', lambda p: True,
629 '/repo/files/path/source.png'),
629 '/repo/files/path/source.png'),
630
630
631 ('../source.png', '/repo/files/path/file.md', lambda p: True,
631 ('../source.png', '/repo/files/path/file.md', lambda p: True,
632 '/repo/files/source.png'),
632 '/repo/files/source.png'),
633
633
634 ('./../source.png', '/repo/files/path/file.md', lambda p: True,
634 ('./../source.png', '/repo/files/path/file.md', lambda p: True,
635 '/repo/files/source.png'),
635 '/repo/files/source.png'),
636
636
637 ('./source.png', '/repo/files/path/file.md', lambda p: True,
637 ('./source.png', '/repo/files/path/file.md', lambda p: True,
638 '/repo/files/path/source.png'),
638 '/repo/files/path/source.png'),
639
639
640 ('../../../source.png', 'path/file.md', lambda p: True,
640 ('../../../source.png', 'path/file.md', lambda p: True,
641 '/source.png'),
641 '/source.png'),
642
642
643 ('../../../../../source.png', '/path/file.md', None,
643 ('../../../../../source.png', '/path/file.md', None,
644 '/source.png'),
644 '/source.png'),
645
645
646 ('../../../../../source.png', 'files/path/file.md', None,
646 ('../../../../../source.png', 'files/path/file.md', None,
647 '/source.png'),
647 '/source.png'),
648
648
649 ('../../../../../https://google.com/image.png', 'files/path/file.md', None,
649 ('../../../../../https://google.com/image.png', 'files/path/file.md', None,
650 '/https://google.com/image.png'),
650 '/https://google.com/image.png'),
651
651
652 ('https://google.com/image.png', 'files/path/file.md', None,
652 ('https://google.com/image.png', 'files/path/file.md', None,
653 'https://google.com/image.png'),
653 'https://google.com/image.png'),
654
654
655 ('://foo', '/files/path/file.md', None,
655 ('://foo', '/files/path/file.md', None,
656 '://foo'),
656 '://foo'),
657
657
658 (u'ν•œκΈ€.png', '/files/path/file.md', None,
658 (u'ν•œκΈ€.png', '/files/path/file.md', None,
659 u'/files/path/ν•œκΈ€.png'),
659 u'/files/path/ν•œκΈ€.png'),
660
660
661 ('my custom image.png', '/files/path/file.md', None,
661 ('my custom image.png', '/files/path/file.md', None,
662 '/files/path/my custom image.png'),
662 '/files/path/my custom image.png'),
663 ])
663 ])
664 def test_relative_path(src_path, server_path, is_path, expected):
664 def test_relative_path(src_path, server_path, is_path, expected):
665 path = relative_path(src_path, server_path, is_path)
665 path = relative_path(src_path, server_path, is_path)
666 assert path == expected
666 assert path == expected
667
667
668
668
669 @pytest.mark.parametrize(
669 @pytest.mark.parametrize(
670 "src_html, expected_html",
670 "src_html, expected_html",
671 [
671 [
672 ('<div></div>', '<div></div>'),
672 ('<div></div>', '<div></div>'),
673 ('<img src="/file.png"></img>', '<img src="/path/raw/file.png">'),
673 ('<img src="/file.png"></img>', '<img src="/path/raw/file.png">'),
674 ('<img src="data:abcd"/>', '<img src="data:abcd">'),
674 ('<img src="data:abcd"/>', '<img src="data:abcd">'),
675 ('<a href="/file.png?raw=1"></a>', '<a href="/path/raw/file.png?raw=1"></a>'),
675 ('<a href="/file.png?raw=1"></a>', '<a href="/path/raw/file.png?raw=1"></a>'),
676 ('<a href="/file.png"></a>', '<a href="/path/file.png"></a>'),
676 ('<a href="/file.png"></a>', '<a href="/path/file.png"></a>'),
677 ('<a href="#anchor"></a>', '<a href="#anchor"></a>'),
677 ('<a href="#anchor"></a>', '<a href="#anchor"></a>'),
678 ('<a href="./README.md?raw=1"></a>', '<a href="/path/raw/README.md?raw=1"></a>'),
678 ('<a href="./README.md?raw=1"></a>', '<a href="/path/raw/README.md?raw=1"></a>'),
679 ('<a href="./README.md"></a>', '<a href="/path/README.md"></a>'),
679 ('<a href="./README.md"></a>', '<a href="/path/README.md"></a>'),
680 ('<a href="../README.md"></a>', '<a href="/README.md"></a>'),
680 ('<a href="../README.md"></a>', '<a href="/README.md"></a>'),
681
681
682 ])
682 ])
683 def test_relative_links(src_html, expected_html):
683 def test_relative_links(src_html, expected_html):
684 server_paths = {'raw': '/path/raw/file.md', 'standard': '/path/file.md'}
684 server_paths = {'raw': '/path/raw/file.md', 'standard': '/path/file.md'}
685 assert relative_links(src_html, server_paths=server_paths) == expected_html
685 assert relative_links(src_html, server_paths=server_paths) == expected_html
686
687
688 @pytest.mark.parametrize("notebook_source, expected_output", [
689 ("""
690 {
691 "nbformat": 3,
692 "nbformat_minor": 0,
693 "worksheets": [
694 {
695 "cells": [
696 {
697 "cell_type": "code",
698 "execution_count": 1,
699 "metadata": {},
700 "outputs": [
701 {
702 "name": "stdout",
703 "output_type": "stream",
704 "text": [
705 "Hello, World!\\n"
706 ]
707 }
708 ],
709 "input": "print('Hello, World!')"
710 }
711 ]
712 }
713 ],
714 "metadata": {
715 "kernelspec": {
716 "display_name": "Python 3",
717 "language": "python",
718 "name": "python3"
719 },
720 "language_info": {
721 "codemirror_mode": {
722 "name": "ipython",
723 "version": 3
724 },
725 "file_extension": ".py",
726 "mimetype": "text/x-python",
727 "name": "python",
728 "nbconvert_exporter": "python",
729 "pygments_lexer": "ipython3",
730 "version": "3.8.5"
731 }
732 }
733 }
734 """, "Hello, World!"),
735 ("""
736 {
737 "nbformat": 4,
738 "nbformat_minor": 1,
739 "cells": [
740 {
741 "cell_type": "code",
742 "execution_count": 1,
743 "metadata": {},
744 "outputs": [
745 {
746 "name": "stdout",
747 "output_type": "stream",
748 "text": [
749 "Hello, World!\\n"
750 ]
751 }
752 ],
753 "source": [
754 "print('Hello, World!')"
755 ]
756 }
757 ],
758 "metadata": {
759 "kernelspec": {
760 "display_name": "Python 3",
761 "language": "python",
762 "name": "python3"
763 },
764 "language_info": {
765 "codemirror_mode": {
766 "name": "ipython",
767 "version": 4
768 },
769 "file_extension": ".py",
770 "mimetype": "text/x-python",
771 "name": "python",
772 "nbconvert_exporter": "python",
773 "pygments_lexer": "ipython3",
774 "version": "3.9.1"
775 }
776 }
777 }
778 """, "Hello, World!")
779 ])
780 def test_jp_notebook_html_generation(notebook_source, expected_output):
781 import mock
782 with mock.patch('rhodecode.lib.helpers.asset'):
783 body = MarkupRenderer.jupyter(notebook_source)
784 assert "<!-- ## IPYTHON NOTEBOOK RENDERING ## -->" in body
785 assert expected_output in body
General Comments 0
You need to be logged in to leave comments. Login now