├── .github ├── FUNDING.yml └── workflows │ ├── autofix.yml │ ├── ci.yml │ ├── deploy-docs.yml │ └── deploy-release.yml ├── .gitignore ├── .tools ├── copier-answers.yml └── release.sh ├── LICENSE.md ├── README.md ├── docs ├── README.md └── reference.md ├── mkdocs.yml ├── mkdocs_literate_nav ├── __init__.py ├── exceptions.py ├── parser.py ├── plugin.py └── py.typed ├── pyproject.toml ├── requirements ├── requirements-docs.txt └── requirements-style.txt └── tests ├── conftest.py ├── nav ├── hybrid │ ├── test_empty.yml │ ├── test_files_in_subsection.yml │ ├── test_flattened_files_in_subsection.yml │ ├── test_from_readme.yml │ ├── test_keeps_original_nav.yml │ ├── test_literate_in_subsection.yml │ ├── test_nav_and_literate.yml │ ├── test_preserves_url.yml │ ├── test_repeated.yml │ ├── test_slash_with_nav.yml │ ├── test_very_nested.yml │ └── test_wildcard_slash.yml ├── nested │ ├── test_basic.yml │ ├── test_borgs.yml │ ├── test_dir_without_slash.yml │ ├── test_go_up.yml │ ├── test_go_up_too_much.yml │ ├── test_implicit_index_as_nav.yml │ ├── test_implicit_index_borgs.yml │ ├── test_missing.yml │ ├── test_nested.yml │ ├── test_recursion.yml │ └── test_repeated.yml ├── test_basic.yml ├── test_broken_before_marker.yml ├── test_element_after.yml ├── test_empty.yml ├── test_empty_section.yml ├── test_fancy_link.yml ├── test_multiple_nav.yml ├── test_multiple_nav_and_others.yml ├── test_multiple_unmarked_nav.yml ├── test_nav_after_gap.yml ├── test_nav_last.yml ├── test_no_title.yml ├── test_obscured_link.yml ├── test_obscured_section.yml ├── test_preserves_url.yml ├── test_section_with_link.yml ├── test_special_chars.yml ├── test_text_after.yml └── wildcard │ ├── test_basic.yml │ ├── test_dirs_then_files.yml │ ├── test_from_readme.yml │ ├── test_implicitly_recursive.yml │ ├── test_multidir_file.yml │ ├── test_no_files.yml │ ├── test_no_repeat_with_directory.yml │ ├── test_non_matching.yml │ ├── test_omits_competing_parent_first.yml │ ├── test_omits_competing_subdir_first.yml │ ├── test_omits_in_subdir.yml │ ├── test_omits_in_superdir.yml │ ├── test_skips_nav_file.yml │ └── test_wildcard_under_index.yml └── test_plugin.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: oprypin 2 | -------------------------------------------------------------------------------- /.github/workflows/autofix.yml: -------------------------------------------------------------------------------- 1 | name: Auto-fix 2 | on: 3 | push: 4 | pull_request: 5 | jobs: 6 | style: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Download source 10 | uses: actions/checkout@v4 11 | - name: Install Python 12 | uses: actions/setup-python@v5 13 | with: 14 | python-version: '3.12' 15 | - name: Install Hatch 16 | run: | 17 | pip install hatch 18 | - name: Install dependencies 19 | run: | 20 | hatch run style:pip freeze 21 | - name: Fix code style 22 | run: | 23 | hatch run style:fix --fix-only 24 | - name: Check if any edits are necessary 25 | run: | 26 | git diff --color --exit-code 27 | - name: Apply automatic fixes using pre-commit-ci-lite 28 | if: failure() && github.event_name == 'pull_request' 29 | uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0 30 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | pull_request: 5 | schedule: 6 | - cron: '0 6 * * 6' 7 | defaults: 8 | run: 9 | shell: bash 10 | jobs: 11 | test: 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | include: 16 | - python: '^3.13' 17 | os: ubuntu-latest 18 | - python: '3.13' 19 | os: macos-latest 20 | - python: '3.12' 21 | os: windows-latest 22 | - python: '3.11' 23 | os: ubuntu-latest 24 | - python: '3.10' 25 | os: macos-latest 26 | - python: '3.9' 27 | os: windows-latest 28 | - python: '3.9' 29 | os: ubuntu-latest 30 | versions: minimal 31 | runs-on: ${{matrix.os}} 32 | steps: 33 | - name: Download source 34 | uses: actions/checkout@v4 35 | - name: Install Python 36 | uses: actions/setup-python@v5 37 | with: 38 | python-version: ${{matrix.python}} 39 | - name: Pin to lowest versions 40 | if: matrix.versions == 'minimal' 41 | run: | 42 | sed -i -E 's/#min //; s/\b >=([0-9])/ ==\1/' pyproject.toml 43 | - name: Install Hatch 44 | run: | 45 | pip install hatch 46 | - name: Install dependencies 47 | run: | 48 | hatch run test:pip freeze 49 | - name: Run tests 50 | run: | 51 | hatch run test:test 52 | style: 53 | runs-on: ubuntu-latest 54 | steps: 55 | - name: Download source 56 | uses: actions/checkout@v4 57 | - name: Install Python 58 | uses: actions/setup-python@v5 59 | with: 60 | python-version: '3.12' 61 | - name: Install Hatch 62 | run: | 63 | pip install hatch 64 | - name: Install dependencies 65 | run: | 66 | hatch run style:pip freeze 67 | hatch run types:pip freeze 68 | - name: Check style 69 | if: always() 70 | run: | 71 | hatch run style:check 72 | - name: Check types 73 | if: always() 74 | run: | 75 | hatch run types:check 76 | -------------------------------------------------------------------------------- /.github/workflows/deploy-docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy docs 2 | on: 3 | push: 4 | pull_request: 5 | schedule: 6 | - cron: '0 6 * * 6' 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Download source 12 | uses: actions/checkout@v4 13 | - name: Install Python 14 | uses: actions/setup-python@v5 15 | with: 16 | python-version: '3.12' 17 | - name: Install dependencies 18 | run: pip install --no-deps -r requirements/requirements-docs.txt 19 | - name: Build site 20 | run: mkdocs build --strict 21 | - name: Upload to GitHub Pages 22 | uses: actions/upload-pages-artifact@v3 23 | with: 24 | path: site 25 | deploy: 26 | if: github.event_name == 'push' && github.ref_name == github.event.repository.default_branch 27 | needs: build 28 | permissions: 29 | pages: write 30 | id-token: write 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Deploy to GitHub Pages 34 | id: deployment 35 | uses: actions/deploy-pages@v4 36 | environment: 37 | name: github-pages 38 | url: ${{ steps.deployment.outputs.page_url }} 39 | -------------------------------------------------------------------------------- /.github/workflows/deploy-release.yml: -------------------------------------------------------------------------------- 1 | name: Deploy release 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | jobs: 7 | pypi: 8 | permissions: 9 | id-token: write 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Setup Python 14 | uses: actions/setup-python@v5 15 | with: 16 | python-version: '3.12' 17 | - name: Install dependencies 18 | run: pip install -U build 19 | - name: Build package 20 | run: python -m build 21 | - name: Publish to PyPI 22 | uses: pypa/gh-action-pypi-publish@release/v1 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | site*/ 3 | .mypy_cache/ 4 | .pytest_cache/ 5 | __pycache__/ 6 | *.egg-info/ 7 | .venv/ 8 | poetry.lock 9 | .vscode/ 10 | -------------------------------------------------------------------------------- /.tools/copier-answers.yml: -------------------------------------------------------------------------------- 1 | _commit: 1c7686408 2 | _src_path: gh:oprypin/py-project-template 3 | copyright_date: '2020' 4 | mkdocs: true 5 | mkdocs_install_self: false 6 | project_description: MkDocs plugin to specify the navigation in Markdown instead of YAML 7 | project_name: mkdocs-literate-nav 8 | pytest: true 9 | python_distribution_name: mkdocs-literate-nav 10 | python_source_path: mkdocs_literate_nav 11 | repository_name: oprypin/mkdocs-literate-nav 12 | script_test: false 13 | 14 | -------------------------------------------------------------------------------- /.tools/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -u -x 4 | cd "$(dirname "$0")/.." 5 | 6 | git diff --staged --quiet 7 | git diff --quiet HEAD pyproject.toml 8 | rm -rf dist 9 | hatch version "$1" 10 | hatch build 11 | git add mkdocs_literate_nav/__init__.py 12 | git commit -m "v$1" 13 | git tag -a -m "" "v$1" 14 | git push origin master --tags 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Oleh Prypin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mkdocs-literate-nav 2 | 3 | **[Plugin][] for [MkDocs][] to specify the navigation in Markdown instead of YAML** 4 | 5 | [![PyPI](https://img.shields.io/pypi/v/mkdocs-literate-nav)](https://pypi.org/project/mkdocs-literate-nav/) 6 | [![License](https://img.shields.io/github/license/oprypin/mkdocs-literate-nav)](https://github.com/oprypin/mkdocs-literate-nav/blob/master/LICENSE.md) 7 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/oprypin/mkdocs-literate-nav/ci.yml.svg)](https://github.com/oprypin/mkdocs-literate-nav/actions?query=event%3Apush+branch%3Amaster) 8 | 9 | ```shell 10 | pip install mkdocs-literate-nav 11 | ``` 12 | 13 | Works well with **[section-index][]** and **[gen-files][]**. Supplants **[awesome-pages][]**. 14 | 15 | [mkdocs]: https://www.mkdocs.org/ 16 | [plugin]: https://www.mkdocs.org/user-guide/plugins/ 17 | [section-index]: https://oprypin.github.io/mkdocs-section-index/ 18 | [gen-files]: https://oprypin.github.io/mkdocs-gen-files/ 19 | [awesome-pages]: https://github.com/lukasgeiter/mkdocs-awesome-pages-plugin 20 | 21 | ## Usage 22 | 23 | Activate the plugin in **mkdocs.yml**: 24 | 25 | ```yaml 26 | plugins: 27 | - search 28 | - literate-nav: 29 | nav_file: SUMMARY.md 30 | ``` 31 | 32 | and **drop** the `nav` section if it's present there; it will be ignored now. ([Unless you want to keep it?](#hybrid-nav)) 33 | 34 | 35 | 36 | 37 | 38 |
To get this navigation,create the file SUMMARY.md:(old YAML equivalent:)
39 | 40 | * [Frob](#index.md) 41 | * [Baz](#baz.md) 42 | * [Borgs](#borgs/index.md) 43 | * [Bar](#borgs/bar.md) 44 | * [Foo](#borgs/foo.md) 45 | 46 | 47 | 48 | ```markdown 49 | * [Frob](index.md) 50 | * [Baz](baz.md) 51 | * [Borgs](borgs/index.md) 52 | * [Bar](borgs/bar.md) 53 | * [Foo](borgs/foo.md) 54 | ``` 55 | 56 | 57 | 58 | ```yaml 59 | nav: 60 | - Frob: index.md 61 | - Baz: baz.md 62 | - Borgs: 63 | - borgs/index.md 64 | - Bar: borgs/bar.md 65 | - Foo: borgs/foo.md 66 | ``` 67 | 68 |
69 | 70 | IMPORTANT: The nav file must be put inside the [`docs` directory][docs_dir] -- at the root of it. 71 | 72 | So, the plugin lets you specify your site's navigation with lists of links that are parsed according to normal Markdown rules. 73 | 74 | Note that, the way we wrote the Markdown, a section seems to also have a page associated with it. MkDocs doesn't actually support that, and neither is it representable in YAML directly, so the plugin tries to do the next best thing: include the link as the first page of the section. However, this structure is perfectly suited for the *[section-index][]* plugin, which actually makes that work. Or you could just *not* associate a link with sections: 75 | 76 | 77 | 78 | 79 | 80 |
To get this navigation,create the file SUMMARY.md:(old YAML equivalent:)
81 | 82 | * [Frob](#index.md) 83 | * [Baz](#baz.md) 84 | * Borgs 85 | * [Bar](#borgs/bar.md) 86 | * [Foo](#borgs/foo.md) 87 | 88 | 89 | 90 | ```markdown 91 | * [Frob](index.md) 92 | * [Baz](baz.md) 93 | * Borgs 94 | * [Bar](borgs/bar.md) 95 | * [Foo](borgs/foo.md) 96 | ``` 97 | 98 | 99 | 100 | ```yaml 101 | nav: 102 | - Frob: index.md 103 | - Baz: baz.md 104 | - Borgs: 105 | - Bar: borgs/bar.md 106 | - Foo: borgs/foo.md 107 | ``` 108 | 109 |
110 | 111 | **[See syntax details about literate nav files.](https://oprypin.github.io/mkdocs-literate-nav/reference.html)** 112 | 113 | You can find more examples of the "literate nav" syntax [in the testcases directory](https://github.com/oprypin/mkdocs-literate-nav/tree/master/tests/nav). 114 | 115 | ### Nav cross-link 116 | 117 | But why stop there? Each directory can have its own decoupled navigation list (see how the trailing slash initiates a nav cross-link): 118 | 119 | 120 | 121 | 122 | 123 | 151 | 152 |
To get this navigation,create the file SUMMARY.md:(old YAML equivalent:)
124 | 125 | * [Frob](#index.md) 126 | * [Baz](#baz.md) 127 | * Borgs 128 | * [Bar](#borgs/bar.md) 129 | * [Foo](#borgs/foo.md) 130 | 131 | 132 | 133 | ```markdown 134 | * [Frob](index.md) 135 | * [Baz](baz.md) 136 | * [Borgs](borgs/) 137 | ``` 138 | 139 | 140 | 141 | ```yaml 142 | nav: 143 | - Frob: index.md 144 | - Baz: baz.md 145 | - Borgs: 146 | - Bar: borgs/bar.md 147 | - Foo: borgs/foo.md 148 | ``` 149 | 150 |
and the file borgs/SUMMARY.md:
153 | 154 | ```markdown 155 | * [Bar](bar.md) 156 | * [Foo](foo.md) 157 | ``` 158 | 159 |
160 | 161 | > NOTE: The nav file in the subdirectory is picked up *only* because its directory is explicitly mentioned in a parent nav file. **SUMMARY.md** (generally [`nav-file`](#customizing-nav_file)) files are **not** picked up implicitly (only the root nav file is "implicit"). 162 | > 163 | > So you might say that the nav construction approach is exactly the opposite from the *[awesome-pages][]* plugin. 164 | > 165 | > That said, an [inferred cross-linked directory](#inferred-sub-directory) (whether directly or through [wildcards](#wildcards)) gets resolved recursively, so that way you actually go back to implicit resolution. 166 | 167 | #### Inferred sub-directory 168 | 169 | Or perhaps you don't care about the order of the pages under the **borgs/** directory? Just drop the file __borgs/SUMMARY.md__ and let it be inferred (recursively, if applicable). For our particular example, the final result would be the same. 170 | 171 | The fallback behavior follows the [default behavior of MkDocs when nav isn't specified][mkdocs-nav], except that you can leave out only some directory trees, rather than an all-or-nothing choice. 172 | 173 | ### Wildcards 174 | 175 | Between the two extremes of entirely specifying a nav and entirely inferring it, there's the option of applying wildcards. 176 | 177 | Instead of putting links like `[Foo 1](foo_1.md)`, `[Foo 2](foo_2.md)` into the nav list, you can write a wildcard item: `foo_*.md` (bare, not as a link). The asterisk indicates that any number of characters can go there, and the file name has to match the rest of the pattern. 178 | 179 | A wildcard item is always required to have at least one `*` asterisk in it, because if it doesn't, then it's *just* a bare item, which are disallowed. 180 | 181 | **[See details about wildcards.](https://oprypin.github.io/mkdocs-literate-nav/reference.html#wildcards)** 182 | 183 | So this can be used to fully specify order for items that matter and apply wildcards for all other cases. Example: 184 | 185 | 186 | 187 | 188 | 189 |
By writing this literate nav file,you may get a nav like this:(assuming the files exist:)
190 | 191 | ```markdown 192 | - [Welcome](index.md) 193 | - Usage 194 | - [Foo](usage/foo.md) 195 | - usage/*.md 196 | - */ 197 | - *.md 198 | - [API docs](api/) 199 | - [License](license.md) 200 | ``` 201 | 202 | 203 | 204 | ```yaml 205 | - Welcome: index.md 206 | - Usage: 207 | - Foo: usage/foo.md 208 | - usage/bar.md 209 | - usage/baz.md 210 | - Tips: 211 | - tips/other-stuff.md 212 | - tips/stuff.md 213 | - changelog.md 214 | - credits.md 215 | - API docs: 216 | - api/Foo.md 217 | - Bar: 218 | - api/Bar/index.md 219 | - api/Bar/Baz.md 220 | - License: license.md 221 | ``` 222 | 223 | 224 | 225 | * index.md 226 | * changelog.md 227 | * credits.md 228 | * usage / bar.md 229 | * usage / baz.md 230 | * usage / foo.md 231 | * tips / stuff.md 232 | * tips / other-stuff.md 233 | * api / Foo.md 234 | * api / Bar / index.md 235 | * api / Bar / Baz.md 236 | 237 |
238 | 239 | TIP: Speaking of API docs... Want to fine-tune file ordering in a large directory tree? Check out [integrations with other plugins](#extras). 240 | 241 | The paths are relative to the directory that the nav file is in. Matching files in subdirectories also works, in both ways: `*/foo.md` and `foo/*.md`. 242 | 243 | As it's impossible for a user to specify the titles of items produced by a wildcard, they have to be inferred, based on [normal rules of MkDocs][mkdocs-nav]. 244 | 245 | > TIP: The ordering of items matches MkDocs' default, so first go all files, alphabetically (but with the index file first), then all directories. But, as an example, you could actually swap that, by writing: 246 | > 247 | > ```markdown 248 | > - */ 249 | > - * 250 | > ``` 251 | 252 | You can find more examples of the wildcard syntax [in the testcases directory](https://github.com/oprypin/mkdocs-literate-nav/tree/master/tests/nav/wildcard). 253 | 254 | ### Customizing `nav_file` 255 | 256 | We've been using **SUMMARY.md** as the name of the file that specifies the nav (actually that is also the default value of `nav_file`), but naturally, you can use any other file name. 257 | 258 | **[See details about the `nav_file` config.](https://oprypin.github.io/mkdocs-literate-nav/reference.html#nav_file)** 259 | 260 | The plugin takes care to not let MkDocs complain if you don't end up using the nav document as an actual page of your doc site. 261 | 262 | #### Show off your nav on the front page 263 | 264 | Or maybe you want the opposite -- make the nav page very prominent? You can actually use the index page, **README.md**, for the nav! 265 | 266 | Why would one do this? Well, GitHub (or another source hosting) also displays the Markdown files, and it's quite a nice perk to show off your navigation right in the index page of a directory. Of course, then you'd probably refrain from using [wildcards](#wildcards). [Directory cross linking](#nav-cross-link) still looks great, though. 267 | 268 | What's that, you ask? If the index page is taken up by navigation, we can't put any other content there, can we? Actually, we can! The nav list can just be put at the bottom of the page that also has whatever other content before that. 269 | 270 | [See an example of all this in action](https://github.com/oprypin/crsfml/tree/master/docs/tutorials) 271 | 272 | #### Explicit nav mark 273 | 274 | If the plugin is confused where in the document the nav is, or if you want to explicitly put it in a particular location, please precede the Markdown list with this HTML comment (verbatim) on a line of its own: 275 | 276 | ```html 277 | 278 | ``` 279 | 280 | ### Hybrid nav 281 | 282 | Do the features of this plugin interest you but you're not on board with the idea of migrating your whole nav? 283 | 284 | You can actually keep using [MkDocs' own nav specification][mkdocs-nav] at the root, *but* defer only some subdirectories to the *literate-nav* plugin. In that case make sure to *not* put a nav file at the [`docs` root][docs_dir], otherwise the native nav will be ignored. 285 | 286 | 287 | 288 | 289 | 290 | 319 | 320 |
To get this navigation,put this into mkdocs.yml:(old YAML equivalent:)
291 | 292 | * [Frob](#index.md) 293 | * [Baz](#baz.md) 294 | * Borgs 295 | * [Bar](#borgs/bar.md) 296 | * [Foo](#borgs/foo.md) 297 | 298 | 299 | 300 | ```yaml 301 | nav: 302 | - Frob: index.md 303 | - Baz: baz.md 304 | - Borgs: borgs/ 305 | ``` 306 | 307 | 308 | 309 | ```yaml 310 | nav: 311 | - Frob: index.md 312 | - Baz: baz.md 313 | - Borgs: 314 | - Bar: borgs/bar.md 315 | - Foo: borgs/foo.md 316 | ``` 317 | 318 |
& create the file borgs/SUMMARY.md:
321 | 322 | ```markdown 323 | * [Bar](bar.md) 324 | * [Foo](foo.md) 325 | ``` 326 | 327 |
328 | 329 | The syntax to defer to a subdirectory, just like [in a literate nav](#nav-cross-link), is to write an item that *ends* with a slash. 330 | 331 | NOTE: There is no way to use a YAML nav for a subdirectory, only a literate nav can be deferred. 332 | 333 | Wildcards also work very similarly. 334 | 335 | **[See details about syntax additions for MkDocs native nav.](https://oprypin.github.io/mkdocs-literate-nav/reference.html#mkdocs-native-nav)** 336 | 337 | You can find examples of the hybrid nav syntax [in the testcases directory](https://github.com/oprypin/mkdocs-literate-nav/tree/master/tests/nav/hybrid). 338 | 339 | #### MkDocs native nav with inferred subdirectories 340 | 341 | As before, whenever you have the option of using a literate nav file for a sub-directory, you can also *not* put any nav file there and infer the sub-directory instead. So, *not* creating the file **borgs/SUMMARY.md** would have yielded the same result in the above example. 342 | 343 | So basically, you can use the *literate-nav* plugin just for its ability to infer only sub-directories, without ever writing any actual "literate navs". 344 | 345 | #### Details about hybrid nav 346 | 347 | As a final example, note that there are two ways to include a subdirectory, with significant difference: 348 | 349 | 350 | 351 | 352 | 353 | 354 |
To get this navigation,put this into mkdocs.yml:To get this navigation,put this into mkdocs.yml:
355 | 356 | * [Frob](#index.md) 357 | * [Baz](#baz.md) 358 | * Borgs 359 | * [Bar](#borgs/bar.md) 360 | * [Foo](#borgs/foo.md) 361 | 362 | 363 | 364 | ```yaml 365 | nav: 366 | - Frob: index.md 367 | - Baz: baz.md 368 | - Borgs: borgs/ 369 | ``` 370 | 371 | 372 | 373 | * [Frob](#index.md) 374 | * [Baz](#baz.md) 375 | * [Bar](#borgs/bar.md) 376 | * [Foo](#borgs/foo.md) 377 | 378 | 379 | 380 | ```yaml 381 | nav: 382 | - Frob: index.md 383 | - Baz: baz.md 384 | - borgs/* 385 | ``` 386 | 387 |
388 | 389 | So, a directory item with a title becomes a section titled as such. And a wildcard (which can't have a title specified) gets inlined into the existing section. This simple example has no sub-sub-directories, but the relative subdirectory structure would be preserved in both cases if it did. 390 | 391 | ### Extras 392 | 393 | #### Programmatic control over the nav 394 | 395 | Let's say you need the ability to infer nav for a sub-directory, but are unhappy with the default naming/layout behavior, and you don't want to write all that out manually either. Then, definitely check out the ***[gen-files][]* plugin**. Its normal usage is to programmatically add files to the site during the build, but that also includes literate nav files! Moreover, you don't even have to teach your program to write Markdown. There's a more direct integration: `mkdocs_gen_files.Nav.build_literate_nav`. 396 | 397 | [See an example that generates both the files and the navigation covering them](https://github.com/mkdocstrings/mkdocstrings/blob/5802b1ef5ad9bf6077974f777bd55f32ce2bc219/docs/gen_doc_stubs.py#L25). 398 | 399 | #### Indent lists by 2 spaces, not 4 400 | 401 | Configure it through [tab_length](https://oprypin.github.io/mkdocs-literate-nav/reference.html#tab_length) or [markdown_extensions](https://oprypin.github.io/mkdocs-literate-nav/reference.html#markdown_extensions) 402 | 403 | #### Migrating from GitBook? 404 | 405 | It might be very easy! Just beware of the stricter Markdown parser; it will *not* accept 2-space indentation for sub-lists. 406 | 407 | And use this for **mkdocs.yml**: 408 | 409 |
410 | 411 | ```yaml 412 | use_directory_urls: false 413 | ``` 414 | ```yaml 415 | plugins: 416 | - search 417 | - same-dir 418 | - section-index 419 | - literate-nav: 420 | nav_file: SUMMARY.md 421 | ``` 422 | 423 | 424 | 425 | ```yaml 426 | theme: 427 | name: material 428 | ``` 429 | ```yaml 430 | markdown_extensions: 431 | - pymdownx.highlight 432 | - pymdownx.magiclink 433 | - pymdownx.superfences 434 | ``` 435 | 436 |
437 | 438 | 439 | 440 | [mkdocs-nav]: https://www.mkdocs.org/user-guide/writing-your-docs/#configure-pages-and-navigation 441 | [docs_dir]: https://www.mkdocs.org/user-guide/configuration/#docs_dir 442 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --8<-- "README.md" 2 | -------------------------------------------------------------------------------- /docs/reference.md: -------------------------------------------------------------------------------- 1 | ## Literate nav syntax 2 | 3 | The literate nav file must be a Markdown file, which can contain arbitrary content, but the **last-encountered Markdown list** in it will be used to determine the navigation. It must adhere to [specific rules](#nav-list-syntax), otherwise an error will be generated. 4 | 5 | ### Explicit nav mark 6 | 7 | If any line in the Markdown file has the exact content "``", that changes the choice of nav list for that file to instead be the first-encountered list after such a line. 8 | 9 | ### Nav list syntax 10 | 11 | !!! example "SUMMARY.md" 12 | 13 | ```markdown 14 | * [First list item](some-page.md) 15 | * Subsection title 16 | * [Something](subdirectory/something.md) 17 | * subdirectory/*.md 18 | * [Other directory](other/) 19 | ``` 20 | 21 | The navigation list must be a top-level [Markdown list](https://daringfireball.net/projects/markdown/syntax#list), regardless if it's an ordered list or not. It can contain other sub-lists, which are used as sub-directories for the nav. 22 | 23 | To include a page into the nav, write a [Markdown link](https://daringfireball.net/projects/markdown/syntax#link) such as `[Page title](path/to/some-page.md)`. Unlike with the [native nav](#mkdocs-native-nav), a title is mandatory; bare paths are an error (unless they contain an asterisk; see below). 24 | 25 | Links in the nav file directly under the root [`docs` dir][docs_dir] are relative to the `docs` dir, and for nav files that are in a subdirectory, links are relative to that subdirectory (generally referred to as "current" directory). Links can refer to files that are in other directories, both below (`sub/dir/foo.md`) and above (`../foo.md`) the current directory -- though the latter is not as well supported. The forward slash `/` must be used as the path separator. 26 | 27 | To add a sub-section into a nav, make a list item that is not a link (so its text becomes the section title) and start a nested list under it (so its content turns into the section nav). The rules apply recursively to that list. 28 | 29 | The parser makes some effort to allow *and strip* other inline Markdown markup (e.g. italics), but this is generally unsupported. 30 | 31 | ### Wildcards 32 | 33 | Bare paths that contain an `*` asterisk are recognized as [wildcards](#wildcards). Asterisks are special *only* in bare items; they don't do anything inside a Markdown link. 34 | 35 | A wildcard item, whenever encountered in a list, will be replaced with every file *and directory* that matches it and is not mentioned in the nav explicitly already *and* hasn't been matched by any preceding wildcard items. It is possible to select only directories by adding a trailing slash, like `*/`. And to distinguish files, you have to rely on them having a file extension, and write e.g. `*.md`. 36 | 37 | Wildcards can traverse to subdirectories and parent directories as well. However, the directly matching items will be included *flatly* into the current nav list -- though if a *directory* matches, its sub-items will not be flattened. 38 | 39 | Currently the only officially supported special character in a wildcard is an `*` asterisk. It indicates that there can be 0 or more arbitrary characters (excluding the path separator) in its place. 40 | 41 | Resolution of wildcards is done in a particular order, depth-first from the perspective of the final layout of the nav. The reason that the order is important is that *literate-nav* always tries to exclude items from a wildcard that was already mentioned elsewhere in the nav, so the items are not duplicated. If the two relevant occurrences are somewhere in the same directory tree (one nav section is a parent of the other), the detection will always just work. If they are in separate directory trees, order starts to matter: only items mentioned "earlier" (in top-to-bottom reading order) will be omitted from wildcards occurring "later". The same ordering applies if there are two wildcards competing for the same items: only the "earlier" one will contain those items. 42 | 43 | #### Subdirectory cross-link 44 | 45 | If a link's destination ends with a `/` slash, it is instead recognized as a [subdirectory cross-link](#subdirectory-cross-link). 46 | 47 | A link that leads to a path ending with `/` is understood to be a directory cross-link. If such a directory actually exists relative to the current directory, the nav for it is inserted into this place. If there is no such subdirectory, the link text is kept as is and is likely invalid in the end. 48 | 49 | If that subdirectory has no [nav file](#nav_file), then writing a directory cross-link means including that directory as a sub-section that recursively includes all the directory's items. I.e. the following two approaches are fully equivalent then: 50 | 51 | ```markdown 52 | * [Foo](foo/) 53 | ``` 54 | 55 | ```markdown 56 | * Foo 57 | * foo/* 58 | ``` 59 | 60 | But if that subdirectory *does* have a nav file, then that is resolved in the context of that subdirectory and put back into the nav under the subsection. 61 | 62 | 63 | 64 | ## MkDocs native nav 65 | 66 | If there is no literate nav file in the [`docs` dir][docs_dir], this plugin will fall back to using the [normal `nav:` defined in the file `mkdocs.yml`](https://www.mkdocs.org/user-guide/writing-your-docs/#configure-pages-and-navigation). But its items gain extended syntax. 67 | 68 | !!! example "mkdocs.yml" 69 | ```yaml 70 | nav: 71 | - Foo: foo.md 72 | - Usual: 73 | - usual/a.md 74 | - usual/b.md 75 | - '*.md' 76 | - Subdir: subdir/ 77 | ``` 78 | 79 | In this example only the last two items are special. 80 | 81 | Wildcards (items without a title that have an asterisk in them) get replaced by files that they match, relative to the root [`docs` dir][docs_dir]. The resolution rules are the same as [wildcards in a literate nav](#wildcards). 82 | 83 | [Subdirectory cross-link](#subdirectory-cross-link) items (items with a title and a link that ends with a slash) get replaced by the literate nav for the linked directory (if it exists), under a section with this title. 84 | 85 | ## MkDocs plugin 86 | 87 | !!! example "mkdocs.yml" 88 | ```yaml 89 | plugins: 90 | - literate-nav: 91 | nav_file: SUMMARY.md 92 | implicit_index: false 93 | tab_length: 4 94 | ``` 95 | 96 | ### Config 97 | 98 | #### `nav_file` 99 | 100 | *string, default `'SUMMARY.md'`* 101 | 102 | The name of the file to read to determine the navigation for a particular directory under [`docs_dir`][docs_dir]. E.g. if the directory `docs/foo/bar/` is referenced, the file `docs/foo/bar/SUMMARY.md` will be read for it. 103 | 104 | This file must contain [a Markdown list](#literate-nav-syntax) that defines the navigation for that directory. If for a particular directory there is no such file, the navigation will be inferred automatically, following [normal MkDocs rules](https://www.mkdocs.org/user-guide/writing-your-docs/#configure-pages-and-navigation). If there is no such file for the [root `docs_dir`][], the nav can fall back to [MkDocs native nav](#mkdocs-native-nav). 105 | 106 | Although there can be several such files throughout the site, the choice of the file name is global and not modifiaable on a case-by-case basis. 107 | 108 | #### `implicit_index` 109 | 110 | *boolean, default `false`* 111 | 112 | If a directory has a file named [`index.md` or `README.md`](https://www.mkdocs.org/user-guide/writing-your-docs/#index-pages), but the literate nav for that directory that never includes it, it will be inserted as the first item of the nav. 113 | 114 | This is important when using directory cross-linking, which otherwise makes it impossible to specify a *[section-index][]* page for a subdirectory. 115 | 116 | #### `tab_length` 117 | 118 | *integer, default `4`* 119 | 120 | By default (like in MkDocs), lists need to be indented by 4 spaces. The more modern style is 2 spaces, though. 121 | 122 | You can change the indentation just for the extension, but that will not affect MkDocs' rendering. If you want to change both at once, install [mdx_truly_sane_lists](https://github.com/radude/mdx_truly_sane_lists) and use it through `markdown_extensions`, instead of this option. See example below. 123 | 124 | #### `markdown_extensions` 125 | 126 | *list of mappings, [same as MkDocs](https://www.mkdocs.org/user-guide/configuration/#markdown_extensions)* 127 | 128 | !!! example "mkdocs.yml" 129 | ```yaml 130 | plugins: 131 | - literate-nav: 132 | markdown_extensions: 133 | - mdx_truly_sane_lists 134 | 135 | markdown_extensions: 136 | - mdx_truly_sane_lists 137 | ``` 138 | 139 | [mkdocs-nav]: https://www.mkdocs.org/user-guide/writing-your-docs/#configure-pages-and-navigation 140 | [docs_dir]: https://www.mkdocs.org/user-guide/configuration/#docs_dir 141 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: "mkdocs-literate-nav" 2 | site_description: "MkDocs plugin to specify the navigation in Markdown instead of YAML" 3 | site_url: "https://oprypin.github.io/mkdocs-literate-nav" 4 | repo_url: "https://github.com/oprypin/mkdocs-literate-nav" 5 | edit_uri: blob/master/docs/ 6 | use_directory_urls: false 7 | 8 | theme: 9 | name: material 10 | features: 11 | - content.action.edit 12 | - navigation.tabs 13 | - toc.integrate 14 | icon: 15 | repo: fontawesome/brands/github 16 | palette: 17 | primary: teal 18 | accent: purple 19 | 20 | markdown_extensions: 21 | - pymdownx.details 22 | - pymdownx.highlight 23 | - pymdownx.magiclink 24 | - pymdownx.saneheaders 25 | - pymdownx.superfences 26 | - pymdownx.snippets: 27 | check_paths: true 28 | base_path: 29 | - !relative $config_dir 30 | - callouts 31 | - admonition 32 | - md_in_html 33 | - toc: 34 | permalink: "#" 35 | 36 | validation: 37 | omitted_files: warn 38 | absolute_links: warn 39 | unrecognized_links: warn 40 | nav: 41 | absolute_links: ignore 42 | 43 | nav: 44 | - section-index: /mkdocs-section-index/ 45 | - literate-nav: 46 | - Overview: README.md 47 | - Reference: reference.md 48 | - gen-files: /mkdocs-gen-files/ 49 | - same-dir: /mkdocs-same-dir/ 50 | - code-validator: /mkdocs-code-validator/ 51 | - callouts: /markdown-callouts/ 52 | -------------------------------------------------------------------------------- /mkdocs_literate_nav/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.6.2" 2 | -------------------------------------------------------------------------------- /mkdocs_literate_nav/exceptions.py: -------------------------------------------------------------------------------- 1 | class LiterateNavError(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /mkdocs_literate_nav/parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import copy 4 | import functools 5 | import itertools 6 | import logging 7 | import posixpath 8 | import urllib.parse 9 | import xml.etree.ElementTree as etree 10 | from collections.abc import Iterator 11 | from typing import TYPE_CHECKING, Callable, Optional, Union, cast 12 | 13 | import markdown 14 | import markdown.extensions 15 | import markdown.postprocessors 16 | import markdown.preprocessors 17 | import markdown.treeprocessors 18 | import mkdocs.utils 19 | 20 | from mkdocs_literate_nav import exceptions 21 | 22 | if TYPE_CHECKING: 23 | from .plugin import MkDocsGlobber 24 | 25 | 26 | log = logging.getLogger(f"mkdocs.plugins.{__name__}") 27 | 28 | _unescape: Callable[[str], str] 29 | try: 30 | _unescape = markdown.treeprocessors.UnescapeTreeprocessor().unescape 31 | except AttributeError: 32 | _unescape = markdown.postprocessors.UnescapePostprocessor().run # type: ignore[attr-defined] 33 | 34 | 35 | class Wildcard: 36 | trim_slash = False 37 | 38 | def __init__(self, *path_parts: str, fallback: bool = True): 39 | norm = posixpath.normpath(posixpath.join(*path_parts).lstrip("/")) 40 | if path_parts[-1].endswith("/") and not self.trim_slash: 41 | norm += "/" 42 | self.value = norm 43 | self.fallback = path_parts[-1] if fallback else None 44 | 45 | def __repr__(self): 46 | return f"{type(self).__name__}({self.value!r})" 47 | 48 | 49 | if TYPE_CHECKING: 50 | NavWithWildcardsItem = Union[ 51 | Wildcard, 52 | str, 53 | "NavWithWildcards", 54 | dict[Optional[str], Union[Wildcard, str, "NavWithWildcards"]], 55 | ] 56 | NavWithWildcards = list[NavWithWildcardsItem] 57 | 58 | NavItem = Union[str, dict[Optional[str], Union[str, "Nav"]]] 59 | Nav = list[NavItem] 60 | 61 | RootStack = tuple[str, ...] 62 | 63 | 64 | class DirectoryWildcard(Wildcard): 65 | trim_slash = True 66 | 67 | 68 | class NavParser: 69 | def __init__( 70 | self, 71 | get_nav_for_dir: Callable[[str], tuple[str, str] | None], 72 | globber: MkDocsGlobber, 73 | *, 74 | implicit_index: bool = False, 75 | markdown_config: dict | None = None, 76 | ): 77 | self.get_nav_for_dir = get_nav_for_dir 78 | self.globber = globber 79 | self.implicit_index = implicit_index 80 | self._markdown_config = markdown_config or {} 81 | self.seen_items: set[str] = set() 82 | self._warn = functools.lru_cache()(log.warning) 83 | 84 | def markdown_to_nav(self, roots: tuple[str, ...] = (".",)) -> Nav: 85 | root = roots[0] 86 | 87 | if dir_nav := self.get_nav_for_dir(root): 88 | nav_file_name, markdown_content = dir_nav 89 | nav = _extract_nav_from_content(self._markdown_config, markdown_content) 90 | 91 | if nav is not None: 92 | self_path = posixpath.normpath(posixpath.join(root, nav_file_name)) 93 | if not (self.implicit_index and self_path == self.globber.find_index(root)): 94 | self.seen_items.add(self_path) 95 | 96 | first_item: Wildcard | None = None 97 | if self.implicit_index: 98 | if found_index := self.globber.find_index(root): 99 | first_item = Wildcard(root, "/" + found_index, fallback=False) 100 | return self._resolve_wildcards( 101 | self._list_element_to_nav(nav, root, first_item), roots 102 | ) 103 | 104 | log.debug(f"Navigation for {root!r} will be inferred.") 105 | return self._resolve_wildcards([Wildcard(root, "*", fallback=False)], roots) 106 | 107 | def _list_element_to_nav( 108 | self, section: etree.Element, root: str, first_item: Wildcard | str | None = None 109 | ) -> NavWithWildcards: 110 | assert section.tag in _LIST_TAGS 111 | result: NavWithWildcards = [] 112 | if first_item is not None: 113 | if isinstance(first_item, str): 114 | self.seen_items.add(first_item) 115 | result.append(first_item) 116 | for item in section: 117 | assert item.tag == "li" 118 | out_title = item.text 119 | out_item: Wildcard | str | list | None = None 120 | 121 | children = _iter_children_without_tail(item) 122 | try: 123 | child = next(children) 124 | if not out_title and child.tag == "a": 125 | if link := child.get("href"): 126 | out_item = self._resolve_string_item(root, link) 127 | out_title = _unescape("".join(child.itertext())) 128 | child = next(children) 129 | if child.tag in _LIST_TAGS: 130 | out_item = self._list_element_to_nav( 131 | child, root, cast("Union[Wildcard, str, None]", out_item) 132 | ) 133 | child = next(children) 134 | except StopIteration: 135 | error = "" 136 | else: 137 | error = f"Expected no more elements, but got {_to_short_string(child)}.\n" 138 | if out_title is None: 139 | error += "Did not find any title specified." + _EXAMPLES 140 | elif out_item is None: 141 | if "*" in out_title: 142 | out_item = Wildcard(root, out_title) 143 | out_title = None 144 | else: 145 | error += "Did not find any item/section content specified." + _EXAMPLES 146 | if error: 147 | raise LiterateNavParseError(error, item) 148 | 149 | assert out_item is not None 150 | if type(out_item) in (str, list, DirectoryWildcard) and out_title is not None: 151 | result.append({out_title: out_item}) 152 | else: 153 | result.append(out_item) 154 | return result 155 | 156 | def _resolve_string_item(self, root: str, link: str) -> Wildcard | str: 157 | parsed = urllib.parse.urlsplit(link) 158 | if parsed.scheme or parsed.netloc: 159 | return link 160 | 161 | abs_link = posixpath.normpath(posixpath.join(root, link)) 162 | self.seen_items.add(abs_link) 163 | if link.endswith("/") and self.globber.isdir(abs_link): 164 | return DirectoryWildcard(root, link) 165 | return abs_link 166 | 167 | def _resolve_wildcards(self, nav: NavWithWildcards, roots: RootStack = (".",)) -> Nav: 168 | def can_recurse(new_root: str) -> bool: 169 | if new_root in roots: 170 | rec = " -> ".join(repr(r) for r in reversed((new_root, *roots))) 171 | self._warn(f"Disallowing recursion {rec}") 172 | return False 173 | return True 174 | 175 | # Ensure depth-first processing, so separate loop for recursive calls first. 176 | for entry in nav: 177 | if isinstance(entry, dict) and len(entry) == 1: 178 | [(key, val)] = entry.items() 179 | if isinstance(val, str): 180 | entry = val 181 | if isinstance(entry, str): 182 | self.seen_items.add(entry) 183 | 184 | resolved: Nav = [] 185 | for entry in nav: 186 | if isinstance(entry, dict) and len(entry) == 1: 187 | [(key, val)] = entry.items() 188 | new_val: str | Nav | None = None 189 | if isinstance(val, list): 190 | new_val = self._resolve_wildcards(val, roots) 191 | elif isinstance(val, DirectoryWildcard): 192 | new_val = ( 193 | self.markdown_to_nav((val.value, *roots)) 194 | if can_recurse(val.value) 195 | else val.fallback 196 | ) 197 | elif isinstance(val, Wildcard): 198 | new_val = self._resolve_wildcards([val], roots) or val.fallback 199 | else: 200 | new_val = val 201 | if new_val: 202 | resolved.append({key: new_val}) 203 | continue 204 | 205 | assert not isinstance(entry, (DirectoryWildcard, list, dict)) 206 | if not isinstance(entry, Wildcard): 207 | resolved.append(entry) 208 | continue 209 | 210 | any_matches = False 211 | for item in self.globber.glob(entry.value.rstrip("/")): 212 | any_matches = True 213 | if item in self.seen_items: 214 | continue 215 | if self.globber.isdir(item): 216 | title = mkdocs.utils.dirname_to_title(posixpath.basename(item)) 217 | if subitems := self.markdown_to_nav((item, *roots)): 218 | resolved.append({title: subitems}) 219 | else: 220 | if entry.value.endswith("/"): 221 | continue 222 | resolved.append({None: item}) 223 | self.seen_items.add(item) 224 | if not any_matches and entry.fallback: 225 | resolved.append(entry.fallback) 226 | return resolved 227 | 228 | def resolve_yaml_nav(self, nav) -> Nav: 229 | if not isinstance(nav, list): 230 | return nav 231 | return self._resolve_wildcards([self._resolve_yaml_nav(x) for x in nav]) 232 | 233 | def _resolve_yaml_nav(self, item) -> NavWithWildcardsItem: 234 | if isinstance(item, str) and "*" in item: 235 | return Wildcard("", item) 236 | if isinstance(item, dict): 237 | assert len(item) == 1 238 | [(key, val)] = item.items() 239 | if isinstance(val, list): 240 | return {key: [self._resolve_yaml_nav(x) for x in val]} 241 | if isinstance(val, str): 242 | if "*" in val: 243 | return {key: Wildcard("", val)} 244 | return {key: self._resolve_string_item("", val)} 245 | return {key: val} 246 | return item 247 | 248 | 249 | def _extract_nav_from_content(markdown_config: dict, markdown_content: str) -> etree.Element | None: 250 | md = markdown.Markdown(**markdown_config) 251 | md.inlinePatterns.deregister("html", strict=False) 252 | md.inlinePatterns.deregister("entity", strict=False) 253 | preprocessor = _Preprocessor(md) 254 | preprocessor._register() 255 | treeprocessor = _Treeprocessor(md) 256 | treeprocessor._register() 257 | md.convert(markdown_content) 258 | return treeprocessor.nav 259 | 260 | 261 | class _Preprocessor(markdown.preprocessors.Preprocessor): 262 | nav_placeholder: str | None = None 263 | 264 | def run(self, lines: list[str]) -> list[str]: 265 | for i, line in enumerate(lines): 266 | if line.strip() == "": 267 | self.nav_placeholder = self.md.htmlStash.store("") 268 | lines[i] = self.nav_placeholder + "\n" 269 | return lines 270 | 271 | def _register(self) -> None: 272 | self.md.preprocessors.register(self, "mkdocs_literate_nav", priority=25) 273 | 274 | 275 | class _Treeprocessor(markdown.treeprocessors.Treeprocessor): 276 | nav: etree.Element | None = None 277 | 278 | def run(self, root: etree.Element) -> None: 279 | preprocessor: _Preprocessor = self.md.preprocessors["mkdocs_literate_nav"] # type: ignore[assignment] 280 | nav_placeholder = preprocessor.nav_placeholder 281 | items: Iterator[etree.Element] 282 | if nav_placeholder is not None: 283 | # Will look for the first list after the last . 284 | items = itertools.dropwhile(lambda el: el.text != nav_placeholder, root) 285 | else: 286 | # Will look for the last list. 287 | items = reversed(root) 288 | for el in items: 289 | if el.tag in _LIST_TAGS: 290 | self.nav = copy.deepcopy(el) 291 | break 292 | 293 | def _register(self) -> None: 294 | self.md.treeprocessors.register(self, "mkdocs_literate_nav", priority=19) 295 | 296 | 297 | _LIST_TAGS = ("ul", "ol") 298 | _EXAMPLES = """ 299 | Examples: 300 | * [Item title](item_content.md) 301 | * Section title 302 | * [Sub content](sub/content.md) 303 | * *.md 304 | """ 305 | 306 | 307 | def _iter_children_without_tail(element: etree.Element) -> Iterator[etree.Element]: 308 | for child in element: 309 | yield child 310 | if child.tail: 311 | raise LiterateNavParseError( 312 | f"Expected no text after {_to_short_string(child)}, but got {child.tail!r}.", 313 | element, 314 | ) 315 | 316 | 317 | def _to_short_string(el: etree.Element) -> str: 318 | el = copy.deepcopy(el) 319 | for child in el: 320 | if len(child): 321 | del child[:] 322 | child.text = "[...]" 323 | el.tail = None 324 | return etree.tostring(el, encoding="unicode") 325 | 326 | 327 | class LiterateNavParseError(exceptions.LiterateNavError): 328 | def __init__(self, message, el): 329 | super().__init__(message + "\nThe problematic item:\n\n" + _to_short_string(el)) 330 | -------------------------------------------------------------------------------- /mkdocs_literate_nav/plugin.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import fnmatch 4 | import logging 5 | import os.path 6 | import re 7 | from collections.abc import Iterator 8 | from pathlib import Path, PurePosixPath 9 | from typing import TYPE_CHECKING 10 | 11 | import mkdocs.structure.files 12 | from mkdocs.config import config_options as opt 13 | from mkdocs.config.base import Config 14 | from mkdocs.plugins import BasePlugin, event_priority 15 | from mkdocs.structure.pages import Page 16 | 17 | from mkdocs_literate_nav import parser 18 | 19 | if TYPE_CHECKING: 20 | from mkdocs.config.defaults import MkDocsConfig 21 | from mkdocs.structure.files import Files 22 | from mkdocs.structure.nav import Navigation 23 | 24 | 25 | log = logging.getLogger(f"mkdocs.plugins.{__name__}") 26 | 27 | 28 | class PluginConfig(Config): 29 | nav_file = opt.Type(str, default="SUMMARY.md") 30 | implicit_index = opt.Type(bool, default=False) 31 | markdown_extensions = opt.MarkdownExtensions() 32 | tab_length = opt.Type(int, default=4) 33 | 34 | 35 | class LiterateNavPlugin(BasePlugin[PluginConfig]): 36 | @event_priority(-100) # Run last 37 | def on_files(self, files: Files, config: MkDocsConfig) -> None: 38 | config.nav = resolve_directories_in_nav( 39 | config.nav, 40 | files, 41 | nav_file_name=self.config.nav_file, 42 | implicit_index=self.config.implicit_index, 43 | markdown_config=dict( 44 | extensions=self.config.markdown_extensions, 45 | extension_configs=self.config["mdx_configs"], 46 | tab_length=self.config.tab_length, 47 | ), 48 | ) 49 | self._files = files 50 | 51 | def on_nav(self, nav: Navigation, config: MkDocsConfig, files: Files) -> None: 52 | if files != getattr(self, "_files", None): 53 | log.warning( 54 | "The literate-nav plugin created the nav based on files that were subsequently modified by another MkDocs plugin! " 55 | "Re-order `plugins` in mkdocs.yml so that 'literate-nav' appears later." 56 | ) 57 | 58 | 59 | def resolve_directories_in_nav( 60 | nav_data, 61 | files: Files, 62 | nav_file_name: str, 63 | *, 64 | implicit_index: bool, 65 | markdown_config: dict | None = None, 66 | ): 67 | """Walk through a standard MkDocs nav config and replace `directory/` references. 68 | 69 | Directories, if found, are resolved by the rules of literate nav insertion: 70 | If it has a literate nav file, that is used. Otherwise an implicit nav is generated. 71 | """ 72 | 73 | def get_nav_for_dir(path: str) -> tuple[str, str] | None: 74 | file = files.get_file_from_path(os.path.join(path, nav_file_name)) 75 | if not file: 76 | return None 77 | log.debug(f"Navigation for {path!r} based on {file.src_path!r}.") 78 | 79 | # Prevent the warning in case the user doesn't also end up including this page in 80 | # the final nav, maybe they want it only for the purpose of feeding to this plugin. 81 | try: # MkDocs 1.5+ 82 | if file.inclusion.is_in_nav(): 83 | file.inclusion = mkdocs.structure.files.InclusionLevel.NOT_IN_NAV 84 | except AttributeError: 85 | # https://github.com/mkdocs/mkdocs/blob/ff0b726056/mkdocs/structure/nav.py#L113 86 | Page(None, file, {}) # type: ignore[arg-type] 87 | 88 | try: # MkDocs 1.6+ 89 | content = file.content_string 90 | except AttributeError: 91 | # https://github.com/mkdocs/mkdocs/blob/fa5aa4a26e/mkdocs/structure/pages.py#L120 92 | assert file.abs_src_path is not None 93 | content = Path(file.abs_src_path).read_text(encoding="utf-8-sig") 94 | 95 | return nav_file_name, content 96 | 97 | globber = MkDocsGlobber(files) 98 | nav_parser = parser.NavParser( 99 | get_nav_for_dir, globber, implicit_index=implicit_index, markdown_config=markdown_config 100 | ) 101 | 102 | result = None 103 | if not nav_data or get_nav_for_dir("."): 104 | result = nav_parser.markdown_to_nav() 105 | return result or nav_parser.resolve_yaml_nav(nav_data or []) 106 | 107 | 108 | class MkDocsGlobber: 109 | def __init__(self, files: Files): 110 | self.files = {} # Ordered set 111 | self.dirs = {} # Ordered set 112 | self.index_dirs = {} 113 | for f in files: 114 | if not f.is_documentation_page(): 115 | continue 116 | path = PurePosixPath("/", f.src_uri) 117 | self.files[path] = True 118 | tail, head = path.parent, path.name 119 | if f.name == "index": 120 | self.index_dirs[tail] = path 121 | while True: 122 | self.dirs[tail] = True 123 | if not head: 124 | break 125 | tail, head = tail.parent, tail.name 126 | 127 | def isdir(self, path: str) -> bool: 128 | return PurePosixPath("/", path) in self.dirs 129 | 130 | def glob(self, pattern: str) -> Iterator[str]: 131 | pat_parts = PurePosixPath("/" + pattern).parts 132 | re_parts = [re.compile(fnmatch.translate(part)) for part in pat_parts] 133 | 134 | for collection in self.files, self.dirs: 135 | for path in collection: 136 | if len(path.parts) == len(re_parts): 137 | zipped = zip(path.parts, re_parts) 138 | next(zipped) # Both the path and the pattern have a slash as their first part. 139 | if all(re_part.match(part) for part, re_part in zipped): 140 | yield str(path)[1:] 141 | 142 | def find_index(self, root: str) -> str | None: 143 | root_path = PurePosixPath("/", root) 144 | if root_path in self.index_dirs: 145 | return str(self.index_dirs[root_path])[1:] 146 | return None 147 | -------------------------------------------------------------------------------- /mkdocs_literate_nav/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oprypin/mkdocs-literate-nav/7423f00fe2c26c879310f76631b8180cb2bb6ace/mkdocs_literate_nav/py.typed -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "mkdocs-literate-nav" 7 | description = "MkDocs plugin to specify the navigation in Markdown instead of YAML" 8 | readme = "README.md" 9 | license = "MIT" 10 | keywords = ["mkdocs", "mkdocs-plugin"] 11 | authors = [ 12 | {name = "Oleh Prypin", email = "oleh@pryp.in"}, 13 | ] 14 | classifiers = [ 15 | "Development Status :: 4 - Beta", 16 | "Environment :: Console", 17 | "Environment :: Web Environment", 18 | "Intended Audience :: Information Technology", 19 | "License :: OSI Approved :: MIT License", 20 | "Operating System :: OS Independent", 21 | "Programming Language :: Python", 22 | # Begin Python versions 23 | "Programming Language :: Python :: 3.9", 24 | "Programming Language :: Python :: 3.10", 25 | "Programming Language :: Python :: 3.11", 26 | "Programming Language :: Python :: 3.12", 27 | "Programming Language :: Python :: 3.13", 28 | # End Python versions 29 | "Topic :: Documentation", 30 | "Topic :: Software Development :: Documentation", 31 | "Topic :: Text Processing :: Markup :: Markdown", 32 | "Typing :: Typed", 33 | ] 34 | dynamic = ["version"] 35 | requires-python = ">=3.9" 36 | dependencies = [ 37 | "mkdocs >=1.4.1", 38 | ] 39 | 40 | [project.urls] 41 | Documentation = "https://oprypin.github.io/mkdocs-literate-nav/" 42 | Source = "https://github.com/oprypin/mkdocs-literate-nav" 43 | Issues = "https://github.com/oprypin/mkdocs-literate-nav/issues" 44 | History = "https://github.com/oprypin/mkdocs-literate-nav/releases" 45 | 46 | [project.entry-points."mkdocs.plugins"] 47 | literate-nav = "mkdocs_literate_nav.plugin:LiterateNavPlugin" 48 | 49 | [tool.hatch.version] 50 | path = "mkdocs_literate_nav/__init__.py" 51 | 52 | [tool.hatch.build.targets.sdist] 53 | include = ["/mkdocs_literate_nav", "/tests"] 54 | 55 | [tool.hatch.env] 56 | requires = [ 57 | "hatch-mkdocs", 58 | "hatch-pip-compile >=1.7.0", 59 | ] 60 | 61 | [tool.hatch.envs.default.scripts] 62 | all = [ 63 | "hatch run style:fix", 64 | "hatch run types:check", 65 | "hatch run test:test", 66 | ] 67 | 68 | [tool.hatch.envs.test] 69 | dependencies = [ 70 | "pytest", 71 | "pytest-golden", 72 | ] 73 | [tool.hatch.envs.test.scripts] 74 | test = [ 75 | "pytest -q {args}", 76 | ] 77 | 78 | [tool.hatch.envs.types] 79 | dependencies = [ 80 | "mypy", 81 | "types-Markdown >=3.4.2", 82 | ] 83 | [tool.hatch.envs.types.scripts] 84 | check = "mypy {args} mkdocs_literate_nav" 85 | 86 | [tool.hatch.envs.style] 87 | type = "pip-compile" 88 | detached = true 89 | dependencies = [ 90 | "ruff", 91 | ] 92 | [tool.hatch.envs.style.scripts] 93 | check = "ruff check mkdocs_literate_nav tests {args}" 94 | format = "ruff format -q mkdocs_literate_nav tests" 95 | fix = [ 96 | "check --fix --unsafe-fixes", 97 | "format", 98 | ] 99 | 100 | [tool.hatch.env.collectors.mkdocs.docs] 101 | [tool.hatch.envs.docs] 102 | type = "pip-compile" 103 | 104 | [tool.ruff] 105 | line-length = 100 106 | [tool.ruff.lint] 107 | preview = true 108 | select = [ 109 | "F", "E", "W", "I", "UP", "YTT", "ASYNC", "FBT", "C4", "DTZ", "T10", "FA", "ISC", "PIE", "T20", "RSE", 110 | "N803", "N804", "N805", "N806", "N807", "N815", "N816", "N999", 111 | "S201", "S202", "S303", "S304", "S305", "S306", "S506", "S602", "S604", "S605", "S612", 112 | "B002", "B003", "B004", "B005", "B007", "B008", "B009", "B010", "B011", "B012", "B013", "B014", "B015", "B016", "B017", "B018", "B019", "B020", "B021", "B022", "B023", "B025", "B026", "B029", "B030", "B031", "B032", "B033", "B034", "B035", "B039", "B905", "B909", "B911", 113 | "COM818", 114 | "LOG001", "LOG007", "LOG009", "LOG015", 115 | "G001", "G002", "G003", "G010", "G101", "G201", "G202", 116 | "PYI001", "PYI002", "PYI003", "PYI004", "PYI005", "PYI006", "PYI007", "PYI008", "PYI009", "PYI010", "PYI011", "PYI012", "PYI013", "PYI014", "PYI015", "PYI016", "PYI017", "PYI018", "PYI019", "PYI020", "PYI021", "PYI024", "PYI025", "PYI026", "PYI029", "PYI030", "PYI032", "PYI033", "PYI034", "PYI035", "PYI036", "PYI041", "PYI042", "PYI043", "PYI044", "PYI045", "PYI046", "PYI047", "PYI048", "PYI049", "PYI050", "PYI051", "PYI052", "PYI053", "PYI054", "PYI055", "PYI056", "PYI057", "PYI058", "PYI059", "PYI061", "PYI062", "PYI063", "PYI064", "PYI066", 117 | "PT001", "PT002", "PT003", "PT006", "PT007", "PT008", "PT009", "PT010", "PT013", "PT014", "PT015", "PT016", "PT018", "PT019", "PT020", "PT021", "PT022", "PT023", "PT024", "PT025", "PT026", "PT027", 118 | "Q004", 119 | "RET502", "RET503", "RET504", 120 | "SIM101", "SIM103", "SIM105", "SIM107", "SIM109", "SIM110", "SIM113", "SIM114", "SIM118", "SIM201", "SIM202", "SIM208", "SIM210", "SIM211", "SIM212", "SIM220", "SIM221", "SIM222", "SIM223", "SIM300", "SIM401", "SIM905", "SIM910", "SIM911", 121 | "TC001", "TC002", "TC003", "TC004", "TC005", "TC006", "TC007", "TC008", "TC010", 122 | "PTH201", 123 | "TD004", "TD005", "TD006", "TD007", 124 | "PGH003", "PGH004", "PGH005", 125 | "PLC0105", "PLC0131", "PLC0132", "PLC0205", "PLC0206", "PLC0208", "PLC0414", "PLC2401", "PLC2403", "PLC2701", "PLC2801", "PLC3002", 126 | "PLR0124", "PLR0133", "PLR0202", "PLR0203", "PLR0206", "PLR0402", "PLR1716", "PLR1722", "PLR1733", "PLR1736", "PLR2044", "PLR6301", 127 | "TRY201", "TRY203", "TRY401", 128 | "FLY002", 129 | "PERF101", "PERF102", "PERF402", "PERF403", 130 | "FURB105", "FURB110", "FURB116", "FURB118", "FURB129", "FURB131", "FURB132", "FURB136", "FURB142", "FURB145", "FURB148", "FURB154", "FURB156", "FURB157", "FURB161", "FURB163", "FURB164", "FURB166", "FURB167", "FURB168", "FURB169", "FURB171", "FURB177", "FURB180", "FURB181", "FURB188", "FURB192", 131 | "RUF001", "RUF002", "RUF003", "RUF005", "RUF006", "RUF007", "RUF008", "RUF009", "RUF010", "RUF012", "RUF013", "RUF015", "RUF016", "RUF017", "RUF018", "RUF019", "RUF020", "RUF021", "RUF022", "RUF023", "RUF024", "RUF026", "RUF028", "RUF029", "RUF030", "RUF031", "RUF032", "RUF033", "RUF034", "RUF035", "RUF036", "RUF037", "RUF038", "RUF039", "RUF040", "RUF041", "RUF043", "RUF046", "RUF047", "RUF048", "RUF049", "RUF051", "RUF052", "RUF055", "RUF056", "RUF057", "RUF058", "RUF100", "RUF101", "RUF200", 132 | ] 133 | ignore = ["E501", "E731", "UP038"] 134 | [tool.ruff.lint.per-file-ignores] 135 | "tests/**" = ["PLC2701", "PLR6301"] 136 | [tool.ruff.lint.flake8-comprehensions] 137 | allow-dict-calls-with-keyword-arguments = true 138 | [tool.ruff.lint.flake8-type-checking] 139 | exempt-modules = ["typing", "collections.abc"] 140 | 141 | [tool.mypy] 142 | warn_unreachable = true 143 | allow_redefinition = true 144 | 145 | [tool.pytest.ini_options] 146 | addopts = "--tb=native" 147 | enable_assertion_pass_hook = true 148 | filterwarnings = ["ignore::DeprecationWarning:.*:", 149 | "default::DeprecationWarning:mkdocs_literate_nav.*:"] 150 | testpaths = ["tests"] 151 | -------------------------------------------------------------------------------- /requirements/requirements-docs.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by hatch-pip-compile with Python 3.12 3 | # 4 | # - markdown-callouts 5 | # - mkdocs 6 | # - mkdocs-material 7 | # - pymdown-extensions 8 | # 9 | 10 | babel==2.16.0 11 | # via mkdocs-material 12 | certifi==2024.8.30 13 | # via requests 14 | charset-normalizer==3.4.0 15 | # via requests 16 | click==8.1.7 17 | # via mkdocs 18 | colorama==0.4.6 19 | # via mkdocs-material 20 | ghp-import==2.1.0 21 | # via mkdocs 22 | idna==3.10 23 | # via requests 24 | jinja2==3.1.4 25 | # via 26 | # mkdocs 27 | # mkdocs-material 28 | markdown==3.7 29 | # via 30 | # markdown-callouts 31 | # mkdocs 32 | # mkdocs-material 33 | # pymdown-extensions 34 | markdown-callouts==0.4.0 35 | # via hatch.envs.docs 36 | markupsafe==3.0.2 37 | # via 38 | # jinja2 39 | # mkdocs 40 | mergedeep==1.3.4 41 | # via 42 | # mkdocs 43 | # mkdocs-get-deps 44 | mkdocs==1.6.1 45 | # via 46 | # hatch.envs.docs 47 | # mkdocs-material 48 | mkdocs-get-deps==0.2.0 49 | # via mkdocs 50 | mkdocs-material==9.5.47 51 | # via hatch.envs.docs 52 | mkdocs-material-extensions==1.3.1 53 | # via mkdocs-material 54 | packaging==24.2 55 | # via mkdocs 56 | paginate==0.5.7 57 | # via mkdocs-material 58 | pathspec==0.12.1 59 | # via mkdocs 60 | platformdirs==4.3.6 61 | # via mkdocs-get-deps 62 | pygments==2.18.0 63 | # via mkdocs-material 64 | pymdown-extensions==10.12 65 | # via 66 | # hatch.envs.docs 67 | # mkdocs-material 68 | python-dateutil==2.9.0.post0 69 | # via ghp-import 70 | pyyaml==6.0.2 71 | # via 72 | # mkdocs 73 | # mkdocs-get-deps 74 | # pymdown-extensions 75 | # pyyaml-env-tag 76 | pyyaml-env-tag==0.1 77 | # via mkdocs 78 | regex==2024.11.6 79 | # via mkdocs-material 80 | requests==2.32.3 81 | # via mkdocs-material 82 | six==1.17.0 83 | # via python-dateutil 84 | urllib3==2.2.3 85 | # via requests 86 | watchdog==6.0.0 87 | # via mkdocs 88 | -------------------------------------------------------------------------------- /requirements/requirements-style.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by hatch-pip-compile with Python 3.13 3 | # 4 | # - ruff 5 | # 6 | 7 | ruff==0.9.6 8 | # via hatch.envs.style 9 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from mkdocs.structure.files import File 2 | 3 | 4 | def pytest_assertrepr_compare(op, left, right): 5 | if isinstance(left, File) and isinstance(right, File) and op == "==": 6 | return [f"File({left.src_path}) != File({right.src_path})"] 7 | return None 8 | -------------------------------------------------------------------------------- /tests/nav/hybrid/test_empty.yml: -------------------------------------------------------------------------------- 1 | files: {} 2 | output: [] 3 | -------------------------------------------------------------------------------- /tests/nav/hybrid/test_files_in_subsection.yml: -------------------------------------------------------------------------------- 1 | nav: 2 | - foo/*: # Just acts as the title anyway 3 | - Foo: foo/ 4 | - bar/ # Doesn't exist, OK. 5 | files: 6 | foo/xxx.md: 7 | foo/yyy.md: 8 | foo/zzz.md: 9 | foo/bar/zzz.md: 10 | output: 11 | - foo/*: 12 | - Foo: 13 | - null: foo/xxx.md 14 | - null: foo/yyy.md 15 | - null: foo/zzz.md 16 | - Bar: 17 | - null: foo/bar/zzz.md 18 | - bar/ 19 | -------------------------------------------------------------------------------- /tests/nav/hybrid/test_flattened_files_in_subsection.yml: -------------------------------------------------------------------------------- 1 | nav: 2 | - foo/*: # Just acts as the title anyway 3 | - foo/* 4 | - bar/* # Doesn't exist, OK. 5 | files: 6 | foo/xxx.md: 7 | foo/yyy.md: 8 | foo/zzz.md: 9 | foo/bar/zzz.md: 10 | output: 11 | - foo/*: 12 | - null: foo/xxx.md 13 | - null: foo/yyy.md 14 | - null: foo/zzz.md 15 | - Bar: 16 | - null: foo/bar/zzz.md 17 | - bar/* 18 | -------------------------------------------------------------------------------- /tests/nav/hybrid/test_from_readme.yml: -------------------------------------------------------------------------------- 1 | nav: 2 | - Foo: foo.md 3 | - Usual: 4 | - usual/a.md 5 | - usual/b.md 6 | - '*.md' 7 | - Subdir: subdir/ 8 | files: 9 | bar.md: 10 | baz.md: 11 | foo.md: 12 | subdir/x.md: 13 | subdir/y.md: 14 | usual/a.md: 15 | usual/b.md: 16 | output: 17 | - Foo: foo.md 18 | - Usual: 19 | - usual/a.md 20 | - usual/b.md 21 | - null: bar.md 22 | - null: baz.md 23 | - Subdir: 24 | - null: subdir/x.md 25 | - null: subdir/y.md 26 | -------------------------------------------------------------------------------- /tests/nav/hybrid/test_keeps_original_nav.yml: -------------------------------------------------------------------------------- 1 | nav: 2 | - hi-hi: /ho-ho 3 | - test: test/ 4 | - sub: 5 | - Test: foo.md 6 | files: {} 7 | output: 8 | - hi-hi: /ho-ho 9 | - test: test 10 | - sub: 11 | - Test: foo.md 12 | -------------------------------------------------------------------------------- /tests/nav/hybrid/test_literate_in_subsection.yml: -------------------------------------------------------------------------------- 1 | nav: 2 | - foo/: 3 | - Bar: bar/ # Doesn't exist, OK. 4 | - Foo: foo/ 5 | files: 6 | foo/SUMMARY.md: | 7 | * [Hi](ho.md) 8 | output: 9 | - foo/: 10 | - Bar: bar 11 | - Foo: 12 | - Hi: foo/ho.md 13 | -------------------------------------------------------------------------------- /tests/nav/hybrid/test_nav_and_literate.yml: -------------------------------------------------------------------------------- 1 | nav: 2 | - foo.md 3 | files: 4 | SUMMARY.md: | 5 | * [Hi](ho.md) 6 | output: 7 | - Hi: ho.md 8 | -------------------------------------------------------------------------------- /tests/nav/hybrid/test_preserves_url.yml: -------------------------------------------------------------------------------- 1 | nav: 2 | - https://example.org/ 3 | - Foo: http://example.com/ 4 | output: 5 | - https://example.org/ 6 | - Foo: http://example.com/ 7 | -------------------------------------------------------------------------------- /tests/nav/hybrid/test_repeated.yml: -------------------------------------------------------------------------------- 1 | nav: 2 | - Foo: foo/ 3 | - Bar: bar/ # Doesn't exist, OK 4 | - Foo: foo/ 5 | files: 6 | foo/SUMMARY.md: | 7 | * [Hi](ho.md) 8 | output: 9 | - Foo: 10 | - Hi: foo/ho.md 11 | - Bar: bar 12 | - Foo: 13 | - Hi: foo/ho.md 14 | -------------------------------------------------------------------------------- /tests/nav/hybrid/test_slash_with_nav.yml: -------------------------------------------------------------------------------- 1 | nav: 2 | - '*' 3 | files: 4 | SUMMARY.md: | 5 | * [Hi](ho.md) 6 | a.md: 7 | output: 8 | - Hi: ho.md 9 | -------------------------------------------------------------------------------- /tests/nav/hybrid/test_very_nested.yml: -------------------------------------------------------------------------------- 1 | nav: 2 | - Foo: 3 | - Bar: 4 | - Baz: aa/aa/aa/ 5 | files: 6 | aa/aa/aa/SUMMARY.md: | 7 | * [test](bb/bb/bb.md) 8 | * [test](/aa/) 9 | aa/cc/cc/cc.md: 10 | output: 11 | - Foo: 12 | - Bar: 13 | - Baz: 14 | - test: aa/aa/aa/bb/bb/bb.md 15 | - test: 16 | - Cc: 17 | - Cc: 18 | - null: aa/cc/cc/cc.md 19 | -------------------------------------------------------------------------------- /tests/nav/hybrid/test_wildcard_slash.yml: -------------------------------------------------------------------------------- 1 | nav: 2 | - '*/' 3 | files: 4 | a.md: 5 | foo/c.md: 6 | output: 7 | - Foo: 8 | - null: foo/c.md 9 | -------------------------------------------------------------------------------- /tests/nav/nested/test_basic.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | 1. Section 1 4 | * [Item1](item1.md) 5 | * [Item2](item2.md) 6 | 2. [Section 2](section2/) 7 | item1.md: 8 | item2.md: 9 | section2/SUMMARY.md: | 10 | * [Item](item.md) 11 | section2/item.md: 12 | output: 13 | - Section 1: 14 | - Item1: item1.md 15 | - Item2: item2.md 16 | - Section 2: 17 | - Item: section2/item.md 18 | -------------------------------------------------------------------------------- /tests/nav/nested/test_borgs.yml: -------------------------------------------------------------------------------- 1 | files: 2 | index.md: 3 | SUMMARY.md: | 4 | * [Frob](index.md) 5 | * [Baz](baz.md) 6 | * [Borgs](borgs/) 7 | baz.md: 8 | borgs/index.md: 9 | borgs/SUMMARY.md: | 10 | * [Bar](bar.md) 11 | * [Foo](foo.md) 12 | borgs/bar.md: 13 | borgs/foo.md: 14 | output: 15 | - Frob: index.md 16 | - Baz: baz.md 17 | - Borgs: 18 | - Bar: borgs/bar.md 19 | - Foo: borgs/foo.md 20 | -------------------------------------------------------------------------------- /tests/nav/nested/test_dir_without_slash.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | * [Foo](foo) 4 | foo/index.md: 5 | foo/bar.md: 6 | output: 7 | - Foo: foo 8 | -------------------------------------------------------------------------------- /tests/nav/nested/test_go_up.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | * [item](item.md) 4 | * [Sub1](sub1/) 5 | item.md: 6 | item2.md: 7 | sub1/SUMMARY.md: | 8 | * [item1](item1.md) 9 | * [Sub2](../sub2/) 10 | sub1/item1.md: 11 | sub2/index.md: 12 | sub2/SUMMARY.md: | 13 | * [item2](../item2.md) 14 | output: 15 | - item: item.md 16 | - Sub1: 17 | - item1: sub1/item1.md 18 | - Sub2: 19 | - item2: item2.md 20 | -------------------------------------------------------------------------------- /tests/nav/nested/test_go_up_too_much.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | * [item](item.md) 4 | * [Sub1](sub1/) 5 | item.md: 6 | sub1/SUMMARY.md: | 7 | * [item1](item1.md) 8 | * [Sub2](../../sub2/) 9 | sub1/item1.md: 10 | sub2/SUMMARY.md: | 11 | * [item2](../item2.md) 12 | sub2/item2.md: 13 | output: 14 | - item: item.md 15 | - Sub1: 16 | - item1: sub1/item1.md 17 | - Sub2: ../sub2 18 | -------------------------------------------------------------------------------- /tests/nav/nested/test_implicit_index_as_nav.yml: -------------------------------------------------------------------------------- 1 | files: 2 | index.md: | 3 | * [Baz](baz.md) 4 | * [Borgs](borgs/) 5 | baz.md: 6 | borgs/index.md: | 7 | * [Bar](bar.md) 8 | * [Foo](foo.md) 9 | borgs/bar.md: 10 | borgs/foo.md: 11 | implicit_index: true 12 | nav_file_name: index.md 13 | output: 14 | - null: index.md 15 | - Baz: baz.md 16 | - Borgs: 17 | - null: borgs/index.md 18 | - Bar: borgs/bar.md 19 | - Foo: borgs/foo.md 20 | -------------------------------------------------------------------------------- /tests/nav/nested/test_implicit_index_borgs.yml: -------------------------------------------------------------------------------- 1 | files: 2 | index.md: 3 | SUMMARY.md: | 4 | * [Baz](baz.md) 5 | * [Borgs](borgs/) 6 | baz.md: 7 | borgs/index.md: 8 | borgs/SUMMARY.md: | 9 | * [Bar](bar.md) 10 | * [Foo](foo.md) 11 | borgs/bar.md: 12 | borgs/foo.md: 13 | implicit_index: true 14 | output: 15 | - null: index.md 16 | - Baz: baz.md 17 | - Borgs: 18 | - null: borgs/index.md 19 | - Bar: borgs/bar.md 20 | - Foo: borgs/foo.md 21 | -------------------------------------------------------------------------------- /tests/nav/nested/test_missing.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | * [Foo](foo/) 4 | bar/SUMMARY.md: | 5 | * [Section 2](/) 6 | output: 7 | - Foo: foo 8 | -------------------------------------------------------------------------------- /tests/nav/nested/test_nested.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | * Foo 4 | * Bar 5 | * [item](item.md) 6 | * [Sub](sub/) 7 | item.md: 8 | sub/SUMMARY.md: | 9 | * Foo 10 | * Bar 11 | * [item](item.md) 12 | * [Sub](sub/) 13 | sub/item.md: 14 | sub/sub/SUMMARY.md: | 15 | * Foo 16 | * Bar 17 | * [item](item.md) 18 | * [Sub](sub/) 19 | sub/sub/item.md: 20 | sub/sub/sub/item.md: 21 | output: 22 | - Foo: 23 | - Bar: 24 | - item: item.md 25 | - Sub: 26 | - Foo: 27 | - Bar: 28 | - item: sub/item.md 29 | - Sub: 30 | - Foo: 31 | - Bar: 32 | - item: sub/sub/item.md 33 | - Sub: 34 | - null: sub/sub/sub/item.md 35 | -------------------------------------------------------------------------------- /tests/nav/nested/test_recursion.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | 1. Section 1 4 | * [Item1](item1.md) 5 | * [Item2](item2.md) 6 | 2. [Section 2](section2/) 7 | 3. [Section 3](section3/) 8 | item1.md: 9 | item2.md: 10 | section2/index.md: 11 | section2/SUMMARY.md: | 12 | * [Section 2](/) 13 | * [Section 2](//) 14 | * [Section 2](./) 15 | * [Section 2](../section2/) 16 | * [Section 3](../section3/) 17 | * [Section 1](..) 18 | section3/index.md: 19 | section3/SUMMARY.md: | 20 | * [Section 2](../section2/) 21 | output: 22 | - Section 1: 23 | - Item1: item1.md 24 | - Item2: item2.md 25 | - Section 2: 26 | - Section 2: / 27 | - Section 2: // 28 | - Section 2: ./ 29 | - Section 2: ../section2/ 30 | - Section 3: 31 | - Section 2: ../section2/ 32 | - Section 1: . 33 | - Section 3: 34 | - Section 2: 35 | - Section 2: / 36 | - Section 2: // 37 | - Section 2: ./ 38 | - Section 2: ../section2/ 39 | - Section 3: ../section3/ 40 | - Section 1: . 41 | logs: 42 | - WARNING:Disallowing recursion '.' -> 'section2' -> '.' 43 | - WARNING:Disallowing recursion '.' -> 'section2' -> 'section2' 44 | - WARNING:Disallowing recursion '.' -> 'section2' -> 'section3' -> 'section2' 45 | - WARNING:Disallowing recursion '.' -> 'section3' -> 'section2' -> '.' 46 | - WARNING:Disallowing recursion '.' -> 'section3' -> 'section2' -> 'section2' 47 | - WARNING:Disallowing recursion '.' -> 'section3' -> 'section2' -> 'section3' 48 | -------------------------------------------------------------------------------- /tests/nav/nested/test_repeated.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | * Foo 4 | * [item](item.md) 5 | * [Sub](sub/) 6 | * [Sub](sub/) 7 | * [Sub](sub/) 8 | item.md: 9 | sub/SUMMARY.md: | 10 | * Bar 11 | * [item](item.md) 12 | sub/item.md: 13 | output: 14 | - Foo: 15 | - item: item.md 16 | - Sub: 17 | - Bar: 18 | - item: sub/item.md 19 | - Sub: 20 | - Bar: 21 | - item: sub/item.md 22 | - Sub: 23 | - Bar: 24 | - item: sub/item.md 25 | -------------------------------------------------------------------------------- /tests/nav/test_basic.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | 1. Section 1 4 | * [Item1](item1.md) 5 | * [Item2](item2.md) 6 | 2. Section 2 7 | * [Item](section2/item.md) 8 | output: 9 | - Section 1: 10 | - Item1: item1.md 11 | - Item2: item2.md 12 | - Section 2: 13 | - Item: section2/item.md 14 | -------------------------------------------------------------------------------- /tests/nav/test_broken_before_marker.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | * foo 4 | 5 | 6 | * Section 1 7 | * [Item1](item1.md) 8 | * [Item2](item2.md) 9 | * Section 2 10 | * [Item](section2/item.md) 11 | output: 12 | - Section 1: 13 | - Item1: item1.md 14 | - Item2: item2.md 15 | - Section 2: 16 | - Item: section2/item.md 17 | -------------------------------------------------------------------------------- /tests/nav/test_element_after.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | 1. Section 1 4 | * [Item1](item1.md)[Item2](item2.md) 5 | exception: 6 | LiterateNavParseError: |- 7 | Expected no more elements, but got Item2. 8 | 9 | The problematic item: 10 | 11 |
  • Item1Item2
  • 12 | -------------------------------------------------------------------------------- /tests/nav/test_empty.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: '' 3 | output: 4 | - null: SUMMARY.md 5 | -------------------------------------------------------------------------------- /tests/nav/test_empty_section.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | 1. Section 1 4 | 2. Section 2 5 | * [Item](section2/item.md) 6 | exception: 7 | LiterateNavParseError: |- 8 | Did not find any item/section content specified. 9 | Examples: 10 | * [Item title](item_content.md) 11 | * Section title 12 | * [Sub content](sub/content.md) 13 | * *.md 14 | 15 | The problematic item: 16 | 17 |
  • Section 1
  • 18 | -------------------------------------------------------------------------------- /tests/nav/test_fancy_link.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | 1. Section 1 4 | * [Item **1**](item1.md) 5 | * [Item 2](item2.md) 6 | 2. Section 2 7 | * [`Item`](section2/item.md) 8 | output: 9 | - Section 1: 10 | - Item 1: item1.md 11 | - Item 2: item2.md 12 | - Section 2: 13 | - Item: section2/item.md 14 | -------------------------------------------------------------------------------- /tests/nav/test_multiple_nav.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | 4 | * [Foo](foo.md) 5 | 6 | 7 | * [Bar](bar.md) 8 | output: 9 | - Bar: bar.md 10 | -------------------------------------------------------------------------------- /tests/nav/test_multiple_nav_and_others.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | 4 | * [A](a.md) 5 | 6 | hi 7 | 8 | * [B](a.md) 9 | 10 | 11 | * [C](C.md) 12 | 13 | hi 14 | 15 | * [D](D.md) 16 | output: 17 | - C: C.md 18 | -------------------------------------------------------------------------------- /tests/nav/test_multiple_unmarked_nav.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | * [Foo](foo.md) 4 | 5 | hi 6 | 7 | * [Bar](bar.md) 8 | output: 9 | - Bar: bar.md 10 | -------------------------------------------------------------------------------- /tests/nav/test_nav_after_gap.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | * [Foo](foo.md) 4 | 5 | 6 | Hi 7 | 8 | Hmm 9 | 10 | * [Bar](bar.md) 11 | output: 12 | - Bar: bar.md 13 | -------------------------------------------------------------------------------- /tests/nav/test_nav_last.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | * [Foo](foo.md) 4 | 5 | 6 | output: 7 | - null: SUMMARY.md 8 | -------------------------------------------------------------------------------- /tests/nav/test_no_title.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | 1. * [Item](section2/item.md) 4 | exception: 5 | LiterateNavParseError: |- 6 | Did not find any title specified. 7 | Examples: 8 | * [Item title](item_content.md) 9 | * Section title 10 | * [Sub content](sub/content.md) 11 | * *.md 12 | 13 | The problematic item: 14 | 15 |
  • 16 | -------------------------------------------------------------------------------- /tests/nav/test_obscured_link.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | 1. Section 1 4 | * [Item1](item1.md) 5 | * **[Item2](item2.md)** 6 | 2. Section 2 7 | * [Item](section2/item.md) 8 | exception: 9 | LiterateNavParseError: |- 10 | Expected no more elements, but got Item2. 11 | Did not find any title specified. 12 | Examples: 13 | * [Item title](item_content.md) 14 | * Section title 15 | * [Sub content](sub/content.md) 16 | * *.md 17 | 18 | The problematic item: 19 | 20 |
  • [...]
  • 21 | -------------------------------------------------------------------------------- /tests/nav/test_obscured_section.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | 1. Section 1 4 | * [Item1](item1.md) 5 | * [Item2](item2.md) 6 | 2. Section *2* 7 | * [Item](section2/item.md) 8 | exception: 9 | LiterateNavParseError: |- 10 | Expected no more elements, but got 2. 11 | Did not find any item/section content specified. 12 | Examples: 13 | * [Item title](item_content.md) 14 | * Section title 15 | * [Sub content](sub/content.md) 16 | * *.md 17 | 18 | The problematic item: 19 | 20 |
  • Section 2
  • 21 | -------------------------------------------------------------------------------- /tests/nav/test_preserves_url.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | - [hi](https://example.org/) 4 | - hello 5 | - [Foo](http://example.com/) 6 | - [other](test.md) 7 | output: 8 | - hi: https://example.org/ 9 | - hello: 10 | - Foo: http://example.com/ 11 | - other: test.md 12 | -------------------------------------------------------------------------------- /tests/nav/test_section_with_link.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | 1. [Section 1](section1.md) 4 | * [Item1](item1.md) 5 | * [Item2](item2.md) 6 | 2. Section 2 7 | * [Item](section2/item.md) 8 | output: 9 | - Section 1: 10 | - section1.md 11 | - Item1: item1.md 12 | - Item2: item2.md 13 | - Section 2: 14 | - Item: section2/item.md 15 | -------------------------------------------------------------------------------- /tests/nav/test_special_chars.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | * [a&b](a.md) 4 | * [a&b](b.md) 5 | * [a&amp;b](c.md) 6 | * [\__init__](d.md) 7 | * [\`hi`](e.md) 8 | output: 9 | - a&b: a.md 10 | - a&b: b.md 11 | - a&amp;b: c.md 12 | - __init__: d.md 13 | - '`hi`': e.md 14 | -------------------------------------------------------------------------------- /tests/nav/test_text_after.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | 1. Section 1 4 | * [Item1](item1.md)foo 5 | exception: 6 | LiterateNavParseError: |- 7 | Expected no text after Item1, but got 'foo'. 8 | The problematic item: 9 | 10 |
  • Item1foo
  • 11 | -------------------------------------------------------------------------------- /tests/nav/wildcard/test_basic.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | - * 4 | item1.md: 5 | item2.md: 6 | section2/item.md: 7 | output: 8 | - null: item1.md 9 | - null: item2.md 10 | - Section2: 11 | - null: section2/item.md 12 | -------------------------------------------------------------------------------- /tests/nav/wildcard/test_dirs_then_files.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | - */ 4 | - *.md 5 | item1.md: 6 | item2.md: 7 | section2/item.md: 8 | output: 9 | - Section2: 10 | - null: section2/item.md 11 | - null: item1.md 12 | - null: item2.md 13 | -------------------------------------------------------------------------------- /tests/nav/wildcard/test_from_readme.yml: -------------------------------------------------------------------------------- 1 | files: 2 | index.md: 3 | SUMMARY.md: | 4 | - [Welcome](index.md) 5 | - Usage 6 | - [Foo](usage/foo.md) 7 | - usage/*.md 8 | - */ 9 | - *.md 10 | - [API docs](api/) 11 | - [License](license.md) 12 | changelog.md: 13 | credits.md: 14 | api/Foo.md: 15 | api/Bar/index.md: 16 | api/Bar/Baz.md: 17 | tips/other-stuff.md: 18 | tips/stuff.md: 19 | usage/bar.md: 20 | usage/baz.md: 21 | usage/foo.md: 22 | output: 23 | - Welcome: index.md 24 | - Usage: 25 | - Foo: usage/foo.md 26 | - null: usage/bar.md 27 | - null: usage/baz.md 28 | - Tips: 29 | - null: tips/other-stuff.md 30 | - null: tips/stuff.md 31 | - null: changelog.md 32 | - null: credits.md 33 | - API docs: 34 | - null: api/Foo.md 35 | - Bar: 36 | - null: api/Bar/index.md 37 | - null: api/Bar/Baz.md 38 | - License: license.md 39 | -------------------------------------------------------------------------------- /tests/nav/wildcard/test_implicitly_recursive.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | - foo/* 4 | foo/a.md: 5 | foo/bar/index.md: 6 | foo/bar/SUMMARY.md: | 7 | - [stuff](index.md) 8 | output: 9 | - null: foo/a.md 10 | - Bar: 11 | - stuff: foo/bar/index.md 12 | -------------------------------------------------------------------------------- /tests/nav/wildcard/test_multidir_file.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | - */foo.md 4 | aaa/bar.md: 5 | aaa/foo.md: 6 | bbb/foo.md: 7 | output: 8 | - null: aaa/foo.md 9 | - null: bbb/foo.md 10 | -------------------------------------------------------------------------------- /tests/nav/wildcard/test_no_files.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | - * 4 | output: [] 5 | -------------------------------------------------------------------------------- /tests/nav/wildcard/test_no_repeat_with_directory.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | - * 4 | - [sub](sub/index.md) 5 | - [a](a.md) 6 | a.md: 7 | b.md: 8 | sub/index.md: 9 | output: 10 | - null: b.md 11 | - sub: sub/index.md 12 | - a: a.md 13 | -------------------------------------------------------------------------------- /tests/nav/wildcard/test_non_matching.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | - [A](a.md) 4 | - a*.md 5 | - b*.md 6 | a.md: 7 | output: 8 | - A: a.md 9 | - b*.md 10 | -------------------------------------------------------------------------------- /tests/nav/wildcard/test_omits_competing_parent_first.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | - sub/*.md 4 | - [sub](sub/) 5 | sub/index.md: 6 | sub/SUMMARY.md: | 7 | - *.md 8 | sub/a.md: 9 | sub/b.md: 10 | output: 11 | - null: sub/index.md 12 | - null: sub/SUMMARY.md 13 | - null: sub/a.md 14 | - null: sub/b.md 15 | -------------------------------------------------------------------------------- /tests/nav/wildcard/test_omits_competing_subdir_first.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | - [sub](sub/) 4 | - sub/*.md 5 | sub/index.md: 6 | sub/SUMMARY.md: | 7 | - *.md 8 | sub/a.md: 9 | sub/b.md: 10 | output: 11 | - sub: 12 | - null: sub/index.md 13 | - null: sub/a.md 14 | - null: sub/b.md 15 | -------------------------------------------------------------------------------- /tests/nav/wildcard/test_omits_in_subdir.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | - [a](sub/a.md) 4 | - [sub](sub/) 5 | - [b](sub/b.md) 6 | - *.md 7 | sub/index.md: 8 | sub/SUMMARY.md: | 9 | - *.md 10 | sub/a.md: 11 | sub/b.md: 12 | sub/c.md: 13 | output: 14 | - a: sub/a.md 15 | - sub: 16 | - null: sub/index.md 17 | - null: sub/c.md 18 | - b: sub/b.md 19 | -------------------------------------------------------------------------------- /tests/nav/wildcard/test_omits_in_superdir.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | - [sub](sub/) 4 | - sub/*.md 5 | sub/index.md: 6 | sub/SUMMARY.md: | 7 | - [a](a.md) 8 | - [b](b.md) 9 | sub/a.md: 10 | sub/b.md: 11 | sub/c.md: 12 | output: 13 | - sub: 14 | - a: sub/a.md 15 | - b: sub/b.md 16 | - null: sub/index.md 17 | - null: sub/c.md 18 | -------------------------------------------------------------------------------- /tests/nav/wildcard/test_skips_nav_file.yml: -------------------------------------------------------------------------------- 1 | files: 2 | SUMMARY.md: | 3 | - *.md 4 | - [Sub](subdir/) 5 | - subother/* 6 | foo.md: 7 | subdir/SUMMARY.md: | 8 | - * 9 | subdir/bar.md: 10 | subother/baz.md: 11 | output: 12 | - null: foo.md 13 | - Sub: 14 | - null: subdir/bar.md 15 | - null: subother/baz.md 16 | -------------------------------------------------------------------------------- /tests/nav/wildcard/test_wildcard_under_index.yml: -------------------------------------------------------------------------------- 1 | files: 2 | index.md: 3 | SUMMARY.md: | 4 | - [b](b/index.md) 5 | - [stuff](b/stuff.md) 6 | - b/*.md 7 | b/index.md: 8 | b/a.md: 9 | b/stuff.md: 10 | b/z.md: 11 | output: 12 | - b: 13 | - b/index.md 14 | - stuff: b/stuff.md 15 | - null: b/a.md 16 | - null: b/z.md 17 | -------------------------------------------------------------------------------- /tests/test_plugin.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | import pytest 4 | from mkdocs.structure.files import File, Files 5 | 6 | from mkdocs_literate_nav import exceptions, plugin 7 | 8 | 9 | @pytest.mark.golden_test("nav/**/*.yml") 10 | def test_nav(tmp_path_factory, golden): 11 | src_dir, dest_dir = map(tmp_path_factory.mktemp, ["src", "dest"]) 12 | 13 | files = [] 14 | for fn, content in (golden.get("files") or {}).items(): 15 | path = src_dir / fn 16 | path.parent.mkdir(parents=True, exist_ok=True) 17 | if content is not None: 18 | path.write_text(content, encoding="utf-8") 19 | files.append(File(fn, src_dir, dest_dir, use_directory_urls=len(golden.path.name) % 2)) 20 | assert [f.src_path for f in sorted(files, key=file_sort_key)] == [f.src_path for f in files] 21 | files = Files(files) 22 | 23 | output = None 24 | with golden.may_raise(exceptions.LiterateNavError): 25 | with golden.capture_logs("mkdocs.plugins.mkdocs_literate_nav"): 26 | output = plugin.resolve_directories_in_nav( 27 | golden.get("nav"), 28 | files, 29 | nav_file_name=golden.get("nav_file_name") or "SUMMARY.md", 30 | implicit_index=golden.get("implicit_index"), 31 | ) 32 | assert output == golden.out.get("output") 33 | 34 | 35 | # https://github.com/oprypin/mkdocs-gen-files/blob/71a4825d5c/mkdocs_gen_files/editor.py#L16 36 | def file_sort_key(f: File): 37 | parts = pathlib.PurePath(f.src_path).parts 38 | return tuple( 39 | chr(f.name != "index" if i == len(parts) - 1 else 2) + p for i, p in enumerate(parts) 40 | ) 41 | --------------------------------------------------------------------------------