serve.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. """PostProcessor for serving reveal.js HTML slideshows."""
  2. # Copyright (c) Jupyter Development Team.
  3. # Distributed under the terms of the Modified BSD License.
  4. from __future__ import annotations
  5. import os
  6. import threading
  7. import typing as t
  8. import webbrowser
  9. from tornado import gen, httpserver, ioloop, log, web
  10. from tornado.httpclient import AsyncHTTPClient
  11. from traitlets import Bool, Int, Unicode
  12. from .base import PostProcessorBase
  13. class ProxyHandler(web.RequestHandler):
  14. """handler the proxies requests from a local prefix to a CDN"""
  15. @gen.coroutine
  16. def get(self, prefix, url):
  17. """proxy a request to a CDN"""
  18. proxy_url = "/".join([self.settings["cdn"], url])
  19. client = self.settings["client"]
  20. response = yield client.fetch(proxy_url)
  21. for header in ["Content-Type", "Cache-Control", "Date", "Last-Modified", "Expires"]:
  22. if header in response.headers:
  23. self.set_header(header, response.headers[header])
  24. self.finish(response.body)
  25. class ServePostProcessor(PostProcessorBase):
  26. """Post processor designed to serve files
  27. Proxies reveal.js requests to a CDN if no local reveal.js is present
  28. """
  29. open_in_browser = Bool(True, help="""Should the browser be opened automatically?""").tag(
  30. config=True
  31. )
  32. browser = Unicode(
  33. "",
  34. help="""Specify what browser should be used to open slides. See
  35. https://docs.python.org/3/library/webbrowser.html#webbrowser.register
  36. to see how keys are mapped to browser executables. If
  37. not specified, the default browser will be determined
  38. by the `webbrowser`
  39. standard library module, which allows setting of the BROWSER
  40. environment variable to override it.
  41. """,
  42. ).tag(config=True)
  43. reveal_cdn = Unicode(
  44. "https://cdnjs.cloudflare.com/ajax/libs/reveal.js/3.5.0", help="""URL for reveal.js CDN."""
  45. ).tag(config=True)
  46. reveal_prefix = Unicode("reveal.js", help="URL prefix for reveal.js").tag(config=True)
  47. ip = Unicode("127.0.0.1", help="The IP address to listen on.").tag(config=True)
  48. port = Int(8000, help="port for the server to listen on.").tag(config=True)
  49. def postprocess(self, input):
  50. """Serve the build directory with a webserver."""
  51. dirname, filename = os.path.split(input)
  52. handlers: list[tuple[t.Any, ...]] = [
  53. (r"/(.+)", web.StaticFileHandler, {"path": dirname}),
  54. (r"/", web.RedirectHandler, {"url": "/%s" % filename}),
  55. ]
  56. if "://" in self.reveal_prefix or self.reveal_prefix.startswith("//"):
  57. # reveal specifically from CDN, nothing to do
  58. pass
  59. elif os.path.isdir(os.path.join(dirname, self.reveal_prefix)):
  60. # reveal prefix exists
  61. self.log.info("Serving local %s", self.reveal_prefix)
  62. else:
  63. self.log.info("Redirecting %s requests to %s", self.reveal_prefix, self.reveal_cdn)
  64. handlers.insert(0, (r"/(%s)/(.*)" % self.reveal_prefix, ProxyHandler))
  65. app = web.Application(
  66. handlers,
  67. cdn=self.reveal_cdn,
  68. client=AsyncHTTPClient(),
  69. )
  70. # hook up tornado logging to our logger
  71. log.app_log = self.log
  72. http_server = httpserver.HTTPServer(app)
  73. http_server.listen(self.port, address=self.ip)
  74. url = "http://%s:%i/%s" % (self.ip, self.port, filename)
  75. print("Serving your slides at %s" % url)
  76. print("Use Control-C to stop this server")
  77. if self.open_in_browser:
  78. try:
  79. browser = webbrowser.get(self.browser or None)
  80. b = lambda: browser.open(url, new=2) # noqa: E731
  81. threading.Thread(target=b).start()
  82. except webbrowser.Error as e:
  83. self.log.warning("No web browser found: %s.", e)
  84. browser = None
  85. try:
  86. ioloop.IOLoop.instance().start()
  87. except KeyboardInterrupt:
  88. print("\nInterrupted")
  89. def main(path):
  90. """allow running this module to serve the slides"""
  91. server = ServePostProcessor()
  92. server(path)
  93. if __name__ == "__main__":
  94. import sys
  95. main(sys.argv[1])