├── .github
└── workflows
│ └── python-app.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── README.md
├── coverage.svg
├── docs
├── all_dir
│ ├── all_dir.md
│ └── all_dir_ignore_heading1.md
├── all_dir_sub
│ └── all_dir_sub2
│ │ └── all_dir_sub2_1.md
├── chapter_exclude_all.md
├── chapter_exclude_heading2.md
├── dir
│ ├── dir_chapter_exclude_all.md
│ └── dir_chapter_ignore_heading3.md
├── index.md
├── toplvl_chapter
│ ├── file_in_toplvl_chapter.md
│ └── sub_chapter
│ │ ├── file1_in_sub_chapter.md
│ │ ├── file2_in_sub_chapter.md
│ │ └── unreferenced_in_sub_chapter.md
├── unreferenced.md
└── without_nav_name.md
├── mkdocs.yml
├── mkdocs_exclude_search
├── __init__.py
├── plugin.py
└── utils.py
├── pylintrc
├── requirements_dev.txt
├── setup.py
└── tests
├── __init__.py
├── context.py
├── globals.py
├── mock_data
├── config.json
└── mock_search_index.json
├── test_plugin.py
└── test_utils.py
/.github/workflows/python-app.yml:
--------------------------------------------------------------------------------
1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
3 |
4 | name: Python application
5 |
6 | on:
7 | push:
8 | branches: [ main ]
9 | pull_request:
10 | branches: [ main ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v2
19 | - name: Set up Python 3.9
20 | uses: actions/setup-python@v2
21 | with:
22 | python-version: 3.9
23 | - name: Install dependencies
24 | run: |
25 | python -m pip install --upgrade pip
26 | pip install flake8 pytest
27 | if [ -f requirements_dev.txt ]; then pip install -r requirements_dev.txt; fi
28 | - name: Test with pytest
29 | run: |
30 | pytest
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
131 | # misc
132 | /aux
133 |
134 | .vscode/
135 | .idea/
136 |
137 | /test-repos
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog & Release Notes
2 |
3 | ## Upgrading
4 |
5 | To upgrade to the latest version of `mkdocs-exclude-search` use `pip`:
6 |
7 | ```bash
8 | pip install mkdocs-exclude-search --upgrade
9 | ```
10 |
11 | You can determine your currently installed version using this command:
12 |
13 | ```bash
14 | pip show mkdocs-exclude-search
15 | ```
16 |
17 | ## Versions
18 |
19 | ### [0.6.6](https://pypi.org/project/mkdocs-exclude-search/) (2023-11-20)
20 | - Prevents long mkdocs logger deprecation warning (#45).
21 |
22 | ### [0.6.5](https://pypi.org/project/mkdocs-exclude-search/) (2023-02-04)
23 | - Fixes issue of search-plugin not being recognized, mkdocs-material adjusted search namespace (#42).
24 |
25 | ### [0.6.4](https://pypi.org/project/mkdocs-exclude-search/) (2022-01-05)
26 | - Fixes issue when mkdocs navigation is provided without chapter names.
27 |
28 | ### [0.6.3](https://pypi.org/project/mkdocs-exclude-search/) (2021-12-23)
29 | - Fixes issue in 0.6.2 when building with no navigation.
30 | - Providing non-header entries to `ignore` is now ignored instead of failing.
31 | - Extended tests and established testing pipeline on various `used-by` repos.
32 |
33 | ### [0.6.2](https://pypi.org/project/mkdocs-exclude-search/) (2021-12-23)
34 | - Fixes issue in 0.6.1 when building with no ignored files defined.
35 |
36 | ### [0.6.1](https://pypi.org/project/mkdocs-exclude-search/) (2021-12-22)
37 | - Fixes issue in 0.6.0 when building with multiple subchapters in the navigation.
38 |
39 | ### [0.6.0](https://pypi.org/project/mkdocs-exclude-search/) (2021-12-22)
40 | - Add `exclude_unreferenced` option to exclude files that are not listed in the
41 | mkdocs.yml nav section.
42 |
43 | ### [0.5.4](https://pypi.org/project/mkdocs-exclude-search/) (2021-12-01)
44 | - Avoid installing "tests" package with installation.
45 |
46 | ### [0.5.2](https://pypi.org/project/mkdocs-exclude-search/) (2021-04-28)
47 | - Reduced logger messages, verbose file exclusion log now available with `mkdocs serve -v`
48 |
49 | ### [0.5.1](https://pypi.org/project/mkdocs-exclude-search/) (2021-04-19)
50 | - Require >= Python 3.6 for installing
51 |
52 | ### [0.5.0](https://pypi.org/project/mkdocs-exclude-search/) (2021-04-05)
53 | - **Breaking changes to specification of excluded and ignored files and directories**, see new examples in the readme.
54 | - Removed not explicitly set wildcard matching of filenames in directory.
55 | - Clarified examples and wildcard matching in readme.
56 |
57 | ### [0.4.0](https://pypi.org/project/mkdocs-exclude-search/) (2021-03-01)
58 | - Adds recursive exclusions of directories and child-directories by utilizing fnmatch, many thanks to @seppi91 #5
59 | - All entries (files, headers, dirs) have to be provided as a list (each entry on a new row) in the plugin configuration, see the example in the readme.
60 | - Refactor and code improvements
61 |
62 | ### [0.3.1](https://pypi.org/project/mkdocs-exclude-search/) (2020-02-13)
63 | - Fix bug deactivating the addon when not configuring exclude_tags
64 | - Refactor, add tests
65 |
66 | ### [0.3.0](https://pypi.org/project/mkdocs-exclude-search/)
67 | - Add (undocumented) option to exclude the tags search entries generated by mkdocs-plugin-tags
68 | - Refactorings
69 |
70 | ### [0.2.1](https://pypi.org/project/mkdocs-exclude-search/)
71 | - Fixes bug where path unpacking failed for non .md files.
72 |
73 | ### [0.2.0](https://pypi.org/project/mkdocs-exclude-search/)
74 | - Add exclude folder structures
75 |
76 | ### [0.1.1](https://pypi.org/project/mkdocs-exclude-search/)
77 | - Initial release
78 |
79 |
80 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2018 Your Name Here
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | install:
2 | pip install -e .
3 |
4 | test:
5 | rm -r .pytest_cache || true
6 | black .
7 | python -m pytest --pylint --pylint-rcfile=./pylintrc --mypy --mypy-ignore-missing-imports --cov=mkdocs_exclude_search/ --durations=3 --ignore=./test-repos/
8 | coverage-badge -f -o coverage.svg
9 |
10 | serve-python:
11 | python /Users/christoph.rieke/.virtualenvs/mkdocs-exclude-search/lib/python3.8/site-packages/mkdocs/__main__.py serve
12 |
13 | debug: #breakpoint, In python Console, attach debugger. The magiccomand requires ipython & -e installation of plugin.
14 | %run /Users/christoph.rieke/.virtualenvs/mkdocs-exclude-search/lib/python3.8/site-packages/mkdocs/__main__.py serve
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # mkdocs-exclude-search
2 |
3 | A mkdocs plugin that excludes selected chapters from the docs search index.
4 |
5 | If you only need to exclude a few pages or sections, mkdocs-material now introduced
6 | [built-in search exclusion](https://squidfunk.github.io/mkdocs-material/setup/setting-up-site-search/#search-exclusion)!
7 | The **mkdocs-exclude-search** plugin
8 | [complements](https://squidfunk.github.io/mkdocs-material/blog/2021/09/26/excluding-content-from-search/#whats-new)
9 | this with more configuration options (wildcard exclusions, ignoring excluded subsections). It also provides
10 | search-exclusion functionality to regular mkdocs users.
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | ## Setup
19 |
20 | Install the plugin using pip:
21 |
22 | ```bash
23 | pip install mkdocs-exclude-search
24 | ```
25 |
26 | **Activate the `search` and `exclude-search` plugins in `mkdocs.yml`**. `search` is required, otherwise
27 | `exclude-search` has no effect!
28 |
29 | ```yaml
30 | plugins:
31 | - search
32 | - exclude-search
33 | ```
34 |
35 | More information about plugins in the [MkDocs documentation][mkdocs-plugins].
36 |
37 | ## Configuration
38 |
39 | - List the markdown files to be excluded under `exclude` using the format `//filename.md` in the docs folder.
40 | - Exclude specific heading subsections using the format `//filename.md#some-heading`. Chapter names are all lowercase, `-` as separator, no spaces.
41 | - Exclude all markdown files within a directory (and its children) with `dirname/*`.
42 | - Exclude all markdown files with a specific name within all subdirectories with `dirname/*/filename.md` or `/*/filename.md`.
43 | - To still include a subsection of an excluded file, list the subsection heading under `ignore` using the format `//filename.md#some-heading`.
44 | - To exclude all unreferenced files (markdown files not listed in mkdocs.yml nav section), use `exclude_unreferenced: true`. Default false.
45 |
46 | ```yaml
47 | plugins:
48 | - search
49 | - exclude-search:
50 | exclude:
51 | - first.md
52 | - dir/second.md
53 | - third.md#some-heading
54 | - dir2/*
55 | - /*/fifth.md
56 | ignore:
57 | - dir/second.md#some-heading
58 | exclude_unreferenced: true
59 |
60 | ```
61 | ```yaml
62 | nav:
63 | - Home: index.md
64 | - First chapter: first.md
65 | - Second chapter: dir/second.md
66 | - Third chapter: third.md
67 | - Fourth chapter: dir2/fourth.md
68 | - Fifth chapter: subdir/fifth.md
69 | ```
70 |
71 | This example would exclude:
72 | - the first chapter.
73 | - the second chapter (but still include its `some-heading` section).
74 | - the `some-heading` section of the third chapter.
75 | - all markdown files within `dir2` (and its children directories).
76 | - all markdown files named `fifth.md` within all subdirectories.
77 | - all unreferenced files
78 |
79 | ## See Also
80 |
81 | More information about templates [here][mkdocs-template].
82 |
83 | More information about blocks [here][mkdocs-block].
84 |
85 | [mkdocs-plugins]: http://www.mkdocs.org/user-guide/plugins/
86 | [mkdocs-template]: https://www.mkdocs.org/user-guide/custom-themes/#template-variables
87 | [mkdocs-block]: https://www.mkdocs.org/user-guide/styling-your-docs/#overriding-template-blocks
88 |
--------------------------------------------------------------------------------
/coverage.svg:
--------------------------------------------------------------------------------
1 |
2 |
22 |
--------------------------------------------------------------------------------
/docs/all_dir/all_dir.md:
--------------------------------------------------------------------------------
1 | # alldir Header all_dir Aex
2 |
3 |
4 | ## alldir header all_dir AAex
5 |
6 | alldir text all_dir AAex
7 |
8 |
9 | ## alldir header all_dir BBex
10 |
11 | alldir text all_dir BBex
--------------------------------------------------------------------------------
/docs/all_dir/all_dir_ignore_heading1.md:
--------------------------------------------------------------------------------
1 | # alldir Header all_dir_ignore_heading1 Aex
2 |
3 |
4 | ## alldir header all_dir_ignore_heading1 AAin
5 |
6 | alldir text all_dir_ignore_heading1 AAin
7 |
8 |
9 | ## alldir header all_dir_ignore_heading1 BBex
10 |
11 | alldir text all_dir_ignore_heading1 BBex
--------------------------------------------------------------------------------
/docs/all_dir_sub/all_dir_sub2/all_dir_sub2_1.md:
--------------------------------------------------------------------------------
1 | # alldir Header all_dir_sub2 Aex
2 |
3 |
4 | ## alldir header all_dir_sub2 AAex
5 |
6 | alldir text all_dir_sub2 AAex
7 |
8 |
9 | ## alldir header all_dir_sub2 BBex
10 |
11 | alldir text all_dir_sub2 BBex
12 |
--------------------------------------------------------------------------------
/docs/chapter_exclude_all.md:
--------------------------------------------------------------------------------
1 | # Header chapter_exclude_all Aex
2 |
3 |
4 | ## header chapter_exclude_all AAex
5 |
6 | text chapter_exclude_all AAex
7 |
8 |
9 | ## header chapter_exclude_all BBex
10 |
11 | text chapter_exclude_all BBex
--------------------------------------------------------------------------------
/docs/chapter_exclude_heading2.md:
--------------------------------------------------------------------------------
1 | # single chapter_exclude_heading2 Header Ain
2 |
3 |
4 | ## single header chapter_exclude_heading2 AAin
5 |
6 | single text chapter_exclude_heading2 AAin
7 |
8 |
9 | ## single header chapter_exclude_heading2 BBex
10 |
11 | single text chapter_exclude_heading2 BBex
--------------------------------------------------------------------------------
/docs/dir/dir_chapter_exclude_all.md:
--------------------------------------------------------------------------------
1 | # dir Header dir_chapter_exclude_all Aex
2 |
3 |
4 | ## dir header dir_chapter_exclude_all AAex
5 |
6 | dir text dir_chapter_exclude_all AAex
7 |
8 |
9 | ## dir header dir_chapter_exclude_all BBex
10 |
11 | dir text dir_chapter_exclude_all BBex
--------------------------------------------------------------------------------
/docs/dir/dir_chapter_ignore_heading3.md:
--------------------------------------------------------------------------------
1 | # dir single Header dir_chapter_ignore_heading3 Aex
2 |
3 |
4 | ## dir single header dir_chapter_ignore_heading3 AAex
5 |
6 | dir single text dir_chapter_ignore_heading3 AAex
7 |
8 |
9 | ## dir single header dir_chapter_ignore_heading3 CCin
10 |
11 | dir single text dir_chapter_ignore_heading3 CCin
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Welcome
3 | tags:
4 | - testing
5 | - unimportant
6 | ---
7 |
8 | # Index
9 |
10 | Hello, hello
--------------------------------------------------------------------------------
/docs/toplvl_chapter/file_in_toplvl_chapter.md:
--------------------------------------------------------------------------------
1 | # Header file_in_toplvl_chapter
2 |
3 | text file_in_toplvl_chapter
--------------------------------------------------------------------------------
/docs/toplvl_chapter/sub_chapter/file1_in_sub_chapter.md:
--------------------------------------------------------------------------------
1 | # Header file1_in_sub_chapter
2 |
3 | text file1_in_sub_chapter
--------------------------------------------------------------------------------
/docs/toplvl_chapter/sub_chapter/file2_in_sub_chapter.md:
--------------------------------------------------------------------------------
1 | # Header file2_in_sub_chapter
2 |
3 | text file2_in_sub_chapter
--------------------------------------------------------------------------------
/docs/toplvl_chapter/sub_chapter/unreferenced_in_sub_chapter.md:
--------------------------------------------------------------------------------
1 | # Header unreferenced_in_sub_chapter
2 |
3 | text unreferenced_in_sub_chapter
--------------------------------------------------------------------------------
/docs/unreferenced.md:
--------------------------------------------------------------------------------
1 | # unreferenced file heading1 Aex
2 |
3 | This is an example of an unreferenced file
4 |
5 | ## Unreferenced file heading2 AAex
6 |
7 | ## Unreferenced file heading2 BBex
--------------------------------------------------------------------------------
/docs/without_nav_name.md:
--------------------------------------------------------------------------------
1 | # without_nav_name file heading1 Ain
2 |
3 | This is an example of a file that in the nav section is provided without a name
4 |
5 | ## without_nav_name file heading2 Ain
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: My Docs
2 |
3 | nav:
4 | - index: index.md
5 | - without_nav_name.md
6 | - chapter_exclude_all: chapter_exclude_all.md
7 | - chapter_exclude_heading2: chapter_exclude_heading2.md
8 | - dir_chapter_exclude_all: dir/dir_chapter_exclude_all.md
9 | - dir_chapter_ignore_heading3: dir/dir_chapter_ignore_heading3.md
10 | - all_dir: all_dir/all_dir.md
11 | - all_dir_ignore_heading1: all_dir/all_dir_ignore_heading1.md
12 | - all_dir_sub2: all_dir_sub/all_dir_sub2/all_dir_sub2_1.md
13 | - toplvl_chapter:
14 | - toplvl_chapter/file_in_toplvl_chapter.md
15 | - sub_chapter:
16 | - toplvl_chapter/sub_chapter/file1_in_sub_chapter.md
17 | - toplvl_chapter/sub_chapter/file2_in_sub_chapter.md
18 |
19 | theme:
20 | name: material
21 |
22 | plugins:
23 | - search
24 | - tags
25 | - exclude-search:
26 | exclude:
27 | - chapter_exclude_all.md
28 | - chapter_exclude_heading2.md#single-header-chapter_exclude_heading2-bbex # Always a single # for all header levels.
29 | - dir/dir_chapter_exclude_all.md
30 | - dir/dir_chapter_ignore_heading3.md
31 | - all_dir/*
32 | - all_dir_sub/all_dir_sub2/*
33 | ignore:
34 | - dir/dir_chapter_ignore_heading3.md#dir-single-header-dir_chapter_ignore_heading3-ccin
35 | - all_dir/all_dir_ignore_heading1.md#alldir-header-all_dir_ignore_heading1-aain
36 | exclude_unreferenced: true
37 | #exclude_tags: true # Default False, only relevant for excluding tags of mkdocs-plugin-tags
--------------------------------------------------------------------------------
/mkdocs_exclude_search/__init__.py:
--------------------------------------------------------------------------------
1 | from .plugin import ExcludeSearch
2 |
--------------------------------------------------------------------------------
/mkdocs_exclude_search/plugin.py:
--------------------------------------------------------------------------------
1 | import json
2 | from pathlib import Path
3 | import logging
4 | from typing import List, Dict, Tuple, Union, Any
5 | from fnmatch import fnmatch
6 |
7 | import mkdocs
8 | from mkdocs.config import config_options
9 | from mkdocs.plugins import BasePlugin
10 | from packaging.version import Version
11 |
12 | from mkdocs_exclude_search.utils import explode_navigation
13 |
14 |
15 | def get_logger():
16 | logger = logging.getLogger("mkdocs.plugins.mkdocs-exclude-search")
17 | MKDOCS_LOG_VERSION = "1.2"
18 | if Version(mkdocs.__version__) < Version(MKDOCS_LOG_VERSION):
19 | # filter doesn't do anything since that version
20 | # pylint: disable=import-outside-toplevel, no-name-in-module
21 | from mkdocs.utils import warning_filter
22 |
23 | logger.addFilter(warning_filter)
24 |
25 | return logger
26 |
27 |
28 | logger = get_logger()
29 |
30 |
31 | class ExcludeSearch(BasePlugin):
32 | """
33 | Excludes selected files, nav chapters and headers from the search index.
34 | """
35 |
36 | config_scheme = (
37 | ("exclude", config_options.Type((str, list), default=[])),
38 | ("ignore", config_options.Type((str, list), default=[])),
39 | ("exclude_unreferenced", config_options.Type(bool, default=False)),
40 | ("exclude_tags", config_options.Type(bool, default=False)),
41 | )
42 |
43 | def __init__(self):
44 | self.enabled = True
45 | self.total_time = 0
46 |
47 | def validate_config(self, plugins: List[str]):
48 | """
49 | Validate mkdocs-exclude-search plugin configuration.
50 | """
51 | if not ("search" in plugins or "material/search" in plugins):
52 | message = (
53 | "mkdocs-exclude-search plugin is activated but has no effect as "
54 | "search plugin is deactivated!"
55 | )
56 | logger.debug(message)
57 | raise ValueError(message)
58 |
59 | if (
60 | not self.config["exclude"]
61 | and not self.config["exclude_unreferenced"]
62 | and not self.config["exclude_tags"]
63 | ):
64 | message = (
65 | "No excluded search entries selected for mkdocs-exclude-search, "
66 | "the plugin has no effect!"
67 | )
68 | logger.info(message)
69 | raise ValueError(message)
70 |
71 | try:
72 | if self.config["ignore"]:
73 | invalid_ignored = [x for x in self.config["ignore"] if "#" not in x]
74 | message = (
75 | f"mkdocs-exclude-search configuration for `ignore` can only be "
76 | f"headers (containing `#`), the following entries will be ignored: {invalid_ignored}"
77 | )
78 | logger.info(message)
79 | self.config["ignore"] = [
80 | x for x in self.config["ignore"] if not x in invalid_ignored
81 | ]
82 | except KeyError:
83 | pass
84 |
85 | @staticmethod
86 | def resolve_excluded_records(
87 | to_exclude: List[str],
88 | ) -> List:
89 | """
90 | Resolve the search index file-name and header-names from the user provided excluded entries.
91 |
92 | Args:
93 | to_exclude: The user provided list of excluded entries for files,
94 | headers and directories ("*").
95 |
96 | Returns:
97 | A list with each resolved entry as a tuple of (file-name, header-name/None).
98 | """
99 | excluded_entries = []
100 | # TODO: This currently could exclude files with an excluded folder of the same name.
101 | for entry in to_exclude:
102 | try:
103 | file_name, header_name = entry.split("#")
104 | except ValueError:
105 | file_name, header_name = entry, None # type: ignore
106 | excluded_entries.append((file_name, header_name))
107 | return excluded_entries
108 |
109 | @staticmethod
110 | def resolve_ignored_chapters(to_ignore: List[str]) -> List:
111 | """
112 | Supplement the search index main entry for each user provided ignored header.
113 |
114 | In order for a header subchapter to be available in the search index, it requires one
115 | "file-name" entry and one "file-name/header-name" entry.
116 |
117 | Args:
118 | to_ignore: The user provided list of ignored entries for chapters.
119 |
120 | Returns:
121 | A list with each resolved entry as a tuple of (file-name, header-name/None),
122 | and with the supplemented main_name entries.
123 | """
124 | file_name_entries = []
125 | file_header_names_entries = []
126 | for entry in to_ignore:
127 | file_name, header_name = entry.split("#")
128 | file_name_entries.append((file_name, None))
129 | file_header_names_entries.append((file_name, header_name))
130 |
131 | ignored_chapters = file_name_entries + file_header_names_entries # type: ignore
132 | return ignored_chapters
133 |
134 | @staticmethod
135 | def is_unreferenced_record(rec_file_name: str, navigation_items: List[str]):
136 | """
137 | Unreferenced markdown files that are not contained in mkdocs.yml navigation
138 | nav section.
139 | """
140 | return rec_file_name not in navigation_items
141 |
142 | @staticmethod
143 | def is_tag_record(rec_file_name: str):
144 | """Tags entries of mkdocs-plugin-tags"""
145 | # TODO: Surface in readme
146 | return "tags.html" in rec_file_name
147 |
148 | @staticmethod
149 | def is_root_record(rec_file_name: str):
150 | """Required mkdocs root files.
151 |
152 | Collides with is_tag_record as these have no slash. Handled by order in select_included_records.
153 | """
154 | return "/" not in rec_file_name
155 |
156 | @staticmethod
157 | def is_ignored_record(
158 | rec_file_name: str, rec_header_name: Union[str, None], to_ignore: List[Tuple]
159 | ):
160 | """
161 | Headers selected by the user as to be ignored from the exclusions.
162 |
163 | Args:
164 | rec_file_name: The file name as in the search index record, e.g. 'all_dir/all_dir_ignore_heading1/'
165 | rec_header_name: The header name as in the search index record, e.g. None or
166 | 'single-header-chapter_exclude_heading2-bbex'
167 | to_ignore: The list of to be ignored (records (from the exclusion) with tuples
168 | of (rec_file_name, rec_header_name), e.g. ('chapter_exclude_all.md', None)
169 |
170 | Returns:
171 | True if the record matches with the to_ignore list, None if not.
172 | """
173 | if any(
174 | (
175 | fnmatch(rec_file_name[:-1], f"{file_name.replace('.md', '')}")
176 | and header_name == rec_header_name
177 | for (file_name, header_name) in to_ignore
178 | )
179 | ):
180 | return True
181 |
182 | @staticmethod
183 | def is_excluded_record(
184 | rec_file_name: str, rec_header_name: Union[str, None], to_exclude: List[Tuple]
185 | ):
186 | """
187 | Files, headers or directories selected by the user to be excluded.
188 |
189 | Args:
190 | rec_file_name: The file name as in the search index record, e.g. 'chapter_exclude_all/'
191 | rec_header_name: The header name as in the search index record, e.g. None or
192 | 'single-header-chapter_exclude_heading2-bbex'
193 | to_exclude: The list of to be excluded records with tuples of (rec_file_name, rec_header_name),
194 | e.g. ('chapter_exclude_all.md', None)
195 |
196 | Returns:
197 | True if the record matches with the to_exclude list, None if not.
198 | """
199 | if any(
200 | (
201 | fnmatch(rec_file_name[:-1], f"{file_name.replace('.md', '')}")
202 | and (rec_header_name == header_name or not header_name)
203 | for (file_name, header_name) in to_exclude
204 | )
205 | ):
206 | return True
207 |
208 | def select_included_records(
209 | self,
210 | search_index: Dict,
211 | to_exclude: List[Tuple[Any, ...]],
212 | to_ignore: List[Tuple[Any, ...]],
213 | navigation_items: List[str],
214 | exclude_unreferenced: bool = False,
215 | exclude_tags: bool = False,
216 | ) -> List[Dict]:
217 | """
218 | Select the search index records to be included in the final selection.
219 |
220 | Args:
221 | search_index: The mkdocs search index in "config.data["site_dir"]) / "search/search_index.json"
222 | to_exclude: Resolved list of excluded search index records.
223 | to_ignore: Resolved list of ignored search index chapter records.
224 | navigation_items: List of markdown filepaths in the mkdocs.yml nav, in the format
225 | ["filename/", dir/filename/]
226 | exclude_unreferenced: Boolean wether unreferenced files (not listed in mkdocs nav)
227 | should be excluded, default False.
228 | exclude_tags: Boolean wether mkdocs-plugin-tags entries should be excluded, default False.
229 |
230 | Returns:
231 | A new search index as a list of dicts.
232 | """
233 | included_records = []
234 | for record in search_index["docs"]:
235 | try:
236 | rec_file_name, rec_header_name = record["location"].split("#")
237 | except ValueError:
238 | rec_file_name, rec_header_name = record["location"], None
239 |
240 | # pylint: disable=no-else-continue
241 | if exclude_tags and self.is_tag_record(rec_file_name):
242 | logger.debug(f"exclude-search (excludedTags): {record['location']}")
243 | continue
244 | elif self.is_root_record(rec_file_name):
245 | # logger.debug(f"include-search (requiredRoot): {record['location']}")
246 | included_records.append(record)
247 | elif exclude_unreferenced and self.is_unreferenced_record(
248 | rec_file_name=rec_file_name, navigation_items=navigation_items
249 | ):
250 | logger.debug(
251 | f"exclude-search (excludedUnreferenced): {record['location']}"
252 | )
253 | continue
254 | elif self.is_ignored_record(rec_file_name, rec_header_name, to_ignore):
255 | logger.debug(f"include-search (ignoredRule): {record['location']}")
256 | included_records.append(record)
257 | elif self.is_excluded_record(rec_file_name, rec_header_name, to_exclude):
258 | logger.debug(f"exclude-search (excludedRule): {record['location']}")
259 | continue
260 | else:
261 | # logger.debug(f"include-search (noRule): {record['location']}")
262 | included_records.append(record)
263 |
264 | return included_records
265 |
266 | # pylint: disable=arguments-differ
267 | def on_post_build(self, config):
268 | # at mkdocs buildtime, self.config does not contain the same as config
269 | try:
270 | self.validate_config(plugins=config["plugins"])
271 | except ValueError:
272 | return config
273 |
274 | search_index_fp = Path(config.data["site_dir"]) / "search/search_index.json"
275 | with open(search_index_fp, "r") as f:
276 | search_index = json.load(f)
277 |
278 | to_exclude = self.config["exclude"]
279 | if to_exclude:
280 | to_exclude = self.resolve_excluded_records(to_exclude=to_exclude)
281 | to_ignore = self.config["ignore"]
282 | if to_ignore:
283 | to_ignore = self.resolve_ignored_chapters(to_ignore=to_ignore)
284 |
285 | if self.config["exclude_unreferenced"] and config.data["nav"] is not None:
286 | navigation_items = explode_navigation(navigation=config.data["nav"])
287 | else:
288 | navigation_items = []
289 |
290 | included_records = self.select_included_records(
291 | search_index=search_index,
292 | to_exclude=to_exclude,
293 | to_ignore=to_ignore,
294 | navigation_items=navigation_items,
295 | exclude_unreferenced=self.config["exclude_unreferenced"],
296 | exclude_tags=self.config["exclude_tags"],
297 | )
298 |
299 | logger.info(
300 | f"mkdocs-exclude-search excluded {len(search_index['docs']) - len(included_records)}"
301 | f" of {len(search_index['docs'])} search index records. Use `mkdocs serve -v` "
302 | f"for more details."
303 | )
304 |
305 | search_index["docs"] = included_records
306 | with open(search_index_fp, "w") as f:
307 | json.dump(search_index, f)
308 |
309 | return config
310 |
--------------------------------------------------------------------------------
/mkdocs_exclude_search/utils.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 |
4 | def iterate_all_values(nested_dict: dict):
5 | """
6 | Returns an iterator that returns all values of a (nested) iterable of the form
7 | {'a': ['aa', {'b': ['cc', {'d': ['ee', 'ff', {'d': ['gg', 'hh']}]}]}]}
8 |
9 | Inspired by https://gist.github.com/PatrikHlobil/9d045e43fe44df2d5fd8b570f9fd78cc
10 | """
11 | if isinstance(nested_dict, dict):
12 | for value in nested_dict.values():
13 | if not isinstance(value, (dict, list)):
14 | yield value
15 | for ret in iterate_all_values(value):
16 | yield ret
17 | elif isinstance(nested_dict, list):
18 | for el in nested_dict:
19 | for ret in iterate_all_values(el):
20 | yield ret
21 | elif isinstance(nested_dict, str):
22 | yield nested_dict
23 |
24 |
25 | def explode_navigation(navigation: list) -> List[str]:
26 | # Paths to chapters in mkdocs.yml navigation section to compare
27 | # with unreferenced files.
28 | navigation_paths = []
29 |
30 | for chapter in navigation:
31 | if isinstance(chapter, str):
32 | # e.g. - index.md (without name in nav)
33 | navigation_paths.append(chapter)
34 | elif isinstance(chapter, dict):
35 | chapter_paths = list(chapter.values())[0]
36 | if isinstance(chapter_paths, str):
37 | # e.g. - chapter_exclude_all: chapter_exclude_all.md
38 | navigation_paths.append(chapter_paths)
39 | elif isinstance(chapter_paths, list):
40 | # e.g. - toplvl_chapter:
41 | # - toplvl_chapter/file_in_toplvl_chapter.md
42 | exploded_chapter_paths = iterate_all_values(nested_dict=chapter)
43 | navigation_paths.extend(exploded_chapter_paths)
44 |
45 | navigation_paths = [nav_path.replace(".md", "/") for nav_path in navigation_paths]
46 |
47 | return navigation_paths
48 |
--------------------------------------------------------------------------------
/pylintrc:
--------------------------------------------------------------------------------
1 | [MASTER]
2 | init-hook='import glob; [sys.path.append(d) for d in glob.glob("*/") if not d.startswith("_")]'
3 |
4 | [MESSAGE CONTROL]
5 | disable=
6 | missing-docstring,
7 | no-else-return,
8 | too-few-public-methods,
9 | missing-final-newline,
10 | too-many-boolean-expressions,
11 | bad-continuation,
12 | invalid-name,
13 | super-init-not-called,
14 | inconsistent-return-statements,
15 | too-many-arguments,
16 | too-many-locals,
17 | protected-access,
18 | redefined-outer-name,
19 | too-many-instance-attributes,
20 | fixme,
21 | duplicate-code,
22 | logging-fstring-interpolation,
23 | logging-format-interpolation,
24 | unspecified-encoding
25 |
26 | [FORMAT]
27 | max-line-length=120
28 | single-line-if-stmt=yes
29 | include-naming-hint=yes
30 | function-rgx=[a-z_][a-z0-9_]*$
31 | argument-rgx=[a-z_][a-z0-9_]*$
32 | variable-rgx=[a-z_][a-z0-9_]*$
33 | # "logger" and "api" are common module-level globals, and not true 'constants'
34 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__)|logger|api|_api)$
35 |
36 | [DESIGN]
37 | max-args=6
38 | ignored-argument-names=_.*|self
39 |
40 | [SIMILARITIES]
41 | # Minimum lines number of a similarity.
42 | min-similarity-lines=20 # TODO: Reset lower when pylint bug fixed #214.
43 | ignore-comments=yes
44 | ignore-docstrings=yes
45 | ignore-imports=no
46 |
--------------------------------------------------------------------------------
/requirements_dev.txt:
--------------------------------------------------------------------------------
1 | mkdocs
2 | mkdocs-material
3 | black
4 | twine
5 | pytest
6 | pylint
7 | pytest
8 | pytest-pylint
9 | pytest-sugar
10 | mypy
11 | mypy-extensions
12 | pytest-cov
13 | pytest-mypy
14 | coverage-badge
15 | git+https://github.com/jldiaz/mkdocs-plugin-tags.git
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 | with open("README.md", "r") as fh:
4 | LONG_DESCRIPTION = fh.read()
5 |
6 | setup(
7 | name="mkdocs-exclude-search",
8 | version="0.6.6",
9 | description="A mkdocs plugin that lets you exclude selected files or sections "
10 | "from the search index.",
11 | long_description=LONG_DESCRIPTION,
12 | long_description_content_type="text/markdown",
13 | keywords="mkdocs",
14 | url="https://github.com/chrieke/mkdocs-exclude-search",
15 | author="Christoph Rieke",
16 | author_email="christoph.k.rieke@gmail.com",
17 | license="MIT",
18 | python_requires=">=3.6",
19 | install_requires=["mkdocs>=1.0.4"],
20 | classifiers=[
21 | "Development Status :: 5 - Production/Stable",
22 | "Intended Audience :: Developers",
23 | "Intended Audience :: Information Technology",
24 | "License :: OSI Approved :: MIT License",
25 | "Programming Language :: Python",
26 | "Programming Language :: Python :: 3 :: Only",
27 | "Programming Language :: Python :: 3.7",
28 | "Programming Language :: Python :: 3.8",
29 | "Programming Language :: Python :: 3.9",
30 | "Programming Language :: Python :: 3.10",
31 | ],
32 | packages=find_packages(exclude=("tests", "docs")),
33 | entry_points={
34 | "mkdocs.plugins": ["exclude-search = mkdocs_exclude_search:ExcludeSearch"]
35 | },
36 | )
37 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrieke/mkdocs-exclude-search/abf2fb2ca67c56271b95e8169187a62fd4820a3c/tests/__init__.py
--------------------------------------------------------------------------------
/tests/context.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
5 | sys.path.insert(
6 | 0,
7 | os.path.abspath(
8 | os.path.join(os.path.dirname(__file__), "../mkdocs_exclude_search")
9 | ),
10 | )
11 |
12 | # pylint: disable=wrong-import-position,unused-import
13 | from plugin import ExcludeSearch
14 | from utils import iterate_all_values, explode_navigation
15 |
--------------------------------------------------------------------------------
/tests/globals.py:
--------------------------------------------------------------------------------
1 | TO_EXCLUDE = [
2 | "chapter_exclude_all.md",
3 | "chapter_exclude_heading2.md#single-header-chapter_exclude_heading2-bbex",
4 | "dir/dir_chapter_exclude_all.md",
5 | "dir/dir_chapter_ignore_heading3.md",
6 | "all_dir/*",
7 | "all_dir_sub/all_dir_sub2/*",
8 | ]
9 |
10 | RESOLVED_EXCLUDED_RECORDS = [
11 | ("chapter_exclude_all.md", None),
12 | ("chapter_exclude_heading2.md", "single-header-chapter_exclude_heading2-bbex"),
13 | ("dir/dir_chapter_exclude_all.md", None),
14 | ("dir/dir_chapter_ignore_heading3.md", None),
15 | ("all_dir/*", None),
16 | ("all_dir_sub/all_dir_sub2/*", None),
17 | ]
18 |
19 | TO_IGNORE = [
20 | "dir/dir_chapter_ignore_heading3.md#dir-single-header-dir_chapter_ignore_heading3-ccin",
21 | "all_dir/all_dir_ignore_heading1.md#alldir-header-all_dir_ignore_heading1-aain",
22 | ]
23 |
24 | RESOLVED_IGNORED_CHAPTERS = [
25 | (
26 | "dir/dir_chapter_ignore_heading3.md",
27 | "dir-single-header-dir_chapter_ignore_heading3-ccin",
28 | ),
29 | (
30 | "all_dir/all_dir_ignore_heading1.md",
31 | "alldir-header-all_dir_ignore_heading1-aain",
32 | ),
33 | ("dir/dir_chapter_ignore_heading3.md", None),
34 | ("all_dir/all_dir_ignore_heading1.md", None),
35 | ]
36 |
37 | EXCLUDE_UNREFERENCED = False
38 | EXCLUDE_TAGS = False
39 |
40 | NAVIGATION = [
41 | {"index": "index.md"},
42 | "without_nav_name.md",
43 | {"chapter_exclude_all": "chapter_exclude_all.md"},
44 | {
45 | "toplvl_chapter": [
46 | "toplvl_chapter/file_in_toplvl_chapter.md",
47 | {
48 | "sub_chapter": [
49 | "toplvl_chapter/sub_chapter/file1_in_sub_chapter.md",
50 | "toplvl_chapter/sub_chapter/file2_in_sub_chapter.md",
51 | ]
52 | },
53 | ]
54 | },
55 | ]
56 |
57 | INCLUDED_RECORDS = [
58 | {"location": "", "text": "Index Hello, hello", "title": "index"},
59 | {"location": "#index", "text": "Hello, hello", "title": "Index"},
60 | {
61 | "location": "chapter_exclude_heading2/",
62 | "text": "single chapter_exclude_heading2 Header Ain single header chapter_exclude_heading2 "
63 | "AAin single text chapter_exclude_heading2 AAin single header chapter_exclude_heading2 "
64 | "BBex single text chapter_exclude_heading2 BBex",
65 | "title": "chapter_exclude_heading2",
66 | },
67 | {
68 | "location": "chapter_exclude_heading2/#single-chapter_exclude_heading2-header-ain",
69 | "text": "",
70 | "title": "single chapter_exclude_heading2 Header Ain",
71 | },
72 | {
73 | "location": "chapter_exclude_heading2/#single-header-chapter_exclude_heading2-aain",
74 | "text": "single text chapter_exclude_heading2 AAin",
75 | "title": "single header chapter_exclude_heading2 AAin",
76 | },
77 | {
78 | "location": "unreferenced/",
79 | "text": "unreferenced file heading1 Aex This is an example of an unreferenced file "
80 | "Unreferenced file heading2 AAex Unreferenced file heading2 BBex",
81 | "title": "unreferenced file heading1 Aex",
82 | },
83 | {
84 | "location": "unreferenced/#unreferenced-file-heading1-aex",
85 | "text": "This is an example of an unreferenced file",
86 | "title": "unreferenced file heading1 Aex",
87 | },
88 | {
89 | "location": "unreferenced/#unreferenced-file-heading2-aaex",
90 | "text": "",
91 | "title": "Unreferenced file heading2 AAex",
92 | },
93 | {
94 | "location": "unreferenced/#unreferenced-file-heading2-bbex",
95 | "text": "",
96 | "title": "Unreferenced file heading2 BBex",
97 | },
98 | {
99 | "location": "all_dir/all_dir_ignore_heading1/",
100 | "text": "alldir Header all_dir_ignore_heading1 Aex alldir header all_dir_ignore_heading1 "
101 | "AAin alldir text all_dir_ignore_heading1 AAin alldir header all_dir_ignore_heading1 "
102 | "BBex alldir text all_dir_ignore_heading1 BBex",
103 | "title": "all_dir_ignore_heading1",
104 | },
105 | {
106 | "location": "all_dir/all_dir_ignore_heading1/#alldir-header-all_dir_ignore_heading1-aain",
107 | "text": "alldir text all_dir_ignore_heading1 AAin",
108 | "title": "alldir header all_dir_ignore_heading1 AAin",
109 | },
110 | {
111 | "location": "dir/dir_chapter_ignore_heading3/",
112 | "text": "dir single Header dir_chapter_ignore_heading3 Aex dir single header dir_chapter_ignore_heading3 "
113 | "AAex dir single text dir_chapter_ignore_heading3 AAex dir single header dir_chapter_ignore_heading3 "
114 | "CCin dir single text dir_chapter_ignore_heading3 CCin",
115 | "title": "dir_chapter_ignore_heading3",
116 | },
117 | {
118 | "location": "dir/dir_chapter_ignore_heading3/#dir-single-header-dir_chapter_ignore_heading3-ccin",
119 | "text": "dir single text dir_chapter_ignore_heading3 CCin",
120 | "title": "dir single header dir_chapter_ignore_heading3 CCin",
121 | },
122 | {
123 | "location": "toplvl_chapter/file_in_toplvl_chapter/",
124 | "text": "Header file_in_toplvl_chapter text file_in_toplvl_chapter",
125 | "title": "Header file_in_toplvl_chapter",
126 | },
127 | {
128 | "location": "toplvl_chapter/file_in_toplvl_chapter/#header-file_in_toplvl_chapter",
129 | "text": "text file_in_toplvl_chapter",
130 | "title": "Header file_in_toplvl_chapter",
131 | },
132 | {
133 | "location": "toplvl_chapter/sub_chapter/file1_in_sub_chapter/",
134 | "text": "Header file1_in_sub_chapter text file1_in_sub_chapter",
135 | "title": "Header file1_in_sub_chapter",
136 | },
137 | {
138 | "location": "toplvl_chapter/sub_chapter/file1_in_sub_chapter/#header-file1_in_sub_chapter",
139 | "text": "text file1_in_sub_chapter",
140 | "title": "Header file1_in_sub_chapter",
141 | },
142 | {
143 | "location": "toplvl_chapter/sub_chapter/file2_in_sub_chapter/",
144 | "text": "Header file2_in_sub_chapter text file2_in_sub_chapter",
145 | "title": "Header file2_in_sub_chapter",
146 | },
147 | {
148 | "location": "toplvl_chapter/sub_chapter/file2_in_sub_chapter/#header-file2_in_sub_chapter",
149 | "text": "text file2_in_sub_chapter",
150 | "title": "Header file2_in_sub_chapter",
151 | },
152 | {
153 | "location": "toplvl_chapter/sub_chapter/unreferenced_in_sub_chapter/",
154 | "text": "Header unreferenced_in_sub_chapter text unreferenced_in_sub_chapter",
155 | "title": "Header unreferenced_in_sub_chapter",
156 | },
157 | {
158 | "location": "toplvl_chapter/sub_chapter/unreferenced_in_sub_chapter/#header-unreferenced_in_sub_chapter",
159 | "text": "text unreferenced_in_sub_chapter",
160 | "title": "Header unreferenced_in_sub_chapter",
161 | },
162 | {
163 | "location": "tags.html",
164 | "text": "Contents grouped by tag testing Welcome unimportant Welcome",
165 | "title": "Tags",
166 | },
167 | {
168 | "location": "tags.html#contents-grouped-by-tag",
169 | "text": "",
170 | "title": "Contents grouped by tag",
171 | },
172 | {"location": "tags.html#testing", "text": "Welcome", "title": "testing"},
173 | {"location": "tags.html#unimportant", "text": "Welcome", "title": "unimportant"},
174 | ]
175 |
--------------------------------------------------------------------------------
/tests/mock_data/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "config_file_path": "mkdocs.yml",
3 | "site_name": "My Docs",
4 | "nav": [
5 | {
6 | "index": "index.md"
7 | },
8 | {
9 | "chapter_exclude_all": "chapter_exclude_all.md"
10 | },
11 | {
12 | "chapter_exclude_heading2": "chapter_exclude_heading2.md"
13 | },
14 | {
15 | "dir_chapter_exclude_all": "dir/dir_chapter_exclude_all.md"
16 | },
17 | {
18 | "dir_chapter_ignore_heading3": "dir/dir_chapter_ignore_heading3.md"
19 | },
20 | {
21 | "all_dir": "all_dir/all_dir.md"
22 | },
23 | {
24 | "all_dir_ignore_heading1": "all_dir/all_dir_ignore_heading1.md"
25 | },
26 | {
27 | "all_dir_sub2": "all_dir_sub/all_dir_sub2/all_dir_sub2_1.md"
28 | },
29 | {
30 | "toplvl_chapter": [
31 | "toplvl_chapter/file_in_toplvl_chapter.md",
32 | {
33 | "sub_chapter": [
34 | "toplvl_chapter/sub_chapter/file1_in_sub_chapter.md",
35 | "toplvl_chapter/sub_chapter/file2_in_sub_chapter.md"
36 | ]
37 | }
38 | ]
39 | }
40 | ],
41 | "pages": null,
42 | "site_url": "http://127.0.0.1:8000/",
43 | "site_description": null,
44 | "site_author": null,
45 | "theme": null,
46 | "plugins": ["search"]}
--------------------------------------------------------------------------------
/tests/mock_data/mock_search_index.json:
--------------------------------------------------------------------------------
1 | {
2 | "config": {
3 | "indexing": "full",
4 | "lang": [
5 | "en"
6 | ],
7 | "min_search_length": 3,
8 | "prebuild_index": false,
9 | "separator": "[\\s\\-]+"
10 | },
11 | "docs": [
12 | {
13 | "location": "",
14 | "text": "Index Hello, hello",
15 | "title": "index"
16 | },
17 | {
18 | "location": "#index",
19 | "text": "Hello, hello",
20 | "title": "Index"
21 | },
22 | {
23 | "location": "chapter_exclude_all/",
24 | "text": "Header chapter_exclude_all Aex header chapter_exclude_all AAex text chapter_exclude_all AAex header chapter_exclude_all BBex text chapter_exclude_all BBex",
25 | "title": "chapter_exclude_all"
26 | },
27 | {
28 | "location": "chapter_exclude_all/#header-chapter_exclude_all-aex",
29 | "text": "",
30 | "title": "Header chapter_exclude_all Aex"
31 | },
32 | {
33 | "location": "chapter_exclude_all/#header-chapter_exclude_all-aaex",
34 | "text": "text chapter_exclude_all AAex",
35 | "title": "header chapter_exclude_all AAex"
36 | },
37 | {
38 | "location": "chapter_exclude_all/#header-chapter_exclude_all-bbex",
39 | "text": "text chapter_exclude_all BBex",
40 | "title": "header chapter_exclude_all BBex"
41 | },
42 | {
43 | "location": "chapter_exclude_heading2/",
44 | "text": "single chapter_exclude_heading2 Header Ain single header chapter_exclude_heading2 AAin single text chapter_exclude_heading2 AAin single header chapter_exclude_heading2 BBex single text chapter_exclude_heading2 BBex",
45 | "title": "chapter_exclude_heading2"
46 | },
47 | {
48 | "location": "chapter_exclude_heading2/#single-chapter_exclude_heading2-header-ain",
49 | "text": "",
50 | "title": "single chapter_exclude_heading2 Header Ain"
51 | },
52 | {
53 | "location": "chapter_exclude_heading2/#single-header-chapter_exclude_heading2-aain",
54 | "text": "single text chapter_exclude_heading2 AAin",
55 | "title": "single header chapter_exclude_heading2 AAin"
56 | },
57 | {
58 | "location": "chapter_exclude_heading2/#single-header-chapter_exclude_heading2-bbex",
59 | "text": "single text chapter_exclude_heading2 BBex",
60 | "title": "single header chapter_exclude_heading2 BBex"
61 | },
62 | {
63 | "location": "unreferenced/",
64 | "text": "unreferenced file heading1 Aex This is an example of an unreferenced file Unreferenced file heading2 AAex Unreferenced file heading2 BBex",
65 | "title": "unreferenced file heading1 Aex"
66 | },
67 | {
68 | "location": "unreferenced/#unreferenced-file-heading1-aex",
69 | "text": "This is an example of an unreferenced file",
70 | "title": "unreferenced file heading1 Aex"
71 | },
72 | {
73 | "location": "unreferenced/#unreferenced-file-heading2-aaex",
74 | "text": "",
75 | "title": "Unreferenced file heading2 AAex"
76 | },
77 | {
78 | "location": "unreferenced/#unreferenced-file-heading2-bbex",
79 | "text": "",
80 | "title": "Unreferenced file heading2 BBex"
81 | },
82 | {
83 | "location": "all_dir/all_dir/",
84 | "text": "alldir Header all_dir Aex alldir header all_dir AAex alldir text all_dir AAex alldir header all_dir BBex alldir text all_dir BBex",
85 | "title": "all_dir"
86 | },
87 | {
88 | "location": "all_dir/all_dir/#alldir-header-all_dir-aex",
89 | "text": "",
90 | "title": "alldir Header all_dir Aex"
91 | },
92 | {
93 | "location": "all_dir/all_dir/#alldir-header-all_dir-aaex",
94 | "text": "alldir text all_dir AAex",
95 | "title": "alldir header all_dir AAex"
96 | },
97 | {
98 | "location": "all_dir/all_dir/#alldir-header-all_dir-bbex",
99 | "text": "alldir text all_dir BBex",
100 | "title": "alldir header all_dir BBex"
101 | },
102 | {
103 | "location": "all_dir/all_dir_ignore_heading1/",
104 | "text": "alldir Header all_dir_ignore_heading1 Aex alldir header all_dir_ignore_heading1 AAin alldir text all_dir_ignore_heading1 AAin alldir header all_dir_ignore_heading1 BBex alldir text all_dir_ignore_heading1 BBex",
105 | "title": "all_dir_ignore_heading1"
106 | },
107 | {
108 | "location": "all_dir/all_dir_ignore_heading1/#alldir-header-all_dir_ignore_heading1-aex",
109 | "text": "",
110 | "title": "alldir Header all_dir_ignore_heading1 Aex"
111 | },
112 | {
113 | "location": "all_dir/all_dir_ignore_heading1/#alldir-header-all_dir_ignore_heading1-aain",
114 | "text": "alldir text all_dir_ignore_heading1 AAin",
115 | "title": "alldir header all_dir_ignore_heading1 AAin"
116 | },
117 | {
118 | "location": "all_dir/all_dir_ignore_heading1/#alldir-header-all_dir_ignore_heading1-bbex",
119 | "text": "alldir text all_dir_ignore_heading1 BBex",
120 | "title": "alldir header all_dir_ignore_heading1 BBex"
121 | },
122 | {
123 | "location": "all_dir_sub/all_dir_sub2/all_dir_sub2_1/",
124 | "text": "alldir Header all_dir_sub2 Aex alldir header all_dir_sub2 AAex alldir text all_dir_sub2 AAex alldir header all_dir_sub2 BBex alldir text all_dir_sub2 BBex",
125 | "title": "all_dir_sub2"
126 | },
127 | {
128 | "location": "all_dir_sub/all_dir_sub2/all_dir_sub2_1/#alldir-header-all_dir_sub2-aex",
129 | "text": "",
130 | "title": "alldir Header all_dir_sub2 Aex"
131 | },
132 | {
133 | "location": "all_dir_sub/all_dir_sub2/all_dir_sub2_1/#alldir-header-all_dir_sub2-aaex",
134 | "text": "alldir text all_dir_sub2 AAex",
135 | "title": "alldir header all_dir_sub2 AAex"
136 | },
137 | {
138 | "location": "all_dir_sub/all_dir_sub2/all_dir_sub2_1/#alldir-header-all_dir_sub2-bbex",
139 | "text": "alldir text all_dir_sub2 BBex",
140 | "title": "alldir header all_dir_sub2 BBex"
141 | },
142 | {
143 | "location": "dir/dir_chapter_exclude_all/",
144 | "text": "dir Header dir_chapter_exclude_all Aex dir header dir_chapter_exclude_all AAex dir text dir_chapter_exclude_all AAex dir header dir_chapter_exclude_all BBex dir text dir_chapter_exclude_all BBex",
145 | "title": "dir_chapter_exclude_all"
146 | },
147 | {
148 | "location": "dir/dir_chapter_exclude_all/#dir-header-dir_chapter_exclude_all-aex",
149 | "text": "",
150 | "title": "dir Header dir_chapter_exclude_all Aex"
151 | },
152 | {
153 | "location": "dir/dir_chapter_exclude_all/#dir-header-dir_chapter_exclude_all-aaex",
154 | "text": "dir text dir_chapter_exclude_all AAex",
155 | "title": "dir header dir_chapter_exclude_all AAex"
156 | },
157 | {
158 | "location": "dir/dir_chapter_exclude_all/#dir-header-dir_chapter_exclude_all-bbex",
159 | "text": "dir text dir_chapter_exclude_all BBex",
160 | "title": "dir header dir_chapter_exclude_all BBex"
161 | },
162 | {
163 | "location": "dir/dir_chapter_ignore_heading3/",
164 | "text": "dir single Header dir_chapter_ignore_heading3 Aex dir single header dir_chapter_ignore_heading3 AAex dir single text dir_chapter_ignore_heading3 AAex dir single header dir_chapter_ignore_heading3 CCin dir single text dir_chapter_ignore_heading3 CCin",
165 | "title": "dir_chapter_ignore_heading3"
166 | },
167 | {
168 | "location": "dir/dir_chapter_ignore_heading3/#dir-single-header-dir_chapter_ignore_heading3-aex",
169 | "text": "",
170 | "title": "dir single Header dir_chapter_ignore_heading3 Aex"
171 | },
172 | {
173 | "location": "dir/dir_chapter_ignore_heading3/#dir-single-header-dir_chapter_ignore_heading3-aaex",
174 | "text": "dir single text dir_chapter_ignore_heading3 AAex",
175 | "title": "dir single header dir_chapter_ignore_heading3 AAex"
176 | },
177 | {
178 | "location": "dir/dir_chapter_ignore_heading3/#dir-single-header-dir_chapter_ignore_heading3-ccin",
179 | "text": "dir single text dir_chapter_ignore_heading3 CCin",
180 | "title": "dir single header dir_chapter_ignore_heading3 CCin"
181 | },
182 | {
183 | "location": "toplvl_chapter/file_in_toplvl_chapter/",
184 | "text": "Header file_in_toplvl_chapter text file_in_toplvl_chapter",
185 | "title": "Header file_in_toplvl_chapter"
186 | },
187 | {
188 | "location": "toplvl_chapter/file_in_toplvl_chapter/#header-file_in_toplvl_chapter",
189 | "text": "text file_in_toplvl_chapter",
190 | "title": "Header file_in_toplvl_chapter"
191 | },
192 | {
193 | "location": "toplvl_chapter/sub_chapter/file1_in_sub_chapter/",
194 | "text": "Header file1_in_sub_chapter text file1_in_sub_chapter",
195 | "title": "Header file1_in_sub_chapter"
196 | },
197 | {
198 | "location": "toplvl_chapter/sub_chapter/file1_in_sub_chapter/#header-file1_in_sub_chapter",
199 | "text": "text file1_in_sub_chapter",
200 | "title": "Header file1_in_sub_chapter"
201 | },
202 | {
203 | "location": "toplvl_chapter/sub_chapter/file2_in_sub_chapter/",
204 | "text": "Header file2_in_sub_chapter text file2_in_sub_chapter",
205 | "title": "Header file2_in_sub_chapter"
206 | },
207 | {
208 | "location": "toplvl_chapter/sub_chapter/file2_in_sub_chapter/#header-file2_in_sub_chapter",
209 | "text": "text file2_in_sub_chapter",
210 | "title": "Header file2_in_sub_chapter"
211 | },
212 | {
213 | "location": "toplvl_chapter/sub_chapter/unreferenced_in_sub_chapter/",
214 | "text": "Header unreferenced_in_sub_chapter text unreferenced_in_sub_chapter",
215 | "title": "Header unreferenced_in_sub_chapter"
216 | },
217 | {
218 | "location": "toplvl_chapter/sub_chapter/unreferenced_in_sub_chapter/#header-unreferenced_in_sub_chapter",
219 | "text": "text unreferenced_in_sub_chapter",
220 | "title": "Header unreferenced_in_sub_chapter"
221 | },
222 | {
223 | "location": "tags.html",
224 | "text": "Contents grouped by tag testing Welcome unimportant Welcome",
225 | "title": "Tags"
226 | },
227 | {
228 | "location": "tags.html#contents-grouped-by-tag",
229 | "text": "",
230 | "title": "Contents grouped by tag"
231 | },
232 | {
233 | "location": "tags.html#testing",
234 | "text": "Welcome",
235 | "title": "testing"
236 | },
237 | {
238 | "location": "tags.html#unimportant",
239 | "text": "Welcome",
240 | "title": "unimportant"
241 | }
242 | ]
243 | }
--------------------------------------------------------------------------------
/tests/test_plugin.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | import json
3 | from unittest.mock import patch, mock_open, MagicMock
4 |
5 | import pytest
6 | from mkdocs.config.base import Config
7 | from mkdocs.config.defaults import get_schema
8 |
9 | from .context import ExcludeSearch
10 | from .globals import (
11 | TO_EXCLUDE,
12 | RESOLVED_EXCLUDED_RECORDS,
13 | TO_IGNORE,
14 | RESOLVED_IGNORED_CHAPTERS,
15 | EXCLUDE_UNREFERENCED,
16 | EXCLUDE_TAGS,
17 | INCLUDED_RECORDS,
18 | )
19 |
20 |
21 | @pytest.mark.parametrize(
22 | "exclude,exclude_unreferenced,exclude_tags",
23 | [
24 | (TO_EXCLUDE, EXCLUDE_UNREFERENCED, EXCLUDE_TAGS),
25 | (TO_EXCLUDE, True, True),
26 | ([], True, EXCLUDE_TAGS),
27 | ([], EXCLUDE_UNREFERENCED, True),
28 | ([], True, True),
29 | ],
30 | )
31 | def test_validate_config(exclude, exclude_unreferenced, exclude_tags):
32 | ex = ExcludeSearch()
33 | ex.config = dict(
34 | {
35 | "exclude": exclude,
36 | "exclude_unreferenced": exclude_unreferenced,
37 | "exclude_tags": exclude_tags,
38 | }
39 | )
40 | ex.validate_config(plugins=["search"])
41 |
42 |
43 | def test_validate_config_raises_search_deactivated():
44 | ex = ExcludeSearch()
45 | with pytest.raises(ValueError) as error:
46 | ex.validate_config(plugins=["abc"])
47 | assert (
48 | str(error.value)
49 | == "mkdocs-exclude-search plugin is activated but has no effect as search plugin is deactivated!"
50 | )
51 |
52 |
53 | def test_validate_config_raises_no_exclusion():
54 | ex = ExcludeSearch()
55 | ex.config = dict(
56 | {
57 | "exclude": [],
58 | "exclude_unreferenced": EXCLUDE_UNREFERENCED,
59 | "exclude_tags": EXCLUDE_TAGS,
60 | }
61 | )
62 | with pytest.raises(ValueError) as error:
63 | ex.validate_config(plugins=["search"])
64 | assert (
65 | str(error.value)
66 | == "No excluded search entries selected for mkdocs-exclude-search, "
67 | "the plugin has no effect!"
68 | )
69 |
70 |
71 | def test_validate_config_pops_ignore_is_not_header():
72 | ex = ExcludeSearch()
73 | ex.config = dict(
74 | {
75 | "exclude": TO_EXCLUDE,
76 | "ignore": ["not_a_header.md", "dir/file.md#header"],
77 | "exclude_unreferenced": EXCLUDE_UNREFERENCED,
78 | "exclude_tags": EXCLUDE_TAGS,
79 | }
80 | )
81 | ex.validate_config(plugins=["search"])
82 | assert "not_a_header.md" not in ex.config["ignore"]
83 | assert "dir/file.md#header" in ex.config["ignore"]
84 | assert len(ex.config["ignore"]) == 1
85 |
86 |
87 | def test_resolve_excluded_records():
88 | resolved_excluded_records = ExcludeSearch.resolve_excluded_records(
89 | to_exclude=TO_EXCLUDE
90 | )
91 | assert isinstance(resolved_excluded_records, list)
92 | assert isinstance(resolved_excluded_records[0], tuple)
93 | assert resolved_excluded_records == RESOLVED_EXCLUDED_RECORDS
94 |
95 |
96 | def test_resolve_ignored_chapters():
97 | resolved_ignored_chapters = ExcludeSearch.resolve_ignored_chapters(
98 | to_ignore=TO_IGNORE
99 | )
100 | assert isinstance(resolved_ignored_chapters, list)
101 | assert isinstance(resolved_ignored_chapters[0], tuple)
102 | assert set(resolved_ignored_chapters) == set(RESOLVED_IGNORED_CHAPTERS)
103 |
104 |
105 | def test_is_tag_record():
106 | assert ExcludeSearch.is_tag_record("tags.html")
107 | assert ExcludeSearch.is_tag_record("tags.html#abc")
108 |
109 |
110 | def test_is_root_record():
111 | assert ExcludeSearch.is_root_record("")
112 | assert ExcludeSearch.is_root_record("index.html")
113 |
114 |
115 | def test_is_ignored_record():
116 | assert ExcludeSearch.is_ignored_record(
117 | rec_file_name="all_dir/all_dir_ignore_heading1/",
118 | rec_header_name="alldir-header-all_dir_ignore_heading1-aain",
119 | to_ignore=[
120 | (
121 | "all_dir/all_dir_ignore_heading1.md",
122 | "alldir-header-all_dir_ignore_heading1-aain",
123 | )
124 | ],
125 | )
126 |
127 |
128 | def test_is_not_ignored_record():
129 | # wrong dir specified
130 | assert not ExcludeSearch.is_ignored_record(
131 | rec_file_name="all_dir/all_dir_ignore_heading1/",
132 | rec_header_name="alldir-header-all_dir_ignore_heading1-aain",
133 | to_ignore=[
134 | ("all_dir_ignore_heading1.md", "alldir-header-all_dir_ignore_heading1-aain")
135 | ],
136 | )
137 | # no heading specified
138 | assert not ExcludeSearch.is_ignored_record(
139 | rec_file_name="all_dir/all_dir_ignore_heading1/",
140 | rec_header_name="alldir-header-all_dir_ignore_heading1-aain",
141 | to_ignore=[("all_dir/all_dir_ignore_heading1.md", None)],
142 | )
143 | # different files
144 | assert not ExcludeSearch.is_ignored_record(
145 | rec_file_name="a.md", rec_header_name="b", to_ignore=[("c.md", "b")]
146 | )
147 |
148 |
149 | def test_is_excluded_record_file():
150 | # file
151 | assert ExcludeSearch.is_excluded_record(
152 | rec_file_name="chapter_exclude_all/",
153 | rec_header_name=None,
154 | to_exclude=[("chapter_exclude_all.md", None)],
155 | )
156 | # file with multiple excluded
157 | assert not ExcludeSearch.is_excluded_record(
158 | rec_file_name="chapter_exclude_all/",
159 | rec_header_name=None,
160 | to_exclude=[("chapter_exclude_all.md", "something.md")],
161 | )
162 | # file + header (not specifically excluded)
163 | assert ExcludeSearch.is_excluded_record(
164 | rec_file_name="chapter_exclude_all/",
165 | rec_header_name="header-chapter_exclude_all-aex",
166 | to_exclude=[("chapter_exclude_all.md", None)],
167 | )
168 | # file + header (specifically excluded)
169 | assert ExcludeSearch.is_excluded_record(
170 | rec_file_name="chapter_exclude_all/",
171 | rec_header_name="header-chapter_exclude_all-aex",
172 | to_exclude=[("chapter_exclude_all.md", "header-chapter_exclude_all-aex")],
173 | )
174 | # file in dir
175 | assert ExcludeSearch.is_excluded_record(
176 | rec_file_name="dir/dir_chapter_exclude_all/",
177 | rec_header_name=None,
178 | to_exclude=[("dir/dir_chapter_exclude_all.md", None)],
179 | )
180 |
181 |
182 | def test_is_excluded_record_dir():
183 | # all dir
184 | assert ExcludeSearch.is_excluded_record(
185 | rec_file_name="all_dir/some-chapter/",
186 | rec_header_name=None,
187 | to_exclude=[("all_dir/*", None)],
188 | )
189 | assert ExcludeSearch.is_excluded_record(
190 | rec_file_name="all_dir/some-chapter/",
191 | rec_header_name=None,
192 | to_exclude=[("all_dir/*", None)],
193 | )
194 | # all dir + header
195 | assert ExcludeSearch.is_excluded_record(
196 | rec_file_name="all_dir/some-chapter/",
197 | rec_header_name="all_dir/some-chapter-aex",
198 | to_exclude=[("all_dir/*", None)],
199 | )
200 | # all subdir
201 | assert ExcludeSearch.is_excluded_record(
202 | rec_file_name="all_dir_sub/all_dir_sub2/some-chapter/",
203 | rec_header_name=None,
204 | to_exclude=[("all_dir_sub/all_dir_sub2/*", None)],
205 | )
206 | # all subdir + header
207 | assert ExcludeSearch.is_excluded_record(
208 | rec_file_name="all_dir_sub/all_dir_sub2/some-chapter/",
209 | rec_header_name="alldir-header-all_dir_sub2-aex",
210 | to_exclude=[("all_dir_sub/all_dir_sub2/*", None)],
211 | )
212 |
213 |
214 | def test_is_excluded_record_wildcard():
215 | # file within subdir wildcard
216 | assert ExcludeSearch.is_excluded_record(
217 | rec_file_name="all_dir_sub/all_dir_sub2/all_dir_sub2_1/",
218 | rec_header_name=None,
219 | to_exclude=[("all_dir_sub/*/all_dir_sub2_1.md", None)],
220 | )
221 | # file within multiple subdir wildcard
222 | assert ExcludeSearch.is_excluded_record(
223 | rec_file_name="all_dir_sub/all_dir_sub2/all_dir_sub2_again/all_dir_sub2_1/",
224 | rec_header_name=None,
225 | to_exclude=[("all_dir_sub/*/all_dir_sub2_1.md", None)],
226 | )
227 | # file within multiple subdir wildcard + header
228 | assert ExcludeSearch.is_excluded_record(
229 | rec_file_name="all_dir_sub/all_dir_sub2/all_dir_sub2_again/all_dir_sub2_1/",
230 | rec_header_name="alldir-header-all_dir_sub2-aex",
231 | to_exclude=[
232 | ("all_dir_sub/*/all_dir_sub2_1.md", "alldir-header-all_dir_sub2-aex")
233 | ],
234 | )
235 |
236 |
237 | def test_is_unreferenced_record_unreferenced():
238 | # unreferenced file, not listed in mkdocs.yml nav
239 | assert ExcludeSearch.is_unreferenced_record(
240 | rec_file_name="unreferenced/",
241 | navigation_items=["index/", "chapter_exclude_all/"],
242 | )
243 |
244 | assert not ExcludeSearch.is_unreferenced_record(
245 | rec_file_name="chapter_exclude_all/",
246 | navigation_items=["index/", "chapter_exclude_all/"],
247 | )
248 |
249 |
250 | def test_is_not_excluded_record():
251 | # file in dir without dir specified
252 | assert not ExcludeSearch.is_excluded_record(
253 | rec_file_name="dir/dir_chapter_exclude_all/",
254 | rec_header_name=None,
255 | to_exclude=[("dir_chapter_exclude_all.md", None)],
256 | )
257 | # partial filename matches
258 | assert not ExcludeSearch.is_excluded_record(
259 | rec_file_name="do_not_match_chapter_exclude_all/",
260 | rec_header_name=None,
261 | to_exclude=[("chapter_exclude_all.md", None)],
262 | )
263 | # partial path match
264 | assert not ExcludeSearch.is_excluded_record(
265 | rec_file_name="all_dir_sub/",
266 | rec_header_name=None,
267 | to_exclude=[("all_dir_sub/*/all_dir_sub2_1.md", None)],
268 | )
269 |
270 |
271 | def test_select_records():
272 | _location_ = Path(__file__).resolve().parent
273 | with open(_location_.joinpath("mock_data/mock_search_index.json"), "r") as f:
274 | mock_search_index = json.load(f)
275 |
276 | included_records = ExcludeSearch().select_included_records(
277 | search_index=mock_search_index,
278 | to_exclude=RESOLVED_EXCLUDED_RECORDS,
279 | to_ignore=RESOLVED_IGNORED_CHAPTERS,
280 | navigation_items=[],
281 | exclude_tags=EXCLUDE_TAGS,
282 | )
283 | assert isinstance(included_records, list)
284 | assert isinstance(included_records[0], dict)
285 | assert included_records == INCLUDED_RECORDS
286 |
287 |
288 | def test_select_records_unreferenced():
289 | _location_ = Path(__file__).resolve().parent
290 | with open(_location_.joinpath("mock_data/mock_search_index.json"), "r") as f:
291 | mock_search_index = json.load(f)
292 |
293 | included_records = ExcludeSearch().select_included_records(
294 | search_index=mock_search_index,
295 | to_exclude=[],
296 | to_ignore=[],
297 | navigation_items=["chapter_exclude_all/"],
298 | exclude_unreferenced=True,
299 | )
300 | assert isinstance(included_records, list)
301 | assert isinstance(included_records[0], dict)
302 | assert included_records != INCLUDED_RECORDS
303 | assert len(included_records) == 10
304 |
305 |
306 | def test_select_records_exclude_tags():
307 | _location_ = Path(__file__).resolve().parent
308 | with open(_location_.joinpath("mock_data/mock_search_index.json"), "r") as f:
309 | mock_search_index = json.load(f)
310 |
311 | included_records = ExcludeSearch().select_included_records(
312 | search_index=mock_search_index,
313 | to_exclude=RESOLVED_EXCLUDED_RECORDS,
314 | to_ignore=RESOLVED_IGNORED_CHAPTERS,
315 | navigation_items=[],
316 | exclude_tags=True,
317 | )
318 | assert len(included_records) != len(INCLUDED_RECORDS)
319 | for rec in included_records:
320 | assert not "tags.html" in rec["location"]
321 |
322 |
323 | def test_on_post_build():
324 | _location_ = Path(__file__).resolve().parent
325 | with open(_location_.joinpath("mock_data/mock_search_index.json"), "r") as f:
326 | mock_search_index = json.load(f)
327 |
328 | mkdocs_config_fp = str(_location_.parent / "mkdocs.yml")
329 | with open(mkdocs_config_fp, "rb") as fd:
330 | cfg = Config(schema=get_schema(), config_file_path=mkdocs_config_fp)
331 | # load the config file
332 | cfg.load_file(fd)
333 |
334 | p1 = patch("builtins.open", mock_open())
335 | p2 = patch("json.load", side_effect=[MagicMock(mock_search_index)])
336 | p3 = patch.object(
337 | ExcludeSearch, "select_included_records", return_value=["some_included_record"]
338 | )
339 | with p1:
340 | with p2:
341 | with p3 as mock_p3:
342 | exs = ExcludeSearch()
343 | exs.config["exclude"] = ["dir/dir_chapter_exclude_all.md"]
344 | # defaults
345 | (
346 | exs.config["ignore"],
347 | exs.config["exclude_unreferenced"],
348 | exs.config["exclude_tags"],
349 | ) = ([], False, False)
350 |
351 | out_config = exs.on_post_build(config=cfg)
352 |
353 | assert isinstance(out_config, Config)
354 | assert mock_p3.call_count == 1
355 |
356 |
357 | def test_on_post_build_no_nav():
358 | _location_ = Path(__file__).resolve().parent
359 | with open(_location_.joinpath("mock_data/mock_search_index.json"), "r") as f:
360 | mock_search_index = json.load(f)
361 |
362 | mkdocs_config_fp = str(_location_.parent / "mkdocs.yml")
363 | with open(mkdocs_config_fp, "rb") as fd:
364 | cfg = Config(schema=get_schema(), config_file_path=mkdocs_config_fp)
365 | # load the config file
366 | cfg.load_file(fd)
367 | cfg.__dict__["data"]["nav"] = None
368 |
369 | p1 = patch("builtins.open", mock_open())
370 | p2 = patch("json.load", side_effect=[MagicMock(mock_search_index)])
371 | p3 = patch.object(
372 | ExcludeSearch, "select_included_records", return_value=["some_included_record"]
373 | )
374 | with p1:
375 | with p2:
376 | with p3 as mock_p3:
377 | exs = ExcludeSearch()
378 | exs.config["exclude"] = ["dir/dir_chapter_exclude_all.md"]
379 | # defaults
380 | (
381 | exs.config["ignore"],
382 | exs.config["exclude_unreferenced"],
383 | exs.config["exclude_tags"],
384 | ) = ([], False, False)
385 |
386 | out_config = exs.on_post_build(config=cfg)
387 |
388 | assert isinstance(out_config, Config)
389 | assert mock_p3.call_count == 1
390 |
--------------------------------------------------------------------------------
/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | from .globals import NAVIGATION
2 | from .context import iterate_all_values, explode_navigation
3 |
4 |
5 | def test_iterate_all_values():
6 | nav = {"a": ["aa", {"b": ["cc", {"d": ["ee", "ff", {"d": ["gg", "hh"]}]}]}]}
7 |
8 | nav_paths = list(iterate_all_values(nested_dict=nav))
9 | assert isinstance(nav_paths, list)
10 | assert nav_paths == ["aa", "cc", "ee", "ff", "gg", "hh"]
11 |
12 |
13 | def test_explode_navigation():
14 | nav_paths = explode_navigation(navigation=NAVIGATION)
15 | assert isinstance(nav_paths, list)
16 | assert nav_paths == [
17 | "index/",
18 | "without_nav_name/",
19 | "chapter_exclude_all/",
20 | "toplvl_chapter/file_in_toplvl_chapter/",
21 | "toplvl_chapter/sub_chapter/file1_in_sub_chapter/",
22 | "toplvl_chapter/sub_chapter/file2_in_sub_chapter/",
23 | ]
24 |
--------------------------------------------------------------------------------