From 8085c65c6acc394be21aad866fdf0f68b6cb650e Mon Sep 17 00:00:00 2001 From: Dougal Matthews Date: Sun, 26 Apr 2015 10:28:42 +0100 Subject: [PATCH] Refactor the pages configuration The current pages configuration looks like this: pages: - ['index.md', 'Home'] - ['writing-your-docs.md', 'User Guide', 'Writing your docs'] - ['styling-your-docs.md', 'User Guide', 'Styling your docs'] - ['configuration.md', 'User Guide', 'Configuration'] - ['about/license.md', 'About', 'License'] - ['about/release-notes.md', 'About', 'Release Notes'] - ['about/contributing.md', 'About', 'Contributing'] This has a number of flaws: - It isn't clear how to add second levels of navigation for newcomers. This is often queried and not easy to document. - We are representing a tree structure as a set of flat items that need to be merged. This creates some interesting edge cases, for example: - ['writing-your-docs.md', 'User Guide'] - ['styling-your-docs.md', 'User Guide', 'Styling your docs'] Is the first entry a page with the title User Guide? or a page in the User Guide category with an automatic title. - We are currently limited to two levels deep in the navigation. Changing this with the current structure isn't trivial. This change adds a new format which makes the above configuration look like this: pages: - Home: index.md - User Guide: - user-guide/writing-your-docs.md - user-guide/styling-your-docs.md - user-guide/configuration.md - About: - License: about/license.md - about/release-notes.md - Contributing: about/contributing.md With this structure, we can more easily see the documentation tree and it is far more obvious what the navigation will look like. It also removes the ambiguous edge cases and opens up the possibility of adding further levels to the navigation more easily. This change restructures the pages configuration, but doesn't yet allow users to add further levels in the navigation. Fixes #6 --- docs/index.md | 11 +- docs/user-guide/configuration.md | 19 +- docs/user-guide/writing-your-docs.md | 16 +- mkdocs.yml | 18 +- mkdocs/config.py | 20 +- mkdocs/legacy.py | 120 +++++++++++ mkdocs/nav.py | 135 +++++++----- mkdocs/tests/build_tests.py | 38 ++-- mkdocs/tests/config_tests.py | 2 +- mkdocs/tests/legacy_tests.py | 65 ++++++ mkdocs/tests/nav_tests.py | 304 +++++++++++++++++++-------- mkdocs/tests/search_tests.py | 4 +- mkdocs/tests/utils_tests.py | 4 +- 13 files changed, 566 insertions(+), 190 deletions(-) create mode 100644 mkdocs/legacy.py create mode 100644 mkdocs/tests/legacy_tests.py diff --git a/docs/index.md b/docs/index.md index 047f6a4e..9f9b1367 100644 --- a/docs/index.md +++ b/docs/index.md @@ -105,8 +105,8 @@ We'd like our documentation site to include some navigation headers, so we'll ed site_name: MkLorum pages: - - [index.md, Home] - - [about.md, About] + - Home: index.md + - About: about.md Refresh the browser and you'll now see a navigation bar with `Home` and `About` headers. @@ -116,9 +116,10 @@ While we're here can also change the configuration file to alter how the documen site_name: MkLorum pages: - - [index.md, Home] - - [about.md, About] -``` + - Home: index.md + - About: about.md + theme: readthedocs + Refresh the browser again, and you'll now see the ReadTheDocs theme being used. diff --git a/docs/user-guide/configuration.md b/docs/user-guide/configuration.md index c7dff762..abf7c48f 100644 --- a/docs/user-guide/configuration.md +++ b/docs/user-guide/configuration.md @@ -90,22 +90,23 @@ The setting should be a list. Each row in the list represents information about Here's a simple example that would cause the build stage to create three pages: - pages: - - ['index.md', 'Introduction'] - - ['user-guide.md', 'User Guide'] - - ['about.md', 'About'] + - 'Introduction': 'index.md' + - 'User Guide': 'user-guide.md' + - 'Abut': 'about.md' Assuming the `docs_dir` setting was left with the default value of `docs`, the source files for this site's build process would be `docs/index.md`, `docs/user-guide.md` and `docs/about.md`. If you have a lot of project documentation you might choose to use headings to break up your site navigation by category. You can do so by including an extra string in the page configuration for any pages that require a navigation heading, like so: pages: - - ['index.md', 'Introduction'] - - ['user-guide/creating.md', 'User Guide', 'Creating a new Mashmallow project'] - - ['user-guide/api.md', 'User Guide', 'Mashmallow API guide'] - - ['user-guide/configuration.md', 'User Guide', 'Configuring Mashmallow'] - - ['about/license.md', 'About', 'License'] + - Introduction: 'index.md' + - User Guide: + - 'Creating a new Mashmallow project': 'user-guide/creating.md' + - 'Mashmallow API guide': 'user-guide/api.md' + - 'Configuring Mashmallow': 'user-guide/configuration.md' + - About: + - License: 'about/license.md' See also the section on [configuring pages and navigation](/user-guide/writing-your-docs/#configure-pages-and-navigation) for a more detailed breakdown. diff --git a/docs/user-guide/writing-your-docs.md b/docs/user-guide/writing-your-docs.md index 57620dd7..8ba930bf 100644 --- a/docs/user-guide/writing-your-docs.md +++ b/docs/user-guide/writing-your-docs.md @@ -17,19 +17,21 @@ A simple pages configuration looks like this: With this example we will build two pages at the top level and they will automatically have their titles inferred from the filename. To provide a custom name for these pages, they can be added after the filename. pages: - - ['index.md', 'Home'] - - ['about.md', 'About MkDocs'] + - Home: 'index.md' + - About: 'about.md' ### Multilevel documentation To create a second level in the navigation and group topics, the category can be provided before the page title. This is best demonstrated in a documentation project with more pages and is slighlt more complicated. pages: - - ['index.md', 'Home'] - - ['user-guide/writing-your-docs.md', 'User Guide', 'Writing your docs'] - - ['user-guide/styling-your-docs.md', 'User Guide', 'Styling your docs'] - - ['about/license.md', 'About', 'License'] - - ['about/release-notes.md', 'About', 'Release Notes'] + - Home: 'index.md' + - User Guide: + - 'Writing your docs': 'user-guide/writing-your-docs.md' + - 'Styling your docs': 'user-guide/styling-your-docs.md' + - About: + - 'License': 'about/license.md' + - 'Release Notes': 'about/release-notes.md' With the above configuration we have three top level sections Home, User Guide and About. Then under User Guide we have two pages, Writing your docs and Styling your docs. Under the About section we also have two pages, License and Release Notes diff --git a/mkdocs.yml b/mkdocs.yml index 48c2a31f..ad7ce968 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,13 +5,17 @@ site_description: Project documentation with Markdown. repo_url: https://github.com/mkdocs/mkdocs/ pages: -- ['index.md', 'Home'] -- ['user-guide/writing-your-docs.md', 'User Guide', 'Writing your docs'] -- ['user-guide/styling-your-docs.md', 'User Guide', 'Styling your docs'] -- ['user-guide/configuration.md', 'User Guide', 'Configuration'] -- ['about/license.md', 'About', 'License'] -- ['about/release-notes.md', 'About', 'Release Notes'] -- ['about/contributing.md', 'About', 'Contributing'] +- Home: index.md +- User Guide: + - Writing Your Docs: user-guide/writing-your-docs.md + - Styling Your Docs: user-guide/styling-your-docs.md + - Configuration: user-guide/configuration.md +- About: + - License: about/license.md + - Release Notes: about/release-notes.md + - Contributing: about/contributing.md + +use_new_pages_structure: true markdown_extensions: toc: diff --git a/mkdocs/config.py b/mkdocs/config.py index 65744fa9..408e3393 100644 --- a/mkdocs/config.py +++ b/mkdocs/config.py @@ -7,7 +7,7 @@ import os from six.moves.urllib.parse import urlparse -from mkdocs import utils +from mkdocs import utils, legacy from mkdocs.exceptions import ConfigurationError log = logging.getLogger(__name__) @@ -68,7 +68,7 @@ DEFAULT_CONFIG = { # enabling strict mode causes MkDocs to stop the build when a problem is # encountered rather than display an error. - 'strict': False + 'strict': False, } @@ -153,11 +153,27 @@ def validate_config(user_config): check for Windows style paths. If they are found, output a warning and continue. """ + + # TODO: Remove in 1.0 + config_types = set(type(l) for l in config['pages']) + if list in config_types and dict not in config_types: + config['pages'] = legacy.pages_compat_shim(config['pages']) + elif list in config_types: + raise ConfigurationError("") + for page_config in config['pages']: + if isinstance(page_config, str): path = page_config + elif isinstance(page_config, dict): + if len(page_config) > 1: + raise ConfigurationError( + "Invalid page config. Should have one key value only") + _, path = next(iter(page_config.items())) elif len(page_config) in (1, 2, 3): path = page_config[0] + else: + raise ConfigurationError("Unrecognised page config") if ntpath.sep in path: log.warning("The config path contains Windows style paths (\\ " diff --git a/mkdocs/legacy.py b/mkdocs/legacy.py new file mode 100644 index 00000000..0ac6d705 --- /dev/null +++ b/mkdocs/legacy.py @@ -0,0 +1,120 @@ +import logging + +from mkdocs.exceptions import ConfigurationError + +log = logging.getLogger(__name__) + + +def pages_compat_shim(original_pages): + """ + Support legacy pages configuration + + Re-write the pages config fron MkDocs <=0.12 to match the + new nested structure added in 0.13. + + Given a pages configuration in the old style of: + + pages: + - ['index.md', 'Home'] + - ['user-guide/writing-your-docs.md', 'User Guide'] + - ['user-guide/styling-your-docs.md', 'User Guide'] + - ['about/license.md', 'About', 'License'] + - ['about/release-notes.md', 'About'] + - ['help/contributing.md', 'Help', 'Contributing'] + - ['support.md'] + - ['cli.md', 'CLI Guide'] + + Rewrite it to look like: + + pages: + - Home: index.md + - User Guide: + - user-guide/writing-your-docs.md + - user-guide/styling-your-docs.md + - About: + - License: about/license.md + - about/release-notes.md + - Help: + - Contributing: about/contributing.md + - support.md + - CLI Guide: cli.md + + TODO: Remove in 1.0 + """ + + log.warning("The pages config in the mkdocs.yml uses the deprecated " + "structure. This will be removed in the next release of " + "MkDocs. See for details on updating: " + "http://www.mkdocs.org/about/release-notes/") + + new_pages = [] + + for config_line in original_pages: + + if len(config_line) not in (1, 2, 3): + msg = ( + "Line in 'page' config contained {0} items. In Line {1}. " + "Expected 1, 2 or 3 strings.".format( + config_line, len(config_line)) + ) + raise ConfigurationError(msg) + + # First we need to pad out the config line as it could contain + # 1-3 items. + path, category, title = (list(config_line) + [None, None])[:3] + + if len(new_pages) > 0: + # Get the previous top-level page so we can see if the category + # matches up with the one we have now. + prev_cat, subpages = next(iter(new_pages[-1].items())) + else: + # We are on the first page + prev_cat, subpages = None, [] + + # If the category is different, add a new top level category. If the + # previous category is None, the it's another top level one too. + if prev_cat is None or prev_cat != category: + subpages = [] + new_pages.append({category: subpages}) + + # Add the current page to the determined category. + subpages.append({title: path}) + + # We need to do a bit of cleaning up to match the new structure. In the + # above example, pages can either be `- file.md` or `- Title: file.md`. + # For pages without a title we currently have `- None: file.md` - so we + # need to remove those Nones by changing from a dict to just a string with + # the path. + for i, category in enumerate(new_pages): + + # Categories are a dictionary with one key as the name and the value + # is a list of pages. So, grab that from the dict. + category, pages = next(iter(category.items())) + + # If we only have one page, then we can assume it is a top level + # category and no further nesting is required unless that single page + # has a title itself, + if len(pages) == 1: + title, path = pages.pop().popitem() + # If we have a title, it should be a sub page + if title is not None: + pages.append({title: path}) + # if we have a category, but no title it should be a top-level page + elif category is not None: + new_pages[i] = {category: path} + # if we have no category or title, it must be a top level page with + # an atomatic title. + else: + new_pages[i] = path + else: + # We have more than one page, so the category is valid. We just + # need to iterate through and convert any {None: path} dicts to + # be just the path string. + for j, page in enumerate(pages): + title, path = page.popitem() + if title: + pages[j] = {title: path} + else: + pages[j] = path + + return new_pages diff --git a/mkdocs/nav.py b/mkdocs/nav.py index 85d33513..ef6234dd 100644 --- a/mkdocs/nav.py +++ b/mkdocs/nav.py @@ -28,7 +28,8 @@ def file_to_title(filename): try: with open(filename, 'r') as f: lines = f.read() - _, table_of_contents, meta = utils.convert_markdown(lines, ['meta', 'toc']) + _, table_of_contents, meta = utils.convert_markdown( + lines, ['meta', 'toc']) if "title" in meta: return meta["title"][0] if len(table_of_contents.items) > 0: @@ -53,8 +54,8 @@ class SiteNavigation(object): def __init__(self, pages_config, use_directory_urls=True): self.url_context = URLContext() self.file_context = FileContext() - self.nav_items, self.pages = \ - _generate_site_navigation(pages_config, self.url_context, use_directory_urls) + self.nav_items, self.pages = _generate_site_navigation( + pages_config, self.url_context, use_directory_urls) self.homepage = self.pages[0] if self.pages else None self.use_directory_urls = use_directory_urls @@ -122,7 +123,8 @@ class URLContext(object): return '.' return url.lstrip('/') # Under Python 2.6, relative_path adds an extra '/' at the end. - relative_path = os.path.relpath(url, start=self.base_path).rstrip('/') + suffix + relative_path = os.path.relpath(url, start=self.base_path) + relative_path = relative_path.rstrip('/') + suffix return utils.path_to_url(relative_path) @@ -154,6 +156,7 @@ class FileContext(object): class Page(object): def __init__(self, title, url, path, url_context): + self.title = title self.abs_url = url self.active = False @@ -209,74 +212,100 @@ class Header(object): return ret -def _generate_site_navigation(pages_config, url_context, use_directory_urls=True): +def _path_to_page(path, title, url_context, use_directory_urls): + if title is None: + title = file_to_title(path.split(os.path.sep)[-1]) + url = utils.get_url_path(path, use_directory_urls) + return Page(title=title, url=url, path=path, + url_context=url_context) + + +def _generate_site_navigation(pages_config, url_context, use_dir_urls=True): """ Returns a list of Page and Header instances that represent the top level site navigation. """ nav_items = [] pages = [] + previous = None for config_line in pages_config: + if isinstance(config_line, str): 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 = os.path.normpath(path) - else: - msg = ( - "Line in 'page' config contained %d items. " - "Expected 1, 2 or 3 strings." % len(config_line) - ) + + page = _path_to_page(path, None, url_context, use_dir_urls) + nav_items.append(page) + + if previous: + page.previous_page = previous + previous.next_page = page + previous = page + + pages.append(page) + continue + + elif not isinstance(config_line, dict): + msg = ("Line in 'page' config is of type {0}, dict or string " + "expected. Line: {1}").format(type(config_line), config_line) raise exceptions.ConfigurationError(msg) - # 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 os.path.sep in path: - filename = path.split(os.path.sep)[-1] - child_title = file_to_title(filename) + if len(config_line) > 1: + raise exceptions.ConfigurationError( + "Page configs should be in the format 'name: markdown.md'. The " + "config contains an invalid entry: {0}".format(config_line)) - if title is None: - filename = path.split(os.path.sep)[0] - title = file_to_title(filename) + try: + category, subpages_or_path = next(iter(config_line.items())) + except StopIteration: + log.warning("Ignoring empty line in the pages config.") + continue - # If we don't have a child title but the other title is the same, we - # should be within a section and the child title needs to be inferred - # from the filename. - if len(nav_items) and title == nav_items[-1].title == title and child_title is None: - filename = path.split(os.path.sep)[-1] - child_title = file_to_title(filename) - - url = utils.get_url_path(path, use_directory_urls) - - if not child_title: - # New top level page. - page = Page(title=title, url=url, path=path, url_context=url_context) + if isinstance(subpages_or_path, str): + path = subpages_or_path + page = _path_to_page(path, category, url_context, use_dir_urls) nav_items.append(page) - elif not nav_items or (nav_items[-1].title != title): - # New second level page. - page = Page(title=child_title, url=url, path=path, url_context=url_context) - header = Header(title=title, children=[page]) - nav_items.append(header) + + if previous: + page.previous_page = previous + previous.next_page = page + previous = page + + pages.append(page) + continue + + header = Header(title=category, children=[]) + nav_items.append(header) + + for subpage in subpages_or_path: + + if isinstance(subpage, str): + path = subpage + page = _path_to_page(path, None, url_context, use_dir_urls) + + elif isinstance(subpage, dict): + title, path = next(iter(subpage.items())) + page = _path_to_page(path, title, url_context, use_dir_urls) + else: + msg = ("Line in 'page' config is of type {0}, dict or string " + "expected. Line: {1}" + ).format(type(config_line), config_line) + raise exceptions.ConfigurationError(msg) + page.ancestors = [header] - else: - # Additional second level page. - page = Page(title=child_title, url=url, path=path, url_context=url_context) - header = nav_items[-1] header.children.append(page) - page.ancestors = [header] - # Add in previous and next information. - if previous: - page.previous_page = previous - previous.next_page = page - previous = page + if previous: + page.previous_page = previous + previous.next_page = page + previous = page - pages.append(page) + pages.append(page) + + if len(pages) == 0: + raise exceptions.ConfigurationError( + "No pages found in the pages config. " + "Remove it entirely to enable automatic page discovery.") return (nav_items, pages) diff --git a/mkdocs/tests/build_tests.py b/mkdocs/tests/build_tests.py index 784d8925..728b4c7a 100644 --- a/mkdocs/tests/build_tests.py +++ b/mkdocs/tests/build_tests.py @@ -84,9 +84,9 @@ class BuildTests(unittest.TestCase): def test_convert_internal_media(self): """Test relative image URL's are the same for different base_urls""" pages = [ - ('index.md',), - ('internal.md',), - ('sub/internal.md') + 'index.md', + 'internal.md', + 'sub/internal.md', ] site_navigation = nav.SiteNavigation(pages) @@ -107,9 +107,9 @@ class BuildTests(unittest.TestCase): def test_convert_internal_asbolute_media(self): """Test absolute image URL's are correct for different base_urls""" pages = [ - ('index.md',), - ('internal.md',), - ('sub/internal.md') + 'index.md', + 'internal.md', + 'sub/internal.md', ] site_navigation = nav.SiteNavigation(pages) @@ -129,9 +129,9 @@ class BuildTests(unittest.TestCase): def test_dont_convert_code_block_urls(self): pages = [ - ('index.md',), - ('internal.md',), - ('sub/internal.md') + 'index.md', + 'internal.md', + 'sub/internal.md', ] site_navigation = nav.SiteNavigation(pages) @@ -150,9 +150,9 @@ class BuildTests(unittest.TestCase): def test_anchor_only_link(self): pages = [ - ('index.md',), - ('internal.md',), - ('sub/internal.md') + 'index.md', + 'internal.md', + 'sub/internal.md', ] site_navigation = nav.SiteNavigation(pages) @@ -172,7 +172,7 @@ class BuildTests(unittest.TestCase): md_text = 'An [internal link](internal.md) to another document.' expected = '

An internal link to another document.

' pages = [ - ('internal.md',) + 'internal.md', ] site_navigation = nav.SiteNavigation(pages, use_directory_urls=False) html, toc, meta = build.convert_markdown(md_text, site_navigation=site_navigation) @@ -297,9 +297,9 @@ class BuildTests(unittest.TestCase): def test_strict_mode_valid(self): pages = [ - ('index.md',), - ('internal.md',), - ('sub/internal.md') + 'index.md', + 'internal.md', + 'sub/internal.md', ] site_nav = nav.SiteNavigation(pages) @@ -309,9 +309,9 @@ class BuildTests(unittest.TestCase): def test_strict_mode_invalid(self): pages = [ - ('index.md',), - ('internal.md',), - ('sub/internal.md') + 'index.md', + 'internal.md', + 'sub/internal.md', ] site_nav = nav.SiteNavigation(pages) diff --git a/mkdocs/tests/config_tests.py b/mkdocs/tests/config_tests.py index 53d2e915..0687edfe 100644 --- a/mkdocs/tests/config_tests.py +++ b/mkdocs/tests/config_tests.py @@ -72,7 +72,7 @@ class ConfigTests(unittest.TestCase): expected_result = { 'site_name': 'Example', 'pages': [ - ['index.md', 'Introduction'] + {'Introduction': 'index.md'} ], } file_contents = dedent(""" diff --git a/mkdocs/tests/legacy_tests.py b/mkdocs/tests/legacy_tests.py new file mode 100644 index 00000000..0951fce7 --- /dev/null +++ b/mkdocs/tests/legacy_tests.py @@ -0,0 +1,65 @@ +import unittest + +import yaml + +from mkdocs import legacy + + +class TestCompatabilityShim(unittest.TestCase): + + # TODO: Remove in 1.0 + + def test_convert(self): + + self.maxDiff = None + + pages_yaml_old = """ + pages: + - ['index.md', 'Home'] + - ['user-guide/writing-your-docs.md', 'User Guide'] + - ['user-guide/styling-your-docs.md', 'User Guide'] + - ['about/license.md', 'About', 'License'] + - ['about/release-notes.md', 'About'] + - ['about/contributing.md', 'About', 'Contributing'] + - ['help/contributing.md', 'Help', 'Contributing'] + - ['support.md'] + - ['cli.md', 'CLI Guide'] + """ + + pages_yaml_new = """ + pages: + - Home: index.md + - User Guide: + - user-guide/writing-your-docs.md + - user-guide/styling-your-docs.md + - About: + - License: about/license.md + - about/release-notes.md + - Contributing: about/contributing.md + - Help: + - Contributing: help/contributing.md + - support.md + - CLI Guide: cli.md + """ + self.assertEqual( + legacy.pages_compat_shim(yaml.load(pages_yaml_old)['pages']), + yaml.load(pages_yaml_new)['pages']) + + def test_convert_no_home(self): + + self.maxDiff = None + + pages_yaml_old = """ + pages: + - ['index.md'] + - ['about.md', 'About'] + """ + + pages_yaml_new = """ + pages: + - index.md + - About: about.md + """ + self.assertEqual( + legacy.pages_compat_shim(yaml.load(pages_yaml_old)['pages']), + yaml.load(pages_yaml_new)['pages']) diff --git a/mkdocs/tests/nav_tests.py b/mkdocs/tests/nav_tests.py index 90fc4469..6b5fd9ea 100644 --- a/mkdocs/tests/nav_tests.py +++ b/mkdocs/tests/nav_tests.py @@ -5,7 +5,7 @@ import mock import os import unittest -from mkdocs import nav +from mkdocs import nav, legacy from mkdocs.exceptions import ConfigurationError from mkdocs.tests.base import dedent @@ -13,8 +13,8 @@ from mkdocs.tests.base import dedent class SiteNavigationTests(unittest.TestCase): def test_simple_toc(self): pages = [ - ('index.md', 'Home'), - ('about.md', 'About') + {'Home': 'index.md'}, + {'About': 'about.md'} ] expected = dedent(""" Home - / @@ -27,8 +27,8 @@ class SiteNavigationTests(unittest.TestCase): def test_empty_toc_item(self): pages = [ - ('index.md',), - ('about.md', 'About') + 'index.md', + {'About': 'about.md'} ] expected = dedent(""" Home - / @@ -41,36 +41,16 @@ class SiteNavigationTests(unittest.TestCase): def test_indented_toc(self): pages = [ - ('index.md', 'Home'), - ('api-guide/running.md', 'API Guide', 'Running'), - ('api-guide/testing.md', 'API Guide', 'Testing'), - ('api-guide/debugging.md', 'API Guide', 'Debugging'), - ('about/release-notes.md', 'About', 'Release notes'), - ('about/license.md', 'About', 'License') - ] - expected = dedent(""" - Home - / - API Guide - Running - /api-guide/running/ - Testing - /api-guide/testing/ - Debugging - /api-guide/debugging/ - About - Release notes - /about/release-notes/ - License - /about/license/ - """) - site_navigation = nav.SiteNavigation(pages) - self.assertEqual(str(site_navigation).strip(), expected) - self.assertEqual(len(site_navigation.nav_items), 3) - self.assertEqual(len(site_navigation.pages), 6) - - def test_indented_toc_missing_child_title(self): - pages = [ - ('index.md', 'Home'), - ('api-guide/running.md', 'API Guide', 'Running'), - ('api-guide/testing.md', 'API Guide'), - ('api-guide/debugging.md', 'API Guide', 'Debugging'), - ('about/release-notes.md', 'About', 'Release notes'), - ('about/license.md', 'About', 'License') + {'Home': 'index.md'}, + {'API Guide': [ + {'Running': 'api-guide/running.md'}, + {'Testing': 'api-guide/testing.md'}, + {'Debugging': 'api-guide/debugging.md'}, + ]}, + {'About': [ + {'Release notes': 'about/release-notes.md'}, + {'License': 'about/license.md'} + ]} ] expected = dedent(""" Home - / @@ -89,9 +69,9 @@ class SiteNavigationTests(unittest.TestCase): def test_nested_ungrouped(self): pages = [ - ('index.md', 'Home'), - ('about/contact.md', 'Contact'), - ('about/sub/license.md', 'License Title') + {'Home': 'index.md'}, + {'Contact': 'about/contact.md'}, + {'License Title': 'about/sub/license.md'}, ] expected = dedent(""" Home - / @@ -105,45 +85,43 @@ class SiteNavigationTests(unittest.TestCase): def test_nested_ungrouped_no_titles(self): pages = [ - ('index.md',), - ('about/contact.md'), - ('about/sub/license.md') + 'index.md', + 'about/contact.md', + 'about/sub/license.md' ] expected = dedent(""" Home - / - About - Contact - /about/contact/ - License - /about/sub/license/ + Contact - /about/contact/ + License - /about/sub/license/ """) site_navigation = nav.SiteNavigation(pages) self.assertEqual(str(site_navigation).strip(), expected) - self.assertEqual(len(site_navigation.nav_items), 2) + self.assertEqual(len(site_navigation.nav_items), 3) self.assertEqual(len(site_navigation.pages), 3) @mock.patch.object(os.path, 'sep', '\\') def test_nested_ungrouped_no_titles_windows(self): pages = [ - ('index.md',), - ('about\\contact.md'), - ('about\\sub\\license.md') + 'index.md', + 'about\\contact.md', + 'about\\sub\\license.md', ] expected = dedent(""" Home - / - About - Contact - /about/contact/ - License - /about/sub/license/ + Contact - /about/contact/ + License - /about/sub/license/ """) site_navigation = nav.SiteNavigation(pages) self.assertEqual(str(site_navigation).strip(), expected) - self.assertEqual(len(site_navigation.nav_items), 2) + self.assertEqual(len(site_navigation.nav_items), 3) self.assertEqual(len(site_navigation.pages), 3) def test_walk_simple_toc(self): pages = [ - ('index.md', 'Home'), - ('about.md', 'About') + {'Home': 'index.md'}, + {'About': 'about.md'} ] expected = [ dedent(""" @@ -161,8 +139,8 @@ class SiteNavigationTests(unittest.TestCase): def test_walk_empty_toc(self): pages = [ - ('index.md',), - ('about.md', 'About') + 'index.md', + {'About': 'about.md'} ] expected = [ dedent(""" @@ -180,12 +158,16 @@ class SiteNavigationTests(unittest.TestCase): def test_walk_indented_toc(self): pages = [ - ('index.md', 'Home'), - ('api-guide/running.md', 'API Guide', 'Running'), - ('api-guide/testing.md', 'API Guide', 'Testing'), - ('api-guide/debugging.md', 'API Guide', 'Debugging'), - ('about/release-notes.md', 'About', 'Release notes'), - ('about/license.md', 'About', 'License') + {'Home': 'index.md'}, + {'API Guide': [ + {'Running': 'api-guide/running.md'}, + {'Testing': 'api-guide/testing.md'}, + {'Debugging': 'api-guide/debugging.md'}, + ]}, + {'About': [ + {'Release notes': 'about/release-notes.md'}, + {'License': 'about/license.md'} + ]} ] expected = [ dedent(""" @@ -255,7 +237,7 @@ class SiteNavigationTests(unittest.TestCase): def test_base_url(self): pages = [ - ('index.md',) + 'index.md' ] site_navigation = nav.SiteNavigation(pages, use_directory_urls=False) base_url = site_navigation.url_context.make_relative('/') @@ -263,8 +245,8 @@ class SiteNavigationTests(unittest.TestCase): def test_relative_md_links_have_slash(self): pages = [ - ('index.md',), - ('user-guide/styling-your-docs.md',) + 'index.md', + 'user-guide/styling-your-docs.md' ] site_navigation = nav.SiteNavigation(pages, use_directory_urls=False) site_navigation.url_context.base_path = "/user-guide/configuration" @@ -277,17 +259,17 @@ class SiteNavigationTests(unittest.TestCase): """ pages = [ - ('index.md', ), - ('api-guide/running.md', ), - ('about/notes.md', ), - ('about/sub/license.md', ), + 'index.md', + 'api-guide/running.md', + 'about/notes.md', + 'about/sub/license.md', ] url_context = nav.URLContext() nav_items, pages = nav._generate_site_navigation(pages, url_context) self.assertEqual([n.title for n in nav_items], - ['Home', 'Api guide', 'About']) + ['Home', 'Running', 'Notes', 'License']) self.assertEqual([p.title for p in pages], ['Home', 'Running', 'Notes', 'License']) @@ -297,25 +279,25 @@ class SiteNavigationTests(unittest.TestCase): Verify inferring page titles based on the filename with a windows path """ pages = [ - ('index.md', ), - ('api-guide\\running.md', ), - ('about\\notes.md', ), - ('about\\sub\\license.md', ), + 'index.md', + 'api-guide\\running.md', + 'about\\notes.md', + 'about\\sub\\license.md', ] url_context = nav.URLContext() nav_items, pages = nav._generate_site_navigation(pages, url_context) self.assertEqual([n.title for n in nav_items], - ['Home', 'Api guide', 'About']) + ['Home', 'Running', 'Notes', 'License']) self.assertEqual([p.title for p in pages], ['Home', 'Running', 'Notes', 'License']) def test_invalid_pages_config(self): bad_pages = [ - (), # too short - ('this', 'is', 'too', 'long'), + set(), # should be dict or string only + {"a": "index.md", "b": "index.md"} # extra key ] for bad_page in bad_pages: @@ -325,15 +307,28 @@ class SiteNavigationTests(unittest.TestCase): self.assertRaises(ConfigurationError, _test) + def test_pages_config(self): + + bad_page = {} # empty + + def _test(): + return nav._generate_site_navigation((bad_page, ), None) + + self.assertRaises(ConfigurationError, _test) + def test_ancestors(self): pages = [ - ('index.md', 'Home'), - ('api-guide/running.md', 'API Guide', 'Running'), - ('api-guide/testing.md', 'API Guide', 'Testing'), - ('api-guide/debugging.md', 'API Guide', 'Debugging'), - ('about/release-notes.md', 'About', 'Release notes'), - ('about/license.md', 'About', 'License') + {'Home': 'index.md'}, + {'API Guide': [ + {'Running': 'api-guide/running.md'}, + {'Testing': 'api-guide/testing.md'}, + {'Debugging': 'api-guide/debugging.md'}, + ]}, + {'About': [ + {'Release notes': 'about/release-notes.md'}, + {'License': 'about/license.md'} + ]} ] site_navigation = nav.SiteNavigation(pages) @@ -361,3 +356,146 @@ class SiteNavigationTests(unittest.TestCase): title = nav.file_to_title("mkdocs/tests/resources/no_title_metadata.md") self.assertEqual(title, "Title") + + +class TestLegacyPagesConfig(unittest.TestCase): + + def test_walk_simple_toc(self): + pages = legacy.pages_compat_shim([ + ('index.md', 'Home'), + ('about.md', 'About') + ]) + expected = [ + dedent(""" + Home - / [*] + About - /about/ + """), + dedent(""" + Home - / + About - /about/ [*] + """) + ] + site_navigation = nav.SiteNavigation(pages) + for index, page in enumerate(site_navigation.walk_pages()): + self.assertEqual(str(site_navigation).strip(), expected[index]) + + def test_walk_empty_toc(self): + pages = legacy.pages_compat_shim([ + ('index.md',), + ('about.md', 'About') + ]) + + print(pages) + + expected = [ + dedent(""" + Home - / [*] + About - /about/ + """), + dedent(""" + Home - / + About - /about/ [*] + """) + ] + site_navigation = nav.SiteNavigation(pages) + for index, page in enumerate(site_navigation.walk_pages()): + self.assertEqual(str(site_navigation).strip(), expected[index]) + + def test_walk_indented_toc(self): + pages = legacy.pages_compat_shim([ + ('index.md', 'Home'), + ('api-guide/running.md', 'API Guide', 'Running'), + ('api-guide/testing.md', 'API Guide', 'Testing'), + ('api-guide/debugging.md', 'API Guide', 'Debugging'), + ('about/release-notes.md', 'About', 'Release notes'), + ('about/license.md', 'About', 'License') + ]) + expected = [ + dedent(""" + Home - / [*] + API Guide + Running - /api-guide/running/ + Testing - /api-guide/testing/ + Debugging - /api-guide/debugging/ + About + Release notes - /about/release-notes/ + License - /about/license/ + """), + dedent(""" + Home - / + API Guide [*] + Running - /api-guide/running/ [*] + Testing - /api-guide/testing/ + Debugging - /api-guide/debugging/ + About + Release notes - /about/release-notes/ + License - /about/license/ + """), + dedent(""" + Home - / + API Guide [*] + Running - /api-guide/running/ + Testing - /api-guide/testing/ [*] + Debugging - /api-guide/debugging/ + About + Release notes - /about/release-notes/ + License - /about/license/ + """), + dedent(""" + Home - / + API Guide [*] + Running - /api-guide/running/ + Testing - /api-guide/testing/ + Debugging - /api-guide/debugging/ [*] + About + Release notes - /about/release-notes/ + License - /about/license/ + """), + dedent(""" + Home - / + API Guide + Running - /api-guide/running/ + Testing - /api-guide/testing/ + Debugging - /api-guide/debugging/ + About [*] + Release notes - /about/release-notes/ [*] + License - /about/license/ + """), + dedent(""" + Home - / + API Guide + Running - /api-guide/running/ + Testing - /api-guide/testing/ + Debugging - /api-guide/debugging/ + About [*] + Release notes - /about/release-notes/ + License - /about/license/ [*] + """) + ] + site_navigation = nav.SiteNavigation(pages) + for index, page in enumerate(site_navigation.walk_pages()): + self.assertEqual(str(site_navigation).strip(), expected[index]) + + def test_indented_toc_missing_child_title(self): + pages = legacy.pages_compat_shim([ + ('index.md', 'Home'), + ('api-guide/running.md', 'API Guide', 'Running'), + ('api-guide/testing.md', 'API Guide'), + ('api-guide/debugging.md', 'API Guide', 'Debugging'), + ('about/release-notes.md', 'About', 'Release notes'), + ('about/license.md', 'About', 'License') + ]) + expected = dedent(""" + Home - / + API Guide + Running - /api-guide/running/ + Testing - /api-guide/testing/ + Debugging - /api-guide/debugging/ + About + Release notes - /about/release-notes/ + License - /about/license/ + """) + site_navigation = nav.SiteNavigation(pages) + self.assertEqual(str(site_navigation).strip(), expected) + self.assertEqual(len(site_navigation.nav_items), 3) + self.assertEqual(len(site_navigation.pages), 6) diff --git a/mkdocs/tests/search_tests.py b/mkdocs/tests/search_tests.py index c53f40b6..182f2693 100644 --- a/mkdocs/tests/search_tests.py +++ b/mkdocs/tests/search_tests.py @@ -103,8 +103,8 @@ class SearchTests(unittest.TestCase): """ pages = [ - ('index.md', 'Home'), - ('about.md', 'About') + {'Home': 'index.md'}, + {'About': 'about.md'}, ] site_navigation = nav.SiteNavigation(pages) diff --git a/mkdocs/tests/utils_tests.py b/mkdocs/tests/utils_tests.py index 23da4e07..e2854187 100644 --- a/mkdocs/tests/utils_tests.py +++ b/mkdocs/tests/utils_tests.py @@ -54,8 +54,8 @@ class UtilsTests(unittest.TestCase): def test_create_media_urls(self): pages = [ - ('index.md', 'Home'), - ('about.md', 'About') + {'Home': 'index.md'}, + {'About': 'about.md'} ] expected_results = { 'https://media.cdn.org/jq.js': 'https://media.cdn.org/jq.js',