mirror of
https://github.com/mkdocs/mkdocs.git
synced 2026-03-27 09:58:31 +07:00
Merge pull request #451 from d0ugal/path-handling
Improve Path handling across MkDocs
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
119
mkdocs/serve.py
119
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)
|
||||
|
||||
@@ -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',),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user