diff --git a/mkdocs/livereload/__init__.py b/mkdocs/livereload/__init__.py index deef55ba..37f4acc7 100644 --- a/mkdocs/livereload/__init__.py +++ b/mkdocs/livereload/__init__.py @@ -8,6 +8,7 @@ import pathlib import posixpath import re import socketserver +import string import threading import time import warnings @@ -16,6 +17,30 @@ import wsgiref.simple_server import watchdog.events import watchdog.observers.polling +_SCRIPT_TEMPLATE = """ +var livereload = function(epoch, requestId) { + var req = new XMLHttpRequest(); + req.onloadend = function() { + if (parseFloat(this.responseText) > epoch) { + location.reload(); + return; + } + var launchNext = livereload.bind(this, epoch, requestId); + if (this.status === 200) { + launchNext(); + } else { + setTimeout(launchNext, 3000); + } + }; + req.open("GET", "${mount_path}livereload/" + epoch + "/" + requestId); + req.send(); + + console.log('Enabled live reload'); +} +livereload(${epoch}, ${request_id}); +""" +_SCRIPT_TEMPLATE = string.Template(_SCRIPT_TEMPLATE) + class _LoggerAdapter(logging.LoggerAdapter): def process(self, msg, kwargs): @@ -176,26 +201,25 @@ class LiveReloadServer(socketserver.ThreadingMixIn, wsgiref.simple_server.WSGISe # https://github.com/bottlepy/bottle/blob/f9b1849db4/bottle.py#L984 path = environ["PATH_INFO"].encode("latin-1").decode("utf-8", "ignore") - m = re.fullmatch(r"/livereload/([0-9]+)/[0-9]+", path) - if m: - epoch = int(m[1]) - start_response("200 OK", [("Content-Type", "text/plain")]) - - def condition(): - return self._visible_epoch > epoch - - with self._epoch_cond: - if not condition(): - # Stall the browser, respond as soon as there's something new. - # If there's not, respond anyway after a minute. - self._log_poll_request(environ.get("HTTP_REFERER"), request_id=path) - self._epoch_cond.wait_for(condition, timeout=self.poll_response_timeout) - return [b"%d" % self._visible_epoch] - - if path == "/js/livereload.js": - file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "livereload.js") - elif path.startswith(self.mount_path): + if path.startswith(self.mount_path): rel_file_path = path[len(self.mount_path):] + + m = re.fullmatch(r"livereload/([0-9]+)/[0-9]+", rel_file_path) + if m: + epoch = int(m[1]) + start_response("200 OK", [("Content-Type", "text/plain")]) + + def condition(): + return self._visible_epoch > epoch + + with self._epoch_cond: + if not condition(): + # Stall the browser, respond as soon as there's something new. + # If there's not, respond anyway after a minute. + self._log_poll_request(environ.get("HTTP_REFERER"), request_id=path) + self._epoch_cond.wait_for(condition, timeout=self.poll_response_timeout) + return [b"%d" % self._visible_epoch] + if path.endswith("/"): rel_file_path += "index.html" # Prevent directory traversal - normalize the path. @@ -235,17 +259,20 @@ class LiveReloadServer(socketserver.ThreadingMixIn, wsgiref.simple_server.WSGISe ) return wsgiref.util.FileWrapper(file) - @classmethod - def _inject_js_into_html(cls, content, epoch): + def _inject_js_into_html(self, content, epoch): try: body_end = content.rindex(b"") except ValueError: body_end = len(content) # The page will reload if the livereload poller returns a newer epoch than what it knows. # The other timestamp becomes just a unique identifier for the initiating page. - return ( - b'%b%b' - % (content[:body_end], epoch, _timestamp(), content[body_end:]) + script = _SCRIPT_TEMPLATE.substitute( + mount_path=self.mount_path, epoch=epoch, request_id=_timestamp() + ) + return b"%b%b" % ( + content[:body_end], + script.encode(), + content[body_end:], ) @classmethod diff --git a/mkdocs/livereload/livereload.js b/mkdocs/livereload/livereload.js deleted file mode 100644 index a0f74365..00000000 --- a/mkdocs/livereload/livereload.js +++ /dev/null @@ -1,19 +0,0 @@ -function livereload(epoch, requestId) { - var req = new XMLHttpRequest(); - req.onloadend = function() { - if (parseFloat(this.responseText) > epoch) { - location.reload(); - return; - } - var launchNext = livereload.bind(this, epoch, requestId); - if (this.status === 200) { - launchNext(); - } else { - setTimeout(launchNext, 3000); - } - }; - req.open("GET", "/livereload/" + epoch + "/" + requestId); - req.send(); - - console.log('Enabled live reload'); -} diff --git a/mkdocs/tests/livereload_tests.py b/mkdocs/tests/livereload_tests.py index 6ec81fda..7fca958d 100644 --- a/mkdocs/tests/livereload_tests.py +++ b/mkdocs/tests/livereload_tests.py @@ -65,7 +65,7 @@ def do_request(server, content): SCRIPT_REGEX = ( - r'' + r'' ) @@ -334,23 +334,18 @@ class BuildTests(unittest.TestCase): _, output = do_request(server, "GET /%E6%B5%8B%E8%AF%952/index.html") self.assertRegex(output, fr"^bbb{SCRIPT_REGEX}$") - @tempdir() - def test_serves_js(self, site_dir): - with testing_server(site_dir) as server: - for mount_path in "/", "/sub/": - server.mount_path = mount_path - - headers, output = do_request(server, "GET /js/livereload.js") - self.assertIn("function livereload", output) - self.assertEqual(headers["_status"], "200 OK") - self.assertEqual(headers.get("content-type"), "application/javascript") - @tempdir() def test_serves_polling_instantly(self, site_dir): with testing_server(site_dir) as server: _, output = do_request(server, "GET /livereload/0/0") self.assertTrue(output.isdigit()) + @tempdir() + def test_serves_polling_from_mount_path(self, site_dir): + with testing_server(site_dir, mount_path="/test/f*o") as server: + _, output = do_request(server, "GET /test/f*o/livereload/0/0") + self.assertTrue(output.isdigit()) + @tempdir() @tempdir() def test_serves_polling_after_event(self, site_dir, docs_dir):