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