notebook.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. """
  2. Notebook Tag
  3. ------------
  4. This is a liquid-style tag to include a static html rendering of an IPython
  5. notebook in a blog post.
  6. Syntax
  7. ------
  8. {% notebook filename.ipynb [ cells[start:end] ]%}
  9. The file should be specified relative to the ``notebooks`` subdirectory of the
  10. content directory. Optionally, this subdirectory can be specified in the
  11. config file:
  12. NOTEBOOK_DIR = 'notebooks'
  13. The cells[start:end] statement is optional, and can be used to specify which
  14. block of cells from the notebook to include.
  15. Requirements
  16. ------------
  17. - The plugin requires IPython version 1.0 or above. It no longer supports the
  18. standalone nbconvert package, which has been deprecated.
  19. Details
  20. -------
  21. Because the notebook relies on some rather extensive custom CSS, the use of
  22. this plugin requires additional CSS to be inserted into the blog theme.
  23. After typing "make html" when using the notebook tag, a file called
  24. ``_nb_header.html`` will be produced in the main directory. The content
  25. of the file should be included in the header of the theme. An easy way
  26. to accomplish this is to add the following lines within the header template
  27. of the theme you use:
  28. {% if EXTRA_HEADER %}
  29. {{ EXTRA_HEADER }}
  30. {% endif %}
  31. and in your ``pelicanconf.py`` file, include the line:
  32. EXTRA_HEADER = open('_nb_header.html').read().decode('utf-8')
  33. this will insert the appropriate CSS. All efforts have been made to ensure
  34. that this CSS will not override formats within the blog theme, but there may
  35. still be some conflicts.
  36. """
  37. import re
  38. import os
  39. from functools import partial
  40. from .mdx_liquid_tags import LiquidTags
  41. import IPython
  42. IPYTHON_VERSION = IPython.version_info[0]
  43. try:
  44. import nbformat
  45. except:
  46. pass
  47. if not IPYTHON_VERSION >= 1:
  48. raise ValueError("IPython version 1.0+ required for notebook tag")
  49. try:
  50. from nbconvert.filters.highlight import _pygments_highlight
  51. except ImportError:
  52. try:
  53. from IPython.nbconvert.filters.highlight import _pygments_highlight
  54. except ImportError:
  55. # IPython < 2.0
  56. from IPython.nbconvert.filters.highlight import _pygment_highlight as _pygments_highlight
  57. from pygments.formatters import HtmlFormatter
  58. try:
  59. from nbconvert.exporters import HTMLExporter
  60. except ImportError:
  61. from IPython.nbconvert.exporters import HTMLExporter
  62. try:
  63. from traitlets.config import Config
  64. except ImportError:
  65. from IPython.config import Config
  66. try:
  67. from nbconvert.preprocessors import Preprocessor
  68. except ImportError:
  69. try:
  70. from IPython.nbconvert.preprocessors import Preprocessor
  71. except ImportError:
  72. # IPython < 2.0
  73. from IPython.nbconvert.transformers import Transformer as Preprocessor
  74. try:
  75. from traitlets import Integer
  76. except ImportError:
  77. from IPython.utils.traitlets import Integer
  78. from copy import deepcopy
  79. #----------------------------------------------------------------------
  80. # Some code that will be added to the header:
  81. # Some of the following javascript/css include is adapted from
  82. # IPython/nbconvert/templates/fullhtml.tpl, while some are custom tags
  83. # specifically designed to make the results look good within the
  84. # pelican-octopress theme.
  85. JS_INCLUDE = r"""
  86. <style type="text/css">
  87. /* Overrides of notebook CSS for static HTML export */
  88. div.entry-content {
  89. overflow: visible;
  90. padding: 8px;
  91. }
  92. .input_area {
  93. padding: 0.2em;
  94. }
  95. a.heading-anchor {
  96. white-space: normal;
  97. }
  98. .rendered_html
  99. code {
  100. font-size: .8em;
  101. }
  102. pre.ipynb {
  103. color: black;
  104. background: #f7f7f7;
  105. border: none;
  106. box-shadow: none;
  107. margin-bottom: 0;
  108. padding: 0;
  109. margin: 0px;
  110. font-size: 13px;
  111. }
  112. /* remove the prompt div from text cells */
  113. div.text_cell .prompt {
  114. display: none;
  115. }
  116. /* remove horizontal padding from text cells, */
  117. /* so it aligns with outer body text */
  118. div.text_cell_render {
  119. padding: 0.5em 0em;
  120. }
  121. img.anim_icon{padding:0; border:0; vertical-align:middle; -webkit-box-shadow:none; -box-shadow:none}
  122. div.collapseheader {
  123. width=100%;
  124. background-color:#d3d3d3;
  125. padding: 2px;
  126. cursor: pointer;
  127. font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;
  128. }
  129. </style>
  130. <script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" type="text/javascript"></script>
  131. <script type="text/javascript">
  132. init_mathjax = function() {
  133. if (window.MathJax) {
  134. // MathJax loaded
  135. MathJax.Hub.Config({
  136. tex2jax: {
  137. inlineMath: [ ['$','$'], ["\\(","\\)"] ],
  138. displayMath: [ ['$$','$$'], ["\\[","\\]"] ]
  139. },
  140. displayAlign: 'left', // Change this to 'center' to center equations.
  141. "HTML-CSS": {
  142. styles: {'.MathJax_Display': {"margin": 0}}
  143. }
  144. });
  145. MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
  146. }
  147. }
  148. init_mathjax();
  149. </script>
  150. <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
  151. <script type="text/javascript">
  152. jQuery(document).ready(function($) {
  153. $("div.collapseheader").click(function () {
  154. $header = $(this).children("span").first();
  155. $codearea = $(this).children(".input_area");
  156. console.log($(this).children());
  157. $codearea.slideToggle(500, function () {
  158. $header.text(function () {
  159. return $codearea.is(":visible") ? "Collapse Code" : "Expand Code";
  160. });
  161. });
  162. });
  163. });
  164. </script>
  165. """
  166. CSS_WRAPPER = """
  167. <style type="text/css">
  168. {0}
  169. </style>
  170. """
  171. #----------------------------------------------------------------------
  172. # Create a custom preprocessor
  173. class SliceIndex(Integer):
  174. """An integer trait that accepts None"""
  175. default_value = None
  176. def validate(self, obj, value):
  177. if value is None:
  178. return value
  179. else:
  180. return super(SliceIndex, self).validate(obj, value)
  181. class SubCell(Preprocessor):
  182. """A transformer to select a slice of the cells of a notebook"""
  183. start = SliceIndex(0, config=True,
  184. help="first cell of notebook to be converted")
  185. end = SliceIndex(None, config=True,
  186. help="last cell of notebook to be converted")
  187. def preprocess(self, nb, resources):
  188. nbc = deepcopy(nb)
  189. if IPYTHON_VERSION < 3:
  190. for worksheet in nbc.worksheets:
  191. cells = worksheet.cells[:]
  192. worksheet.cells = cells[self.start:self.end]
  193. else:
  194. nbc.cells = nbc.cells[self.start:self.end]
  195. return nbc, resources
  196. call = preprocess # IPython < 2.0
  197. #----------------------------------------------------------------------
  198. # Custom highlighter:
  199. # instead of using class='highlight', use class='highlight-ipynb'
  200. def custom_highlighter(source, language='ipython', metadata=None):
  201. formatter = HtmlFormatter(cssclass='highlight-ipynb')
  202. if not language:
  203. language = 'ipython'
  204. output = _pygments_highlight(source, formatter, language)
  205. return output.replace('<pre>', '<pre class="ipynb">')
  206. #----------------------------------------------------------------------
  207. # Below is the pelican plugin code.
  208. #
  209. SYNTAX = "{% notebook /path/to/notebook.ipynb [ cells[start:end] ] [ language[language] ] %}"
  210. FORMAT = re.compile(r"""^(\s+)?(?P<src>\S+)(\s+)?((cells\[)(?P<start>-?[0-9]*):(?P<end>-?[0-9]*)(\]))?(\s+)?((language\[)(?P<language>-?[a-z0-9\+\-]*)(\]))?(\s+)?$""")
  211. @LiquidTags.register('notebook')
  212. def notebook(preprocessor, tag, markup):
  213. match = FORMAT.search(markup)
  214. if match:
  215. argdict = match.groupdict()
  216. src = argdict['src']
  217. start = argdict['start']
  218. end = argdict['end']
  219. language = argdict['language']
  220. else:
  221. raise ValueError("Error processing input, "
  222. "expected syntax: {0}".format(SYNTAX))
  223. if start:
  224. start = int(start)
  225. else:
  226. start = 0
  227. if end:
  228. end = int(end)
  229. else:
  230. end = None
  231. language_applied_highlighter = partial(custom_highlighter, language=language)
  232. nb_dir = preprocessor.configs.getConfig('NOTEBOOK_DIR')
  233. nb_path = os.path.join('content', nb_dir, src)
  234. if not os.path.exists(nb_path):
  235. raise ValueError("File {0} could not be found".format(nb_path))
  236. # Create the custom notebook converter
  237. c = Config({'CSSHTMLHeaderTransformer':
  238. {'enabled':True, 'highlight_class':'.highlight-ipynb'},
  239. 'SubCell':
  240. {'enabled':True, 'start':start, 'end':end}})
  241. template_file = 'basic'
  242. if IPYTHON_VERSION >= 3:
  243. if os.path.exists('pelicanhtml_3.tpl'):
  244. template_file = 'pelicanhtml_3'
  245. elif IPYTHON_VERSION == 2:
  246. if os.path.exists('pelicanhtml_2.tpl'):
  247. template_file = 'pelicanhtml_2'
  248. else:
  249. if os.path.exists('pelicanhtml_1.tpl'):
  250. template_file = 'pelicanhtml_1'
  251. if IPYTHON_VERSION >= 2:
  252. subcell_kwarg = dict(preprocessors=[SubCell])
  253. else:
  254. subcell_kwarg = dict(transformers=[SubCell])
  255. exporter = HTMLExporter(config=c,
  256. template_file=template_file,
  257. filters={'highlight2html': language_applied_highlighter},
  258. **subcell_kwarg)
  259. # read and parse the notebook
  260. with open(nb_path) as f:
  261. nb_text = f.read()
  262. if IPYTHON_VERSION < 3:
  263. nb_json = IPython.nbformat.current.reads_json(nb_text)
  264. else:
  265. try:
  266. nb_json = nbformat.reads(nb_text, as_version=4)
  267. except:
  268. nb_json = IPython.nbformat.reads(nb_text, as_version=4)
  269. (body, resources) = exporter.from_notebook_node(nb_json)
  270. # if we haven't already saved the header, save it here.
  271. if not notebook.header_saved:
  272. print ("\n ** Writing styles to _nb_header.html: "
  273. "this should be included in the theme. **\n")
  274. header = '\n'.join(CSS_WRAPPER.format(css_line)
  275. for css_line in resources['inlining']['css'])
  276. header += JS_INCLUDE
  277. with open('_nb_header.html', 'w') as f:
  278. f.write(header)
  279. notebook.header_saved = True
  280. # this will stash special characters so that they won't be transformed
  281. # by subsequent processes.
  282. body = preprocessor.configs.htmlStash.store(body, safe=True)
  283. return body
  284. notebook.header_saved = False
  285. #----------------------------------------------------------------------
  286. # This import allows notebook to be a Pelican plugin
  287. from liquid_tags import register