Repository: mkdocs/mkdocs

Issue:
Automatically refresh documentation when using `mkdocs serve`
This would make the eediting experience really neat. @ericholscher had an idea for a GUI app that could do this, but this approach might be enough.

One possible approach - inject some JavaScript that either polls or uses web sockets to be notified of an update.



Discussion:
sphinx-autobuild does this with websockets. It's pretty nifty.

On Fri, Oct 17, 2014 at 3:45 AM, Dougal Matthews notifications@github.com
wrote:

> This would make the eediting experience really neat. @ericholscher
> https://github.com/ericholscher had an idea for a GUI app that could do
> this, but this approach might be enough.
> 
> One possible approach - inject some JavaScript that either polls or uses
> web sockets to be notified of an update.
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/tomchristie/mkdocs/issues/163.

## 

Eric Holscher
Maker of the internet residing in Portland, Or
http://ericholscher.com

looks like sphinx-autobuild uses [python-livereload](https://github.com/lepture/python-livereload/), which in turn relies on [livereload](http://livereload.com/).
I tried my hands on livereload, pretty cool and it works simply from the command line (you need to install the extension for your browser first though)
Looking at the way sphinx-autobuild [does it](https://github.com/GaretJax/sphinx-autobuild/blob/develop/sphinx_autobuild/__init__.py) scares me a little. I went ahead to look at the source of `python-livereload` and figured that for our use case, it is a simple case of serving out of our temp build directory.
This was problematic due to an issue, [details here](https://github.com/lepture/python-livereload/issues/76), hopefully that is resolved soon and I will have a PR for this.

The author of `python-livereload` has [confirmed that this is a bug](https://github.com/lepture/python-livereload/issues/76#issuecomment-61436294), and proceeded to fix it up really quickly. The fix will propagate to pypi in [a couple of days](https://github.com/lepture/python-livereload/issues/76#issuecomment-61436570).

BTW. python-livereload has a built-in static server.

It would be a little off topic, maybe you will be interested in https://github.com/lepture/mistune. It is the fastest markdown parser in pure Python.

Intresting, I'll check out livereload but I really don't like the idea of requiring a plugin. I can't see why that is needed?

@lepture thanks, speed isn't an issue for us at the moment. It is interesting to know alternatives are available. Thankfully docs building doesn't need to scale far ;)

@d0ugal You don't need a plugin. This python-livereload will inject the javascript automatically.

Oh, sorry, I must have totally missread something. I can't even see where I got that from :) So yeah, we should totally look into trying that out. I'll have a go with it. Thanks.


Resolving patch:
diff --git a/mkdocs/config.py b/mkdocs/config.py
--- a/mkdocs/config.py
+++ b/mkdocs/config.py
@@ -78,6 +78,8 @@ def load_config(filename='mkdocs.yml', options=None):
     options = options or {}
     if 'config' in options:
         filename = options['config']
+    else:
+        options['config'] = filename
     if not os.path.exists(filename):
         raise ConfigurationError("Config file '%s' does not exist." % filename)
     with open(filename, 'r', encoding='utf-8') as fp:
diff --git a/mkdocs/serve.py b/mkdocs/serve.py
--- a/mkdocs/serve.py
+++ b/mkdocs/serve.py
@@ -1,72 +1,9 @@
-# coding: utf-8
-from __future__ import print_function
-
-from watchdog import events
-from watchdog.observers.polling import PollingObserver
-from mkdocs.build import build
-from mkdocs.compat import httpserver, socketserver, urlunquote
-from mkdocs.config import load_config
-import os
-import posixpath
-import shutil
-import sys
 import tempfile
 
+from livereload import Server
 
-class BuildEventHandler(events.FileSystemEventHandler):
-    """
-    Perform a rebuild when anything in the theme or docs directory changes.
-    """
-    def __init__(self, options):
-        super(BuildEventHandler, self).__init__()
-        self.options = options
-
-    def on_any_event(self, event):
-        if not isinstance(event, events.DirModifiedEvent):
-            print('Rebuilding documentation...', end='')
-            config = load_config(options=self.options)
-            build(config, live_server=True)
-            print(' done')
-
-
-class ConfigEventHandler(BuildEventHandler):
-    """
-    Perform a rebuild when the config file changes.
-    """
-    def on_any_event(self, event):
-        try:
-            if os.path.basename(event.src_path) == 'mkdocs.yml':
-                super(ConfigEventHandler, self).on_any_event(event)
-        except Exception as e:
-            print(e)
-
-
-class FixedDirectoryHandler(httpserver.SimpleHTTPRequestHandler):
-    """
-    Override the default implementation to allow us to specify the served
-    directory, instead of being hardwired to the current working directory.
-    """
-    base_dir = os.getcwd()
-
-    def translate_path(self, path):
-        # abandon query parameters
-        path = path.split('?', 1)[0]
-        path = path.split('#', 1)[0]
-        path = posixpath.normpath(urlunquote(path))
-        words = path.split('/')
-        words = filter(None, words)
-        path = self.base_dir
-        for word in words:
-            drive, word = os.path.splitdrive(word)
-            head, word = os.path.split(word)
-            if word in (os.curdir, os.pardir):
-                continue
-            path = os.path.join(path, word)
-        return path
-
-    def log_message(self, format, *args):
-        date_str = self.log_date_time_string()
-        sys.stderr.write('[%s] %s\n' % (date_str, format % args))
+from mkdocs.build import build
+from mkdocs.config import load_config
 
 
 def serve(config, options=None):
@@ -77,48 +14,20 @@ def serve(config, options=None):
     tempdir = tempfile.mkdtemp()
     options['site_dir'] = tempdir
 
-    # Only use user-friendly URLs when running the live server
-    options['use_directory_urls'] = True
+    def builder():
+        config = load_config(options=options)
+        build(config, live_server=True)
 
     # Perform the initial build
-    config = load_config(options=options)
-    build(config, live_server=True)
-
-    # Note: We pass any command-line options through so that we
-    #       can re-apply them if the config file is reloaded.
-    event_handler = BuildEventHandler(options)
-    config_event_handler = ConfigEventHandler(options)
-
-    # We could have used `Observer()`, which can be faster, but
-    # `PollingObserver()` works more universally.
-    observer = PollingObserver()
-    observer.schedule(event_handler, config['docs_dir'], recursive=True)
-    for theme_dir in config['theme_dir']:
-        if not os.path.exists(theme_dir):
-            continue
-        observer.schedule(event_handler, theme_dir, recursive=True)
-    observer.schedule(config_event_handler, '.')
-    observer.start()
+    builder()
 
-    class TCPServer(socketserver.TCPServer):
-        allow_reuse_address = True
+    server = Server()
 
-    class DocsDirectoryHandler(FixedDirectoryHandler):
-        base_dir = config['site_dir']
+    # Watch the documentation files, the config file and the theme files.
+    server.watch(config['docs_dir'], builder)
+    server.watch(config['config'], builder)
+    for d in config['theme_dir']:
+        server.watch(d, builder)
 
     host, port = config['dev_addr'].split(':', 1)
-    server = TCPServer((host, int(port)), DocsDirectoryHandler)
-
-    print('Running at: http://%s:%s/' % (host, port))
-    print('Live reload enabled.')
-    print('Hold ctrl+c to quit.')
-    try:
-        server.serve_forever()
-    except KeyboardInterrupt:
-        print('Stopping server...')
-
-    # Clean up
-    observer.stop()
-    observer.join()
-    shutil.rmtree(tempdir)
-    print('Quit complete')
+    server.serve(root=tempdir, host=host, port=port, restart_delay=0)
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -20,9 +20,9 @@
 install_requires = [
     'ghp-import>=0.4.1',
     'Jinja2>=2.7.1',
+    'livereload>=2.3.2',
     'Markdown>=2.3.1,<2.5' if PY26 else 'Markdown>=2.3.1',
     'PyYAML>=3.10',
-    'watchdog>=0.7.0',
 ]
 
 long_description = (


Test patch: