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