├── .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 | PyPI - Downloads 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 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | coverage 17 | coverage 18 | 95% 19 | 95% 20 | 21 | 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 | --------------------------------------------------------------------------------