|
1 | 1 | from pandocfilters import toJSONFilter, Div, RawBlock, Para, Str, Space, Link, Code, CodeBlock
|
2 | 2 | import markdown
|
3 | 3 | import re
|
| 4 | +import html |
| 5 | +import markdown.inlinepatterns |
| 6 | +import os |
| 7 | +import nbformat as nbf |
4 | 8 |
|
5 |
| -def to_markdown(item): |
| 9 | +def to_markdown(item, skip_octicon=False): |
| 10 | + # A handler function to process strings, links, code, and code |
| 11 | + # blocks |
6 | 12 | if item['t'] == 'Str':
|
7 | 13 | return item['c']
|
8 | 14 | elif item['t'] == 'Space':
|
9 | 15 | return ' '
|
10 | 16 | elif item['t'] == 'Link':
|
11 |
| - # Assuming the link text is always in the first item |
12 |
| - return f"[{item['c'][1][0]['c']}]({item['c'][2][0]})" |
| 17 | + link_text = ''.join(to_markdown(i, skip_octicon) for i in item['c'][1]) |
| 18 | + return f'<a href="{item["c"][2][0]}">{link_text}</a>' |
13 | 19 | elif item['t'] == 'Code':
|
14 |
| - return f"`{item['c'][1]}`" |
| 20 | + # Need to remove icticon as they don't render in .ipynb |
| 21 | + if any(value == 'octicon' for key, value in item['c'][0][2]): |
| 22 | + return '' |
| 23 | + else: |
| 24 | + # Escape the code and wrap it in <code> tags |
| 25 | + return f'<code>{html.escape(item["c"][1])}</code>' |
15 | 26 | elif item['t'] == 'CodeBlock':
|
16 |
| - return f"```\n{item['c'][1]}\n```" |
| 27 | + # Escape the code block and wrap it in <pre><code> tags |
| 28 | + return f'<pre><code>{html.escape(item["c"][1])}</code></pre>' |
17 | 29 |
|
18 | 30 | def process_admonitions(key, value, format, meta):
|
| 31 | + # Replace admonitions with proper HTML. |
19 | 32 | if key == 'Div':
|
20 | 33 | [[ident, classes, keyvals], contents] = value
|
21 | 34 | if 'note' in classes:
|
@@ -49,20 +62,73 @@ def process_admonitions(key, value, format, meta):
|
49 | 62 | html_content = markdown.markdown(note_content_md)
|
50 | 63 |
|
51 | 64 | return [{'t': 'RawBlock', 'c': ['html', f'<div style="background-color: {color}; color: #fff; font-weight: 700; padding-left: 10px; padding-top: 5px; padding-bottom: 5px">{label}</div>']}, {'t': 'RawBlock', 'c': ['html', '<div style="background-color: #f3f4f7; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; padding-right: 10px">']}, {'t': 'RawBlock', 'c': ['html', html_content]}, {'t': 'RawBlock', 'c': ['html', '</div>']}]
|
52 |
| - |
53 | 65 | elif key == 'RawBlock':
|
| 66 | + # this is needed for the cells that have embedded video. |
| 67 | + # We add a special tag to those: ``` {python, .jupyter-code-cell} |
| 68 | + # The post-processing script then finds those and genrates separate |
| 69 | + # code cells that can load video. |
54 | 70 | [format, content] = value
|
55 | 71 | if format == 'html' and 'iframe' in content:
|
56 | 72 | # Extract the video URL
|
57 | 73 | video_url = content.split('src="')[1].split('"')[0]
|
58 | 74 | # Create the Python code to display the video
|
59 |
| - html_code = f""" |
| 75 | + python_code = f""" |
60 | 76 | from IPython.display import display, HTML
|
61 | 77 | html_code = \"""
|
62 | 78 | {content}
|
63 | 79 | \"""
|
64 | 80 | display(HTML(html_code))
|
65 | 81 | """
|
66 | 82 |
|
| 83 | + return {'t': 'CodeBlock', 'c': [['', ['python', 'jupyter-code-cell'], []], python_code]} |
| 84 | + |
| 85 | + |
| 86 | +def process_images(key, value, format, meta): |
| 87 | + # Add https://pytorch.org/tutorials/ to images so that they |
| 88 | + # load correctly in the notebook. |
| 89 | + if key == 'Image': |
| 90 | + [ident, classes, keyvals], caption, [src, title] = value |
| 91 | + if not src.startswith('http'): |
| 92 | + while src.startswith('../'): |
| 93 | + src = src[3:] |
| 94 | + if src.startswith('/_static'): |
| 95 | + src = src[1:] |
| 96 | + src = 'https://pytorch.org/tutorials/' + src |
| 97 | + return {'t': 'Image', 'c': [[ident, classes, keyvals], caption, [src, title]]} |
| 98 | + |
| 99 | +def process_grids(key, value, format, meta): |
| 100 | + # Generate side by side grid cards. Only for the two-cards layout |
| 101 | + # that we use in the tutorial template. |
| 102 | + if key == 'Div': |
| 103 | + [[ident, classes, keyvals], contents] = value |
| 104 | + if 'grid' in classes: |
| 105 | + columns = ['<div style="width: 45%; float: left; padding: 20px;">', |
| 106 | + '<div style="width: 45%; float: right; padding: 20px;">'] |
| 107 | + column_num = 0 |
| 108 | + for block in contents: |
| 109 | + if 't' in block and block['t'] == 'Div' and 'grid-item-card' in block['c'][0][1]: |
| 110 | + item_html = '' |
| 111 | + for item in block['c'][1]: |
| 112 | + if item['t'] == 'Para': |
| 113 | + item_html += '<h2>' + ''.join(to_markdown(i) for i in item['c']) + '</h2>' |
| 114 | + elif item['t'] == 'BulletList': |
| 115 | + item_html += '<ul>' |
| 116 | + for list_item in item['c']: |
| 117 | + item_html += '<li>' + ''.join(to_markdown(i) for i in list_item[0]['c']) + '</li>' |
| 118 | + item_html += '</ul>' |
| 119 | + columns[column_num] += item_html |
| 120 | + column_num = (column_num + 1) % 2 |
| 121 | + columns = [column + '</div>' for column in columns] |
| 122 | + return {'t': 'RawBlock', 'c': ['html', ''.join(columns)]} |
| 123 | + |
| 124 | +def is_code_block(item): |
| 125 | + return item['t'] == 'Code' and 'octicon' in item['c'][1] |
| 126 | +def process_all(key, value, format, meta): |
| 127 | + new_value = process_admonitions(key, value, format, meta) |
| 128 | + if new_value is None: |
| 129 | + new_value = process_images(key, value, format, meta) |
| 130 | + if new_value is None: |
| 131 | + new_value = process_grids(key, value, format, meta) |
| 132 | + return new_value |
67 | 133 | if __name__ == "__main__":
|
68 |
| - toJSONFilter(process_admonitions) |
| 134 | + toJSONFilter(process_all) |
0 commit comments