diff --git a/.travis.yml b/.travis.yml index 742511f..774f063 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,11 +4,9 @@ python: - 2.7 - 3.3 before_install: - - pip install jinja2 - easy_install -q pyzmq + - pip install jinja2 sphinx pygments tornado - sudo apt-get install pandoc - - pip install pygments - - pip install sphinx install: - python setup.py install -q script: diff --git a/IPython/nbconvert/nbconvertapp.py b/IPython/nbconvert/nbconvertapp.py index 08bc0b8..370b2b3 100755 --- a/IPython/nbconvert/nbconvertapp.py +++ b/IPython/nbconvert/nbconvertapp.py @@ -63,8 +63,7 @@ nbconvert_aliases.update({ 'writer' : 'NbConvertApp.writer_class', 'post': 'NbConvertApp.postprocessor_class', 'output': 'NbConvertApp.output_base', - 'offline-slides': 'RevealHelpPreprocessor.url_prefix', - 'slide-notes': 'RevealHelpPreprocessor.speaker_notes' + 'reveal-prefix': 'RevealHelpPreprocessor.url_prefix', }) nbconvert_flags = {} diff --git a/IPython/nbconvert/postprocessors/__init__.py b/IPython/nbconvert/postprocessors/__init__.py index 6cae641..9f954f3 100644 --- a/IPython/nbconvert/postprocessors/__init__.py +++ b/IPython/nbconvert/postprocessors/__init__.py @@ -1,3 +1,8 @@ from .base import PostProcessorBase from .pdf import PDFPostProcessor -from .serve import ServePostProcessor + +# protect against unavailable tornado +try: + from .serve import ServePostProcessor +except ImportError: + pass diff --git a/IPython/nbconvert/postprocessors/serve.py b/IPython/nbconvert/postprocessors/serve.py index 828f5f3..8a83419 100644 --- a/IPython/nbconvert/postprocessors/serve.py +++ b/IPython/nbconvert/postprocessors/serve.py @@ -1,6 +1,4 @@ -""" -Contains postprocessor for serving nbconvert output. -""" +"""PostProcessor for serving reveal.js HTML slideshows.""" #----------------------------------------------------------------------------- #Copyright (c) 2013, the IPython Development Team. # @@ -16,40 +14,94 @@ Contains postprocessor for serving nbconvert output. import os import webbrowser -from BaseHTTPServer import HTTPServer -from SimpleHTTPServer import SimpleHTTPRequestHandler +from tornado import web, ioloop, httpserver +from tornado.httpclient import AsyncHTTPClient -from IPython.utils.traitlets import Bool +from IPython.utils.traitlets import Bool, Unicode, Int from .base import PostProcessorBase #----------------------------------------------------------------------------- # Classes #----------------------------------------------------------------------------- + +class ProxyHandler(web.RequestHandler): + """handler the proxies requests from a local prefix to a CDN""" + @web.asynchronous + def get(self, prefix, url): + """proxy a request to a CDN""" + proxy_url = "/".join([self.settings['cdn'], url]) + client = self.settings['client'] + client.fetch(proxy_url, callback=self.finish_get) + + def finish_get(self, response): + """finish the request""" + # copy potentially relevant headers + for header in ["Content-Type", "Cache-Control", "Date", "Last-Modified", "Expires"]: + if header in response.headers: + self.set_header(header, response.headers[header]) + self.finish(response.body) + class ServePostProcessor(PostProcessorBase): - """Post processor designed to serve files""" + """Post processor designed to serve files + + Proxies reveal.js requests to a CDN if no local reveal.js is present + """ open_in_browser = Bool(True, config=True, - help="""Set to False to deactivate - the opening of the browser""") + help="""Should the browser be opened automatically?""" + ) + reveal_cdn = Unicode("https://cdn.jsdelivr.net/reveal.js/2.4.0", config=True, + help="""URL for reveal.js CDN.""" + ) + reveal_prefix = Unicode("reveal.js", config=True, help="URL prefix for reveal.js") + ip = Unicode("127.0.0.1", config=True, help="The IP address to listen on.") + port = Int(8000, config=True, help="port for the server to listen on.") def postprocess(self, input): - """ - Simple implementation to serve the build directory. - """ - + """Serve the build directory with a webserver.""" + dirname, filename = os.path.split(input) + handlers = [ + (r"/(.+)", web.StaticFileHandler, {'path' : dirname}), + (r"/", web.RedirectHandler, {"url": "/%s" % filename}) + ] + + if ('://' in self.reveal_prefix or self.reveal_prefix.startswith("//")): + # reveal specifically from CDN, nothing to do + pass + elif os.path.isdir(os.path.join(dirname, self.reveal_prefix)): + # reveal prefix exists + self.log.info("Serving local %s", self.reveal_prefix) + else: + self.log.info("Redirecting %s requests to %s", self.reveal_prefix, self.reveal_cdn) + handlers.insert(0, (r"/(%s)/(.*)" % self.reveal_prefix, ProxyHandler)) + + app = web.Application(handlers, + cdn=self.reveal_cdn, + client=AsyncHTTPClient(), + ) + # hook up tornado logging to our logger + from tornado import log + log.app_log = self.log + + http_server = httpserver.HTTPServer(app) + http_server.listen(self.port, address=self.ip) + url = "http://%s:%i/%s" % (self.ip, self.port, filename) + print("Serving your slides at %s" % url) + print("Use Control-C to stop this server") + if self.open_in_browser: + webbrowser.open(url, new=2) try: - dirname, filename = os.path.split(input) - if dirname: - os.chdir(dirname) - httpd = HTTPServer(('127.0.0.1', 8000), SimpleHTTPRequestHandler) - sa = httpd.socket.getsockname() - url = "http://" + sa[0] + ":" + str(sa[1]) + "/" + filename - if self.open_in_browser: - webbrowser.open(url, new=2) - print("Serving your slides on " + url) - print("Use Control-C to stop this server.") - httpd.serve_forever() + ioloop.IOLoop.instance().start() except KeyboardInterrupt: - print("The server is shut down.") + print("\nInterrupted") + +def main(path): + """allow running this module to serve the slides""" + server = ServePostProcessor() + server(path) + +if __name__ == '__main__': + import sys + main(sys.argv[1]) diff --git a/IPython/nbconvert/preprocessors/revealhelp.py b/IPython/nbconvert/preprocessors/revealhelp.py index ae1110e..d9e213f 100755 --- a/IPython/nbconvert/preprocessors/revealhelp.py +++ b/IPython/nbconvert/preprocessors/revealhelp.py @@ -24,15 +24,14 @@ from IPython.utils.traitlets import Unicode, Bool class RevealHelpPreprocessor(Preprocessor): - url_prefix = Unicode('//cdn.jsdelivr.net/reveal.js/2.4.0', - config=True, - help="""If you want to use a local reveal.js library, - use 'url_prefix':'reveal.js' in your config object.""") - - speaker_notes = Bool(False, - config=True, - help="""If you want to use the speaker notes - set this to True.""") + url_prefix = Unicode('reveal.js', config=True, + help="""The URL prefix for reveal.js. + This can be a a relative URL for a local copy of reveal.js, + or point to a CDN. + + For speaker notes to work, a local reveal.js prefix must be used. + """ + ) def preprocess(self, nb, resources): """ @@ -65,30 +64,4 @@ class RevealHelpPreprocessor(Preprocessor): if not isinstance(resources['reveal'], dict): resources['reveal'] = {} resources['reveal']['url_prefix'] = self.url_prefix - resources['reveal']['notes_prefix'] = self.url_prefix - - cdn = 'http://cdn.jsdelivr.net/reveal.js/2.4.0' - local = 'local' - html_path = 'plugin/notes/notes.html' - js_path = 'plugin/notes/notes.js' - - html_infile = os.path.join(cdn, html_path) - js_infile = os.path.join(cdn, js_path) - html_outfile = os.path.join(local, html_path) - js_outfile = os.path.join(local, js_path) - - if self.speaker_notes: - if 'outputs' not in resources: - resources['outputs'] = {} - resources['outputs'][html_outfile] = self.notes_helper(html_infile) - resources['outputs'][js_outfile] = self.notes_helper(js_infile) - resources['reveal']['notes_prefix'] = local - return nb, resources - - def notes_helper(self, infile): - """Helper function to get the content from an url.""" - - content = urllib2.urlopen(infile).read() - - return content diff --git a/IPython/nbconvert/templates/slides_reveal.tpl b/IPython/nbconvert/templates/slides_reveal.tpl index 72444c5..2581c4d 100644 --- a/IPython/nbconvert/templates/slides_reveal.tpl +++ b/IPython/nbconvert/templates/slides_reveal.tpl @@ -135,7 +135,7 @@ transition: Reveal.getQueryHash().transition || 'linear', // default/cube/page/c dependencies: [ { src: "{{resources.reveal.url_prefix}}/lib/js/classList.js", condition: function() { return !document.body.classList; } }, { src: "{{resources.reveal.url_prefix}}/plugin/highlight/highlight.js", async: true, callback: function() { hljs.initHighlightingOnLoad(); } }, -{ src: "{{resources.reveal.notes_prefix}}/plugin/notes/notes.js", async: true, condition: function() { return !!document.body.classList; } } +{ src: "{{resources.reveal.url_prefix}}/plugin/notes/notes.js", async: true, condition: function() { return !!document.body.classList; } } // { src: 'http://s7.addthis.com/js/300/addthis_widget.js', async: true}, ] }); diff --git a/IPython/testing/iptest.py b/IPython/testing/iptest.py index 2a20d3c..2fbb848 100644 --- a/IPython/testing/iptest.py +++ b/IPython/testing/iptest.py @@ -293,6 +293,8 @@ def make_exclude(): if not have['tornado']: exclusions.append(ipjoin('html')) + exclusions.append(ipjoin('nbconvert', 'post_processors', 'serve')) + exclusions.append(ipjoin('nbconvert', 'post_processors', 'tests', 'test_serve')) if not have['jinja2']: exclusions.append(ipjoin('html', 'notebookapp')) diff --git a/docs/source/interactive/nbconvert.rst b/docs/source/interactive/nbconvert.rst index 0423274..1789a62 100644 --- a/docs/source/interactive/nbconvert.rst +++ b/docs/source/interactive/nbconvert.rst @@ -61,14 +61,9 @@ The currently supported export formats are: * ``--to slides`` This generates a Reveal.js HTML slideshow. - It must be served by an HTTP server. The easiest way to get this is to add + It must be served by an HTTP server. The easiest way to do this is adding ``--post serve`` on the command-line. - If you want to use the speaker notes plugin, just add - ``--slide-notes=True`` on the command-line. - For low connectivity environments, you can use a local copy of the reveal.js library, - just add ``--offline-slides=reveal.js`` on the command-line, and do not forget to move - your downloaded ``reveal.js`` library to the same folder where your slides are located. - + * ``--to markdown`` Simple markdown output. Markdown cells are unaffected,