diff --git a/mkdocs/structure/files.py b/mkdocs/structure/files.py index 69e05d5281..6bc4bfb189 100644 --- a/mkdocs/structure/files.py +++ b/mkdocs/structure/files.py @@ -8,7 +8,7 @@ import shutil import warnings from functools import cached_property -from pathlib import PurePath +from pathlib import PurePath, PurePosixPath from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Mapping, Sequence, overload from urllib.parse import quote as urlquote @@ -582,12 +582,25 @@ def get_files(config: MkDocsConfig) -> Files: return Files(files) +def file_sort_key(f: File, /): + """ + Replicates the sort order how `get_files` produces it - index first, directories last. + + To sort a list of `File`, pass as the `key` argument to `sort`. + """ + parts = PurePosixPath(f.src_uri).parts + if not parts: + return () + return (parts[:-1], f.name != "index", parts[-1]) + + def _file_sort_key(f: str): - """Always sort `index` or `README` as first filename in list.""" + """Always sort `index` or `README` as first filename in list. This works only on basenames of files.""" return (os.path.splitext(f)[0] not in ('index', 'README'), f) def _sort_files(filenames: Iterable[str]) -> list[str]: + """Soft-deprecated, do not use.""" return sorted(filenames, key=_file_sort_key) diff --git a/mkdocs/structure/nav.py b/mkdocs/structure/nav.py index 2c39fa2085..21815fe612 100644 --- a/mkdocs/structure/nav.py +++ b/mkdocs/structure/nav.py @@ -6,6 +6,7 @@ from mkdocs.exceptions import BuildError from mkdocs.structure import StructureItem +from mkdocs.structure.files import file_sort_key from mkdocs.structure.pages import Page, _AbsoluteLinksValidationValue from mkdocs.utils import nest_paths @@ -129,9 +130,10 @@ def __repr__(self): def get_navigation(files: Files, config: MkDocsConfig) -> Navigation: """Build site navigation from config and files.""" documentation_pages = files.documentation_pages() - nav_config = config['nav'] or nest_paths( - f.src_uri for f in documentation_pages if f.inclusion.is_in_nav() - ) + nav_config = config['nav'] + if nav_config is None: + documentation_pages = sorted(documentation_pages, key=file_sort_key) + nav_config = nest_paths(f.src_uri for f in documentation_pages if f.inclusion.is_in_nav()) items = _data_to_navigation(nav_config, files, config) if not isinstance(items, list): items = [items] diff --git a/mkdocs/tests/structure/file_tests.py b/mkdocs/tests/structure/file_tests.py index 2739000dbd..fe3c4143f6 100644 --- a/mkdocs/tests/structure/file_tests.py +++ b/mkdocs/tests/structure/file_tests.py @@ -3,7 +3,7 @@ import unittest from unittest import mock -from mkdocs.structure.files import File, Files, _sort_files, get_files +from mkdocs.structure.files import File, Files, _sort_files, file_sort_key, get_files from mkdocs.tests.base import PathAssertionMixin, load_config, tempdir @@ -57,6 +57,16 @@ def test_sort_files(self): ['README.md', 'A.md', 'B.md'], ) + def test_file_sort_key(self): + for case in [ + ["a/b.md", "b/index.md", "b/a.md"], + ["SUMMARY.md", "foo/z.md", "foo/bar/README.md", "foo/bar/index.md", "foo/bar/a.md"], + ]: + with self.subTest(case): + files = [File(f, "", "", use_directory_urls=True) for f in case] + for a, b in zip(files, files[1:]): + self.assertLess(file_sort_key(a), file_sort_key(b)) + def test_md_file(self): for use_directory_urls in True, False: with self.subTest(use_directory_urls=use_directory_urls):