├── .gitignore
├── LICENSE
├── README.md
├── examples
├── full_width.py
└── start.py
├── handout
├── __init__.py
├── blocks.py
├── data
│ ├── favicon.ico
│ ├── highlight.css
│ ├── highlight.js
│ ├── marked.js
│ ├── script.js
│ └── style.css
├── handout.py
├── tests
│ └── test_handout.py
└── tools.py
└── setup.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Python
2 | __pycache__/
3 | .pytest_cache/
4 | *.py[cod]
5 |
6 | # Pip
7 | pip-selfcheck.json
8 | *.whl
9 | *.egg
10 | *.egg-info
11 |
12 | # Setuptools
13 | /build
14 | /dist
15 | *.eggs
16 |
17 | # Mac
18 | .DS_Store
19 |
20 | # Example
21 | /output
22 | /examples/output
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Python Handout
2 |
3 | [](https://pypi.python.org/pypi/handout/#history)
4 |
5 | Turn Python scripts into handouts with Markdown comments and inline figures. An
6 | alternative to Jupyter notebooks without hidden state that supports any text
7 | editor.
8 |
9 | | Code | Handout |
10 | | ---- | ------- |
11 | |  |  |
12 |
13 | ## Getting started
14 |
15 | You use Python Handout as a library inside a normal Python program:
16 |
17 | 1. Install via `pip3 install -U handout`.
18 | 2. Run your script via `python3 script.py`. (You can start with
19 | `examples/start.py` from the repository.)
20 | 3. Open `output/index.html` in your browser to view the result.
21 | 4. Iterate and refresh your browser.
22 |
23 | ## Features
24 |
25 | Create the handout via `doc = handout.Handout(outdir)` to access these features:
26 |
27 | | Feature | Example |
28 | | ------- | ------- |
29 | | Add [Markdown text][markdown] as triple-quote comments. | `"""Markdown text"""` |
30 | | Add text via `print()` syntax. | `doc.add_text('text:', variable)` |
31 | | Add image from array or url. | `doc.add_image(image, 'png', width=1)` |
32 | | Add video from array or url. | `doc.add_video(video, 'gif', fps=30, width=1)` |
33 | | Add matplotlib figure. | `doc.add_figure(fig, width=1)` |
34 | | Add custom HTML. | `doc.add_html(string)` |
35 | | Insert added items and save to ` "+e+"
\n'
32 |
33 |
34 | class Text(object):
35 |
36 | def __init__(self, lines=None):
37 | self._lines = lines or []
38 |
39 | def append(self, line):
40 | self._lines.append(line)
41 |
42 | def render(self):
43 | lines = '\n'.join(tools.strip_empty_lines(self._lines))
44 | if not lines:
45 | return ''
46 | lines = lines.replace("&", "&")
47 | lines = lines.replace("<", "<")
48 | lines = lines.replace(">", ">")
49 | return '' + lines + '
'
67 | return output
68 |
69 |
70 | class Video(object):
71 |
72 | def __init__(self, filename, width=None):
73 | self._filename = filename
74 | self._width = width
75 |
76 | def append(self, line):
77 | raise NotImplementedError()
78 |
79 | def render(self):
80 | output = ''
86 | return output
87 |
88 |
89 | class Message(object):
90 |
91 | def __init__(self, lines=None):
92 | self._lines = lines or []
93 |
94 | def append(self, line):
95 | self._lines.append(line)
96 |
97 | def render(self):
98 | lines = ''.join(self._lines)
99 | lines = lines.replace("&", "&")
100 | lines = lines.replace("<", "<")
101 | lines = lines.replace(">", ">")
102 | return ' \n'
103 |
--------------------------------------------------------------------------------
/handout/data/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danijar/handout/c5a357223d6b7f08fa67465104dbb01c0ea73822/handout/data/favicon.ico
--------------------------------------------------------------------------------
/handout/data/highlight.css:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | github.com style (c) Vasily Polovnyov
":j.tabReplace?n.replace(/\t/g,j.tabReplace):""}):e}function d(e,n,t){var r=n?y[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),-1===e.indexOf(r)&&a.push(r),a.join(" ").trim()}function h(e){var n,t,r,o,s,l=i(e);a(l)||(j.useBR?(n=document.createElementNS("http://www.w3.org/1999/xhtml","div"),n.innerHTML=e.innerHTML.replace(/\n/g,"").replace(/
/g,"\n")):n=e,s=n.textContent,r=l?f(l,s,!0):g(s),t=c(n),t.length&&(o=document.createElementNS("http://www.w3.org/1999/xhtml","div"),o.innerHTML=r.value,r.value=u(t,c(o),s)),r.value=p(r.value),e.innerHTML=r.value,e.className=d(e.className,l,r.language),e.result={language:r.language,re:r.r},r.second_best&&(e.second_best={language:r.second_best.language,re:r.second_best.r}))}function b(e){j=o(j,e)}function v(){if(!v.called){v.called=!0;var e=document.querySelectorAll("pre code");E.forEach.call(e,h)}}function m(){addEventListener("DOMContentLoaded",v,!1),addEventListener("load",v,!1)}function N(n,t){var r=L[n]=t(e);r.aliases&&r.aliases.forEach(function(e){y[e]=n})}function R(){return B(L)}function w(e){return e=(e||"").toLowerCase(),L[e]||L[y[e]]}function x(e){var n=w(e);return n&&!n.disableAutodetect}var E=[],B=Object.keys,L={},y={},k=/^(no-?highlight|plain|text)$/i,M=/\blang(?:uage)?-([\w-]+)\b/i,C=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,I=" ?/gm,""),this.token(s,t),this.tokens.push({type:"blockquote_end"});else if(s=this.rules.list.exec(e)){for(e=e.substring(s[0].length),a={type:"list_start",ordered:d=1<(i=s[2]).length,start:d?+i:"",loose:!1},this.tokens.push(a),n=!(h=[]),f=(s=s[0].match(this.rules.item)).length,u=0;u'+(n?e:c(e,!0))+"
\n":"
"},r.prototype.blockquote=function(e){return""+(n?e:c(e,!0))+"
\n"+e+"
\n"},r.prototype.html=function(e){return e},r.prototype.heading=function(e,t,n){return this.options.headerIds?"
\n":"
\n"},r.prototype.list=function(e,t,n){var r=t?"ol":"ul";return"<"+r+(t&&1!==n?' start="'+n+'"':"")+">\n"+e+""+r+">\n"},r.prototype.listitem=function(e){return"\n\n"+e+"\n"+t+"
\n"},r.prototype.tablerow=function(e){return"\n"+e+" \n"},r.prototype.tablecell=function(e,t){var n=t.header?"th":"td";return(t.align?"<"+n+' align="'+t.align+'">':"<"+n+">")+e+""+n+">\n"},r.prototype.strong=function(e){return""+e+""},r.prototype.em=function(e){return""+e+""},r.prototype.codespan=function(e){return""+e+"
"},r.prototype.br=function(){return this.options.xhtml?"
":"
"},r.prototype.del=function(e){return""+e+""},r.prototype.link=function(e,t,n){if(null===(e=i(this.options.sanitize,this.options.baseUrl,e)))return n;var r='"+n+""},r.prototype.image=function(e,t,n){if(null===(e=i(this.options.sanitize,this.options.baseUrl,e)))return n;var r='":">"},r.prototype.text=function(e){return e},s.prototype.strong=s.prototype.em=s.prototype.codespan=s.prototype.del=s.prototype.text=function(e){return e},s.prototype.link=s.prototype.image=function(e,t,n){return""+n},s.prototype.br=function(){return""},p.parse=function(e,t){return new p(t).parse(e)},p.prototype.parse=function(e){this.inline=new h(e.links,this.options),this.inlineText=new h(e.links,f({},this.options,{renderer:new s})),this.tokens=e.reverse();for(var t="";this.next();)t+=this.tok();return t},p.prototype.next=function(){return this.token=this.tokens.pop()},p.prototype.peek=function(){return this.tokens[this.tokens.length-1]||0},p.prototype.parseText=function(){for(var e=this.token.text;"text"===this.peek().type;)e+="\n"+this.next().text;return this.inline.output(e)},p.prototype.tok=function(){switch(this.token.type){case"space":return"";case"hr":return this.renderer.hr();case"heading":return this.renderer.heading(this.inline.output(this.token.text),this.token.depth,u(this.inlineText.output(this.token.text)));case"code":return this.renderer.code(this.token.text,this.token.lang,this.token.escaped);case"table":var e,t,n,r,s="",i="";for(n="",e=0;e
"+c(e.message+"",!0)+"";throw e}}g.exec=g,d.options=d.setOptions=function(e){return f(d.defaults,e),d},d.getDefaults=function(){return{baseUrl:null,breaks:!1,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:new r,sanitize:!1,sanitizer:null,silent:!1,smartLists:!1,smartypants:!1,tables:!0,xhtml:!1}},d.defaults=d.getDefaults(),d.Parser=p,d.parser=p.parse,d.Renderer=r,d.TextRenderer=s,d.Lexer=a,d.lexer=a.lex,d.InlineLexer=h,d.inlineLexer=h.output,d.parse=d,"undefined"!=typeof module&&"object"==typeof exports?module.exports=d:"function"==typeof define&&define.amd?define(function(){return d}):e.marked=d}(this||("undefined"!=typeof window?window:global)); -------------------------------------------------------------------------------- /handout/data/script.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function(){ 2 | var elements = document.getElementsByClassName('markdown'); 3 | for (var index = 0; index < elements.length; index++) { 4 | var element = elements[index]; 5 | element.innerHTML = marked(element.textContent); 6 | } 7 | }, false); 8 | -------------------------------------------------------------------------------- /handout/data/style.css: -------------------------------------------------------------------------------- 1 | @media print { 2 | html, article { 3 | background: #fff !important; 4 | margin: 0 !important; 5 | padding: 0 !important; 6 | max-width: none !important; 7 | border: none !important; 8 | font-size: 13px !important; 9 | } 10 | article { 11 | /* This prevents page breaks within images. However, it also means images 12 | * in one line will be centered without margin rather than spaced evenly. */ 13 | display: block !important; 14 | } 15 | } 16 | 17 | html { 18 | background: #eee; 19 | } 20 | 21 | h1 { font-size: 2.0em; margin: 0.4em 0; } 22 | h2 { font-size: 1.5em; margin: 0.4em 0; } 23 | h3 { font-size: 1.3em; margin: 0.4em 0; } 24 | h4 { font-size: 1.2em; margin: 0.4em 0; } 25 | h5 { font-size: 1.1em; margin: 0.4em 0; } 26 | h6 { font-size: 1.0em; margin: 0.4em 0; } 27 | 28 | article { 29 | max-width: 48.5em; /* Fit 79 characters. */ 30 | margin: 2em auto; 31 | padding: 3em; 32 | text-align: center; 33 | font-size: 15px; 34 | font-family: Helvetica, sans-serif; 35 | background: #fff; 36 | border-radius: .3em; 37 | border: 1px solid #ddd; 38 | display: flex; 39 | flex-wrap: wrap; 40 | justify-content: space-around; 41 | } 42 | 43 | article > * { 44 | text-align: left; 45 | } 46 | 47 | article > div, article > pre, article > .message { 48 | width: 100%; 49 | } 50 | 51 | article > div p, article > pre, article > .message { 52 | margin: 0 0 1em; 53 | } 54 | 55 | article > .message + .message { 56 | margin-top: -1em; 57 | } 58 | 59 | code { 60 | text-align: left; 61 | border-radius: .25em; 62 | border: 1px solid #eee; 63 | } 64 | 65 | .message { 66 | white-space: pre-wrap; 67 | word-break: break-all; 68 | } 69 | 70 | img, video { 71 | display: inline-block; 72 | margin: 0 0 1em; 73 | max-width: 100%; 74 | } 75 | -------------------------------------------------------------------------------- /handout/handout.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import inspect 3 | import io 4 | import logging 5 | import pathlib 6 | import shutil 7 | 8 | from handout import blocks 9 | 10 | 11 | MATHJAX_SCRIPTS = ( 12 | '' 15 | '' 17 | ) 18 | 19 | 20 | class Handout(object): 21 | 22 | def __init__(self, directory, title='Handout'): 23 | self._directory = pathlib.Path(directory).expanduser() 24 | self._directory.mkdir(parents=True, exist_ok=True) 25 | self._title = title 26 | self._blocks = collections.defaultdict(list) 27 | self._pending = [] 28 | # The logger is configured in handout/__init__.py to make it available 29 | # right after importing the handout package. This allows the user to change 30 | # the logging level, message format, etc. 31 | self._logger = logging.getLogger('handout') 32 | for info in inspect.stack(): 33 | if info.filename == __file__: 34 | continue 35 | break 36 | module = inspect.getmodule(info.frame) 37 | self._source_name = info.filename 38 | self._source_text = inspect.getsource(module) 39 | self._num_images = 0 40 | self._num_videos = 0 41 | self._num_figures = 0 42 | 43 | def add_text(self, *args, **kwargs): 44 | show = kwargs.pop('show', False) 45 | stream = io.StringIO() 46 | kwargs['file'] = stream 47 | print(*args, **kwargs) # Print into custom stream. 48 | message = stream.getvalue() 49 | block = blocks.Message([message]) 50 | self._pending.append(block) 51 | # Remove up to one line break since the logger adds one. 52 | if message.endswith('\n'): 53 | message = message[:-1] 54 | self._logger.info(message) 55 | 56 | def add_image(self, image, format='png', width=None): 57 | if isinstance(image, str): 58 | filename = image 59 | else: 60 | import imageio 61 | filename = 'image-{}.{}'.format(self._num_images, format) 62 | imageio.imsave(self._directory / filename, image) 63 | self._logger.info('Saved image: {}'.format(filename)) 64 | block = blocks.Image(filename, width) 65 | self._pending.append(block) 66 | self._num_images += 1 67 | 68 | def add_video(self, video, format='gif', fps=30, width=None): 69 | if isinstance(video, str): 70 | filename = video 71 | else: 72 | import imageio 73 | filename = 'video-{}.{}'.format(self._num_videos, format) 74 | imageio.mimsave(self._directory / filename, video, fps=fps) 75 | self._logger.info('Saved video: {}'.format(filename)) 76 | if filename.endswith('.gif'): 77 | block = blocks.Image(filename, width) 78 | else: 79 | block = blocks.Video(filename, width) 80 | self._pending.append(block) 81 | self._num_videos += 1 82 | 83 | def add_html(self, string): 84 | block = blocks.Html([string]) 85 | self._pending.append(block) 86 | self._logger.info(string) 87 | 88 | def add_figure(self, figure, width=None): 89 | filename = 'figure-{}.png'.format(self._num_figures) 90 | block = blocks.Image(filename, width) 91 | self._pending.append(block) 92 | filename = self._directory / filename 93 | figure.savefig(filename) 94 | self._logger.info('Saved figure: {}'.format(filename)) 95 | self._num_figures += 1 96 | 97 | def show(self): 98 | self._blocks[self._get_current_line()] += self._pending 99 | self._pending = [] 100 | output = self._generate(self._source_text) 101 | filename = self._directory / 'index.html' 102 | with open(str(filename), 'w') as f: 103 | f.write(output) 104 | datadir = pathlib.Path(__file__).parent / 'data' 105 | for source in datadir.glob('**/*'): 106 | target = self._directory / source.relative_to(datadir) 107 | if source.is_dir() or target.exists(): 108 | continue 109 | target.parent.mkdir(exist_ok=True) 110 | shutil.copyfile(str(source), str(target)) 111 | self._logger.info("Handout written to: {}".format(filename)) 112 | 113 | def _generate(self, source): 114 | content = [] 115 | content.append(blocks.Html([ 116 | '', 117 | '', 118 | '', 119 | '