├── mkdocs_unused_files ├── __init__.py └── plugin.py ├── requirements.txt ├── LICENSE ├── setup.py ├── .gitignore └── README.md /mkdocs_unused_files/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs>=1.0.4 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Lars Wilhelmer 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. -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='mkdocs-unused-files', 5 | version='0.2.0', 6 | description='An MkDocs plugin to find unused (orphaned) files in your project.', 7 | long_description='', 8 | keywords='mkdocs', 9 | url='https://github.com/wilhelmer/mkdocs-unused-files.git', 10 | author='Lars Wilhelmer', 11 | author_email='lars@wilhelmer.de', 12 | license='MIT', 13 | python_requires='>=2.7', 14 | install_requires=[ 15 | 'mkdocs>=1.0.4', 16 | 'beautifulsoup4>=4.12.0' 17 | ], 18 | classifiers=[ 19 | 'Development Status :: 4 - Beta', 20 | 'Intended Audience :: Developers', 21 | 'Intended Audience :: Information Technology', 22 | 'License :: OSI Approved :: MIT License', 23 | 'Programming Language :: Python', 24 | 'Programming Language :: Python :: 3 :: Only', 25 | 'Programming Language :: Python :: 3.4', 26 | 'Programming Language :: Python :: 3.5', 27 | 'Programming Language :: Python :: 3.6', 28 | 'Programming Language :: Python :: 3.7' 29 | ], 30 | packages=find_packages(), 31 | entry_points={ 32 | 'mkdocs.plugins': [ 33 | 'unused_files = mkdocs_unused_files.plugin:UnusedFilesPlugin' 34 | ] 35 | } 36 | ) 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .DS_Store 3 | *.whl 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # celery beat schedule file 89 | celerybeat-schedule 90 | 91 | # SageMath parsed files 92 | *.sage.py 93 | 94 | # Environments 95 | .env 96 | .venv 97 | env/ 98 | venv/ 99 | ENV/ 100 | env.bak/ 101 | venv.bak/ 102 | 103 | # Spyder project settings 104 | .spyderproject 105 | .spyproject 106 | 107 | # Rope project settings 108 | .ropeproject 109 | 110 | # mkdocs documentation 111 | /site 112 | 113 | # mypy 114 | .mypy_cache/ 115 | .dmypy.json 116 | dmypy.json 117 | 118 | # Pyre type checker 119 | .pyre/ 120 | 121 | # DOS batch files 122 | *.cmd 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mkdocs-unused-files 2 | 3 | An MkDocs plugin to find unused (orphaned) files in your project. 4 | 5 | This is useful, e.g., if your project contains a lot of image files and you lost track which images are still in use. 6 | 7 | A file is considered "used" when it is referenced in at least one Markdown file of your project, either as an image or as a hyperlink reference. 8 | 9 | > :bulb: The plugin only searches in the page content, not in the rendered template (footer, header, navigation), for better performance. Therefore, the plugin may incorrectly report template files as unused. 10 | 11 | ## Installation 12 | 13 | Install the package with pip: 14 | 15 | ``` 16 | pip install mkdocs-unused-files 17 | ``` 18 | 19 | Enable the plugin in your mkdocs.yml: 20 | 21 | ```yaml 22 | plugins: 23 | - search 24 | - unused_files 25 | ``` 26 | 27 | > **Note:** If you have no `plugins` entry in your config file yet, you'll likely also want to add the `search` plugin. MkDocs enables it by default if there is no `plugins` entry set, but now you have to enable it explicitly. 28 | 29 | ## How It Works 30 | 31 | When building your MkDocs project, the plugin searches for unused files of certain types in a specified directory. If unused files are found, the plugin displays an info message, listing the files. 32 | 33 | Search is done as follows: 34 | 35 | 1. Get the list of files in the specified directory, including all subdirectories. 36 | 2. Collect all image and hyperlink references in the HTML output (`` and ``). 37 | 3. Remove all referenced files from the list of files. 38 | 4. Once all pages have been processed, display an MkDocs info message listing all non-referenced files: 39 | 40 | ``` 41 | INFO - The following files exist in the docs directory, but may be unused: 42 | - images/image1.svg 43 | - images/subdir/image2.png 44 | ``` 45 | 46 | ## Options 47 | 48 | * `dir`: The directory where to search for unused files. Path is relative to `docs_dir`. The plugin recurses all subdirectories. For example, if you specify `images` and `docs_dir` is set to `docs`, the plugin searches in `docs/images`, including all subdirectories. Defaults to `docs_dir`. 49 | * `file_types`: List of file types the plugin should process (whitelist). If empty or omitted, all files **except Markdown (md)** files will be processed. Defaults to `[]`. 50 | * `excluded_files`: List of files the plugin should **not** process (blacklist). Works in combination with `file_types`. Entries apply to `dir` and all its subdirectories. Do not specify paths here, only file names. You can use wildcards. For example, `foo-*.jpg` excludes all JPG files prefixed with `foo-` in all directories. Defaults to `[]`. 51 | * `strict`: Elevates the log level to `warning`. This allows you to use MkDocs' strict flag (`mkdocs build -s`) to abort a build if unused files exist. Defaults to `false`. 52 | * `enabled`: This option specifies whether the plugin is enabled when building your project. If you want to switch the plugin off, e.g. for local builds, use an [environment variable](https://www.mkdocs.org/user-guide/configuration/#environment-variables). Defaults to `true`. 53 | 54 | ### Example 55 | 56 | ```yml 57 | plugins: 58 | - unused_files: 59 | dir: images 60 | file_types: 61 | - png 62 | - jpg 63 | - svg 64 | excluded_files: 65 | - favicon.png 66 | - foo-*.jpg 67 | strict: true 68 | enabled: !ENV [CI, false] 69 | ``` 70 | -------------------------------------------------------------------------------- /mkdocs_unused_files/plugin.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import urllib.parse 4 | from fnmatch import fnmatch 5 | from mkdocs.config import config_options 6 | from mkdocs.plugins import BasePlugin 7 | from bs4 import BeautifulSoup 8 | 9 | log = logging.getLogger('mkdocs') 10 | 11 | class UnusedFilesPlugin(BasePlugin): 12 | 13 | file_list = [] 14 | 15 | config_scheme = ( 16 | ('dir', config_options.Type(str, default='')), 17 | ('file_types',config_options.Type((str, list), default=[])), 18 | ('excluded_files', config_options.Type((str, list), default=[])), 19 | ('strict', config_options.Type(bool, default=False)), 20 | ('enabled', config_options.Type(bool, default=True)), 21 | ) 22 | 23 | def _matches_type(self, str): 24 | types = self.config['file_types'] 25 | return not types or (str and str.endswith(tuple(types))) 26 | 27 | def _rewrite_ref(self, ref, page_uri): 28 | ref = urllib.parse.unquote(ref) 29 | # Add the path of the page containing the reference 30 | # When use_directory_urls is set to true, "../" may be added to some refs 31 | # normpath() works around that and also ensures Windows compatibility 32 | ref = os.path.normpath(os.path.join(os.path.dirname(page_uri), ref)) 33 | return ref 34 | 35 | def on_startup(self, *, command, dirty): 36 | if not self.config['enabled']: 37 | return 38 | # Disable plugin when the documentation is served, i.e., "mkdocs serve" is used 39 | if command == "serve": 40 | self.config['enabled'] = False 41 | log.info("Unused-files plugin disabled while MkDocs is running in 'serve' mode.") 42 | 43 | def on_files(self, files, config): 44 | dir = os.path.join(config.docs_dir, self.config['dir']) 45 | # Get all files in directory 46 | for path, _, files in os.walk(dir): 47 | for file in files: 48 | # Add all files with the given types to file_list 49 | # If no types were given, add all files except Markdown files 50 | if not file.endswith("md") and self._matches_type(file): 51 | # Create entry from relative path between full path and docs_dir + filename 52 | # When path and docs_dir are identical, relpath returns ".". We use normpath() to resolve that 53 | entry = os.path.normpath(os.path.join(os.path.relpath(path, config.docs_dir), file)) 54 | # Check whether file is excluded 55 | is_excluded = False 56 | for excluded_file in self.config['excluded_files']: 57 | if fnmatch(file, excluded_file): 58 | is_excluded = True 59 | if is_excluded: 60 | continue 61 | self.file_list.append(entry) 62 | 63 | def on_page_content(self, html, page, config, files): 64 | if not self.config['enabled']: 65 | return 66 | soup = BeautifulSoup(html, 'html.parser') 67 | ref_list = [] 68 | # Get all file references in 69 | for a in soup.find_all('a', href=self._matches_type): 70 | ref_list.append(self._rewrite_ref(a['href'], page.file.dest_uri)) 71 | 72 | # Get all file references in 73 | for img in soup.find_all('img', src=self._matches_type): 74 | ref_list.append(self._rewrite_ref(img['src'], page.file.dest_uri)) 75 | 76 | # Remove all referenced files from file list 77 | self.file_list = [i for i in self.file_list if i not in ref_list] 78 | 79 | def on_post_build(self, config): 80 | if not self.config['enabled']: 81 | return 82 | logger = log.info 83 | if self.config['strict']: 84 | logger = log.warning 85 | if self.file_list: 86 | logger('The following files exist in the docs directory, but may be unused:\n - {}'.format('\n - '.join(self.file_list))) 87 | 88 | --------------------------------------------------------------------------------