diff --git a/docs/about/release-notes.md b/docs/about/release-notes.md index 65e96358..a0cda862 100644 --- a/docs/about/release-notes.md +++ b/docs/about/release-notes.md @@ -12,7 +12,7 @@ You can determine your currently installed version using `pip freeze`: pip freeze | grep mkdocs -## Version 0.12.0 (2015-04-08) +## Version 0.12.0 (2015-04-14) * Display the current MkDocs version in the CLI output. (#258) * Check for CNAME file when using gh-deploy. (#285) @@ -26,6 +26,7 @@ You can determine your currently installed version using `pip freeze`: * Add the option to specify a remote branch when deploying to GitHub. This enables deploying to GitHub pages on personal and repo sites. (#354) * Add favicon support to the ReadTheDocs theme HTML. (#422) +* Automatically refresh the browser when files are edited. (#163) * Bugfix: Never re-write URL's in code blocks. (#240) * Bugfix: Don't copy ditfiles when copying media from the `docs_dir`. (#254) * Bugfix: Fix the rendering of tables in the ReadTheDocs theme. (#106) diff --git a/mkdocs/build.py b/mkdocs/build.py index 412aff32..87e88e83 100644 --- a/mkdocs/build.py +++ b/mkdocs/build.py @@ -63,9 +63,9 @@ def get_global_context(nav, config): page_description = config['site_description'] - extra_javascript = utils.create_media_urls(nav=nav, url_list=config['extra_javascript']) + extra_javascript = utils.create_media_urls(nav, config['extra_javascript']) - extra_css = utils.create_media_urls(nav=nav, url_list=config['extra_css']) + extra_css = utils.create_media_urls(nav, config['extra_css']) return { 'site_name': site_name, diff --git a/mkdocs/compat.py b/mkdocs/compat.py index 518a4937..05b2ed03 100644 --- a/mkdocs/compat.py +++ b/mkdocs/compat.py @@ -8,6 +8,7 @@ if PY2: from urlparse import urljoin, urlparse, urlunparse import urllib urlunquote = urllib.unquote + from urllib import pathname2url import SimpleHTTPServer as httpserver httpserver = httpserver @@ -25,6 +26,7 @@ if PY2: basestring = basestring else: # PY3 from urllib.parse import urljoin, urlparse, urlunparse, unquote + from urllib.request import pathname2url urlunquote = unquote import http.server as httpserver diff --git a/mkdocs/config.py b/mkdocs/config.py index 787fdd36..99171ced 100644 --- a/mkdocs/config.py +++ b/mkdocs/config.py @@ -1,14 +1,16 @@ # coding: utf-8 +from io import open +import logging +import ntpath +import os + +import yaml + from mkdocs import utils from mkdocs.compat import urlparse from mkdocs.exceptions import ConfigurationError -import logging -import os -import yaml -from io import open - log = logging.getLogger(__name__) DEFAULT_CONFIG = { @@ -78,6 +80,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: @@ -116,10 +120,10 @@ def validate_config(user_config): pages = [] extra_css = [] extra_javascript = [] - for (dirpath, dirnames, filenames) in os.walk(config['docs_dir']): + for (dirpath, _, filenames) in os.walk(config['docs_dir']): for filename in sorted(filenames): fullpath = os.path.join(dirpath, filename) - relpath = os.path.relpath(fullpath, config['docs_dir']) + relpath = os.path.normpath(os.path.relpath(fullpath, config['docs_dir'])) if utils.is_markdown_file(filename): # index pages should always be the first listed page. @@ -134,6 +138,23 @@ def validate_config(user_config): if config['pages'] is None: config['pages'] = pages + else: + """ + If the user has provided the pages config, then iterate through and + check for Windows style paths. If they are found, output a warning + and continue. + """ + for page_config in config['pages']: + if isinstance(page_config, str): + path = page_config + elif len(page_config) in (1, 2, 3): + path = page_config[0] + + if ntpath.sep in path: + log.warning("The config path contains Windows style paths (\\ " + " backward slash) and will have comparability " + "issues if it is used on another platform.") + break if config['extra_css'] is None: config['extra_css'] = extra_css diff --git a/mkdocs/main.py b/mkdocs/main.py index 8b9a9412..a196c039 100755 --- a/mkdocs/main.py +++ b/mkdocs/main.py @@ -17,7 +17,10 @@ from mkdocs.serve import serve def configure_logging(options): '''When a --verbose flag is passed, increase the verbosity of mkdocs''' logger = logging.getLogger('mkdocs') - logger.addHandler(logging.StreamHandler()) + sh = logging.StreamHandler() + formatter = logging.Formatter('%(levelname)s - %(message)s') + sh.setFormatter(formatter) + logger.addHandler(sh) if 'verbose' in options: logger.setLevel(logging.DEBUG) else: diff --git a/mkdocs/nav.py b/mkdocs/nav.py index 3e95a074..5a215b28 100644 --- a/mkdocs/nav.py +++ b/mkdocs/nav.py @@ -6,10 +6,13 @@ Deals with generating the site-wide navigation. This consists of building a set of interlinked page and header objects. """ -from mkdocs import utils, exceptions -import posixpath +import logging import os +from mkdocs import utils, exceptions + +log = logging.getLogger(__name__) + def filename_to_title(filename): """ @@ -84,7 +87,7 @@ class URLContext(object): self.base_path = '/' def set_current_url(self, current_url): - self.base_path = posixpath.dirname(current_url) + self.base_path = os.path.dirname(current_url) def make_relative(self, url): """ @@ -92,16 +95,16 @@ class URLContext(object): given the context of the current page. """ suffix = '/' if (url.endswith('/') and len(url) > 1) else '' - # Workaround for bug on `posixpath.relpath()` in Python 2.6 + # Workaround for bug on `os.path.relpath()` in Python 2.6 if self.base_path == '/': if url == '/': # Workaround for static assets return '.' return url.lstrip('/') # Under Python 2.6, relative_path adds an extra '/' at the end. - relative_path = posixpath.relpath(url, start=self.base_path).rstrip('/') + suffix + relative_path = os.path.relpath(url, start=self.base_path).rstrip('/') + suffix - return relative_path + return utils.path_to_url(relative_path) class FileContext(object): @@ -126,7 +129,7 @@ class FileContext(object): Given a relative file path return it as a POSIX-style absolute filepath, given the context of the current page. """ - return posixpath.normpath(posixpath.join(self.base_path, path)) + return os.path.normpath(os.path.join(self.base_path, path)) class Page(object): @@ -196,13 +199,13 @@ def _generate_site_navigation(pages_config, url_context, use_directory_urls=True for config_line in pages_config: if isinstance(config_line, str): - path = utils.normalise_path(config_line) + path = os.path.normpath(config_line) title, child_title = None, None elif len(config_line) in (1, 2, 3): # Pad any items that don't exist with 'None' padded_config = (list(config_line) + [None, None])[:3] path, title, child_title = padded_config - path = utils.normalise_path(path) + path = os.path.normpath(path) else: msg = ( "Line in 'page' config contained %d items. " @@ -213,12 +216,12 @@ def _generate_site_navigation(pages_config, url_context, use_directory_urls=True # If both the title and child_title are None, then we # have just been given a path. If that path contains a / # then lets automatically nest it. - if title is None and child_title is None and posixpath.sep in path: - filename = path.split(posixpath.sep)[-1] + if title is None and child_title is None and os.path.sep in path: + filename = path.split(os.path.sep)[-1] child_title = filename_to_title(filename) if title is None: - filename = path.split(posixpath.sep)[0] + filename = path.split(os.path.sep)[0] title = filename_to_title(filename) url = utils.get_url_path(path, use_directory_urls) diff --git a/mkdocs/relative_path_ext.py b/mkdocs/relative_path_ext.py index 248ea7bf..cfd1fcbf 100644 --- a/mkdocs/relative_path_ext.py +++ b/mkdocs/relative_path_ext.py @@ -35,7 +35,7 @@ tutorial/install.md | tutorial/install/ | ../img/initial-layout.png | tutorial/intro.md | tutorial/intro/ | ../../img/initial-layout.png | """ -from __future__ import print_function +import logging from markdown.extensions import Extension from markdown.treeprocessors import Treeprocessor @@ -44,6 +44,8 @@ from mkdocs import utils from mkdocs.compat import urlparse, urlunparse from mkdocs.exceptions import MarkdownNotFound +log = logging.getLogger(__name__) + def _iter(node): # TODO: Remove when dropping Python 2.6. Replace this @@ -74,9 +76,9 @@ def path_to_url(url, nav, strict): # In strict mode raise an error at this point. if strict: raise MarkdownNotFound(msg) - # Otherwise, when strict mode isn't enabled, print out a warning + # Otherwise, when strict mode isn't enabled, log a warning # to the user and leave the URL as it is. - print(msg) + log.warning(msg) return url path = utils.get_url_path(target_file, nav.use_directory_urls) path = nav.url_context.make_relative(path) diff --git a/mkdocs/serve.py b/mkdocs/serve.py index 911e9b53..3b436b8e 100644 --- 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) + builder() - # 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) + server = Server() - # 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() - - class TCPServer(socketserver.TCPServer): - allow_reuse_address = True - - 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/mkdocs/tests/nav_tests.py b/mkdocs/tests/nav_tests.py index 26ecdc94..4e8531cd 100644 --- a/mkdocs/tests/nav_tests.py +++ b/mkdocs/tests/nav_tests.py @@ -97,6 +97,7 @@ class SiteNavigationTests(unittest.TestCase): self.assertEqual(len(site_navigation.nav_items), 2) self.assertEqual(len(site_navigation.pages), 3) + @mock.patch.object(os.path, 'sep', '\\') def test_nested_ungrouped_no_titles_windows(self): pages = [ ('index.md',), diff --git a/mkdocs/utils.py b/mkdocs/utils.py index 8fa5af38..55c8d154 100644 --- a/mkdocs/utils.py +++ b/mkdocs/utils.py @@ -7,12 +7,10 @@ Nothing in this module should have an knowledge of config or the layout and structure of the site and pages in the site. """ -import ntpath import os -import posixpath import shutil -from mkdocs.compat import urlparse +from mkdocs.compat import urlparse, pathname2url def copy_file(source_path, output_path): @@ -162,19 +160,24 @@ def is_html_file(path): ] -def create_media_urls(nav, url_list): +def create_media_urls(nav, path_list): """ - Return a list of URLs that have been processed correctly for inclusion in a page. + Return a list of URLs that have been processed correctly for inclusion in + a page. """ final_urls = [] - for url in url_list: + + for path in path_list: # Allow links to fully qualified URL's - parsed = urlparse(url) + parsed = urlparse(path) if parsed.netloc: - final_urls.append(url) - else: - relative_url = '%s/%s' % (nav.url_context.make_relative('/'), url) - final_urls.append(relative_url) + final_urls.append(path) + continue + # We must be looking at a local path. + url = path_to_url(path) + relative_url = '%s/%s' % (nav.url_context.make_relative('/'), url) + final_urls.append(relative_url) + return final_urls @@ -216,12 +219,10 @@ def create_relative_media_url(nav, url): return relative_url -def normalise_path(path): - """ - Normalise POSIX and NT paths to be consistently POSIX style. - """ +def path_to_url(path): + """Convert a system path to a URL.""" - if ntpath.sep in path: - path = path.replace(ntpath.sep, posixpath.sep) + if os.path.sep == '/': + return path - return path + return pathname2url(path) diff --git a/requirements.txt b/requirements.txt index ac87b96d..5f8cf682 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ ghp-import>=0.4.1 Jinja2>=2.7.1 +livereload>=2.3.2 Markdown>=2.5 PyYAML>=3.10 -watchdog>=0.7.0 diff --git a/setup.py b/setup.py index 64698632..d6e55b8c 100755 --- a/setup.py +++ b/setup.py @@ -20,9 +20,9 @@ license = 'BSD' 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 = (