├── 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 |
--------------------------------------------------------------------------------