├── .DS_Store ├── .github ├── .DS_Store ├── FUNDING.yml └── workflows │ ├── codeql-analysis.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── codecov.yml ├── example_project ├── 1.2 ├── docs │ ├── index.md │ ├── openapi.yml │ └── openapi_example.md └── mkdocs.yml ├── pyproject.toml ├── render_swagger.py ├── scripts └── pre_push.sh ├── setup.cfg └── tests ├── __init__.py ├── arbitrary_sample └── openapi.yml ├── samples └── openapi_3.0.yml └── test_plugin.py /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bharel/mkdocs-render-swagger-plugin/0f57755af4c9fa7c2c9cf817ab1dfe3168355996/.DS_Store -------------------------------------------------------------------------------- /.github/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bharel/mkdocs-render-swagger-plugin/0f57755af4c9fa7c2c9cf817ab1dfe3168355996/.github/.DS_Store -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | buy_me_a_coffee: bharel 3 | github: bharel 4 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '21 7 * * 0' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'python' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release mkdocs_render_swagger_plugin 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set up Python 13 | uses: actions/setup-python@v2 14 | with: 15 | python-version: 3.8 16 | - name: Build wheel 17 | run: pip wheel . 18 | - name: Install package locally for verification 19 | run: pip install . 20 | - name: Verify version 21 | run: | 22 | PY_VER=$(python -c "import render_swagger;print(render_swagger.__version__)") 23 | echo Python version - "$PY_VER" 24 | TAG_VER=$(echo ${{ github.event.release.tag_name }}) 25 | echo Tag version "$TAG_VER" 26 | [[ $TAG_VER == $PY_VER ]] 27 | CFG_VER=$(grep version setup.cfg | cut -d '=' -f2 | xargs) 28 | echo Config version "$CFG_VER" 29 | [[ $CFG_VER == $PY_VER ]] 30 | - name: Install twine 31 | run: pip install twine 32 | - uses: AButler/upload-release-assets@v2.0 33 | with: 34 | files: 'mkdocs_render_swagger_plugin-*.whl' 35 | repo-token: ${{ secrets.GITHUB_TOKEN }} 36 | - name: Publish on Test PyPi 37 | run: twine upload -r testpypi -u ${{ secrets.TEST_PYPI_USERNAME }} -p ${{ secrets.TEST_PYPI_PASSWORD }} mkdocs_render_swagger_plugin-*.whl -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test mkdocs-render-swagger-plugin 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up Python 3.8 15 | uses: actions/setup-python@v2 16 | with: 17 | python-version: 3.8 18 | - name: Install flake8 19 | run: pip install flake8 20 | - name: Lint using flake8 21 | run: python -m flake8 render_swagger.py tests 22 | test: 23 | runs-on: ${{ matrix.os }} 24 | strategy: 25 | matrix: 26 | os: [ubuntu-latest, macos-latest, windows-latest] 27 | python-version: [3.8] 28 | steps: 29 | - uses: actions/checkout@v2 30 | - name: Set up Python ${{ matrix.python-version }} 31 | uses: actions/setup-python@v2 32 | with: 33 | python-version: ${{ matrix.python-version }} 34 | - name: Install dependencies 35 | run: | 36 | python -m pip install --upgrade pip 37 | pip install -e ".[dev]" 38 | - name: Test on Python ${{ matrix.python-version }} 39 | run: python -m coverage run --branch -m unittest tests && coverage xml 40 | - name: Upload Coverage to Codecov 41 | uses: codecov/codecov-action@v3 42 | with: 43 | token: ${{ secrets.CODECOV_TOKEN }} -------------------------------------------------------------------------------- /.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 | .idea 131 | example_project/site 132 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # Change Log 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## [Unreleased] - yyyy-mm-dd 9 | 10 | ## [0.1.2] - 2024-05-26 11 | 12 | ### Fixed 13 | 14 | - [https://github.com/bharel/mkdocs-render-swagger-plugin/pull/30] Supports MKDocs 1.5.2+ 15 | 16 | ## [0.1.1] - 2023-10-07 17 | 18 | ### Added 19 | - Support for arbitrary file locations 20 | 21 | ### Changed 22 | - JS and CSS should now be referenced in the plugin configuration. Backwards compatible. 23 | 24 | ### Fixed 25 | - Multiple files should now be included correctly 26 | 27 | ## [0.1.0] - 2023-10-05 28 | 29 | ### Added 30 | 31 | - [https://github.com/bharel/mkdocs-render-swagger-plugin/pull/24] Now supporting OpenAPI 3.0 32 | 33 | ## [0.0.4] - 2022-08-04 34 | 35 | ### Fixed 36 | 37 | - Removed `ui` local variable as it conflicts with existing JS. 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Bar Harel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mkdocs-render-swagger-plugin 2 | This is the [Mkdocs](https://www.mkdocs.org) plugin for rendering swagger & openapi schemas using [Swagger UI](https://swagger.io/tools/swagger-ui/). It is written in Python. 3 | 4 | [![GitHub branch checks state](https://img.shields.io/github/checks-status/bharel/mkdocs-render-swagger-plugin/main)](https://github.com/bharel/mkdocs-render-swagger-plugin/actions) 5 | [![PyPI](https://img.shields.io/pypi/v/mkdocs-render-swagger-plugin)](https://pypi.org/project/mkdocs-render-swagger-plugin/) 6 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/mkdocs-render-swagger-plugin)](https://pypi.org/project/mkdocs-render-swagger-plugin/) 7 | [![codecov](https://codecov.io/gh/bharel/mkdocs-render-swagger-plugin/branch/main/graph/badge.svg?token=GXV70TL21V)](https://codecov.io/gh/bharel/mkdocs-render-swagger-plugin) 8 | 9 | ## Usage 10 | Install the plugin using `pip install mkdocs-render-swagger-plugin`. 11 | 12 | Add the following lines to your mkdocs.yml: 13 | 14 | plugins: 15 | - render_swagger 16 | 17 | ## Example 18 | 19 | Here's an [example](https://docs.scarf.sh/api-v2/) (courtesy of Scarf) of how the plugin renders swagger. 20 | 21 | ### Referencing a local json 22 | 23 | Place an OpenAPI json file in the same folder as the `.md` file. 24 | 25 | Enter `!!swagger FILENAME!!` at the appropriate location inside the markdown file. 26 | 27 | If you wish to reference any files on the filesytem (security risk), make sure 28 | you enable `allow_arbitrary_locations` in the config (mkdocs.yml) like so: 29 | 30 | plugins: 31 | - render_swagger: 32 | allow_arbitrary_locations : true 33 | 34 | ### Referencing an external json 35 | 36 | You may reference an external OpenAPI json using the following syntax: `!!swagger-http URL!!`. 37 | 38 | ## Explicit declaration of the Swagger JS library 39 | 40 | You can explicitly specify the swagger-ui css and js dependencies if you wish to not use the unpkg CDN. 41 | 42 | To specify this use `javascript` and `css` in your mkdocs.yaml: 43 | ```yaml 44 | plugins: 45 | - render_swagger: 46 | javascript: assets/js/swagger-ui-bundle.js 47 | css: assets/css/swagger-ui.css 48 | ``` 49 | 50 | ## Contributing & Developing Locally 51 | 52 | After downloading and extracting the `.tar.gz`, install this package locally using `pip` and the `--editable` flag: 53 | 54 | ```bash 55 | pip install --editable ".[dev]" 56 | ``` 57 | 58 | You'll then have the `render-swagger` package available to use in Mkdocs and `pip` will point the dependency to this folder. You are then able to run the docs using `mkdocs serve`. Make sure you restart the process between code changes as the plugin is loaded on startup. 59 | 60 | ## MkDocs plugins and Swagger api 61 | 62 | The Render Swagger MkDocs plugin uses a set of extensions and plugin APIs that MkDocs and Swagger UI supports. 63 | You can find more info about MkDocs plugins and Swagger UI on the official website of [MkDocs](https://www.mkdocs.org/user-guide/plugins/) and [SwaggerUI](https://github.com/swagger-api/swagger-ui/blob/master/docs/customization/plugin-api.md). 64 | 65 | The input OpenAPI files processed by the plugin should conform to the [OpenAPI specification](https://swagger.io/specification/). It is generated by a few projects such as [pydantic](https://pydantic-docs.helpmanual.io/), [FastAPI](https://fastapi.tiangolo.com/) and others. 66 | 67 |
68 | 69 | Disclaimer: This plugin is unofficial, and is not sponsored, owned or endorsed by mkdocs, swagger, or any other 3rd party.
70 | Credits to @aviramha for starting this project. 71 |
72 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bharel/mkdocs-render-swagger-plugin/0f57755af4c9fa7c2c9cf817ab1dfe3168355996/codecov.yml -------------------------------------------------------------------------------- /example_project/1.2: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /example_project/docs/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to MkDocs 2 | 3 | This is a simple test project. 4 | 5 | ## Commands 6 | 7 | * `mkdocs new [dir-name]` - Create a new project. 8 | * `mkdocs serve` - Start the live-reloading docs server. 9 | * `mkdocs build` - Build the documentation site. 10 | * `mkdocs -h` - Print help message and exit. 11 | 12 | ## Project layout 13 | 14 | mkdocs.yml # The configuration file. 15 | docs/ 16 | index.md # The documentation homepage. 17 | openapi_example.md # Plugin example 18 | -------------------------------------------------------------------------------- /example_project/docs/openapi.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: Example API 4 | version: 1.0.0 5 | servers: 6 | - url: http://localhost:8000 7 | paths: 8 | /users: 9 | get: 10 | summary: Get a list of users 11 | responses: 12 | '200': 13 | description: A list of users 14 | content: 15 | application/json: 16 | schema: 17 | type: array 18 | items: 19 | type: object 20 | properties: 21 | id: 22 | type: integer 23 | name: 24 | type: string 25 | post: 26 | summary: Create a new user 27 | requestBody: 28 | required: true 29 | content: 30 | application/json: 31 | schema: 32 | type: object 33 | properties: 34 | name: 35 | type: string 36 | responses: 37 | '201': 38 | description: The created user 39 | content: 40 | application/json: 41 | schema: 42 | type: object 43 | properties: 44 | id: 45 | type: integer 46 | name: 47 | type: string 48 | 49 | -------------------------------------------------------------------------------- /example_project/docs/openapi_example.md: -------------------------------------------------------------------------------- 1 | # OpenAPI example 2 | 3 | This is a rendered OpenAPI file: 4 | 5 | !!swagger openapi.yml!! -------------------------------------------------------------------------------- /example_project/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: My Docs 2 | 3 | plugins: 4 | - render_swagger: 5 | allow_arbitrary_locations : true -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /render_swagger.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import string 4 | import urllib.parse 5 | from pathlib import Path 6 | from xml.sax.saxutils import escape 7 | 8 | import mkdocs.plugins 9 | from mkdocs.config import config_options 10 | from mkdocs.config.base import Config as MkDocsConfig 11 | from mkdocs.structure.files import File 12 | 13 | __version__ = "0.1.2" 14 | 15 | USAGE_MSG = ( 16 | "Usage: '!!swagger !!' or '!!swagger-http !!'. " 17 | "File must either exist locally and be placed next to the .md that " 18 | "contains the swagger statement, or be an http(s) URL.") 19 | 20 | TEMPLATE = string.Template(""" 21 | 22 | 23 |
24 |
25 | 26 | 32 | 33 | """) 34 | 35 | 36 | def generate_id(): 37 | generate_id.counter += 1 38 | return f"swagger-ui-{generate_id.counter}" 39 | 40 | 41 | generate_id.counter = 0 42 | 43 | 44 | ERROR_TEMPLATE = string.Template("!! SWAGGER ERROR: $error !!") 45 | 46 | # Used for JS. Runs locally on end-user. 47 | # RFI / LFI possible. Use with caution. 48 | TOKEN = re.compile(r"!!swagger(?: (?P[^\s<>&:!]+))?!!") 49 | 50 | # HTTP(S) variant 51 | TOKEN_HTTP = re.compile(r"!!swagger-http(?: (?Phttps?://[^\s!]+))?!!") 52 | 53 | DEFAULT_SWAGGER_LIB = { 54 | 'css': "https://unpkg.com/swagger-ui-dist@5/swagger-ui.css", 55 | 'js': "https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js" 56 | } 57 | 58 | 59 | def swagger_lib(config) -> dict: 60 | """ 61 | Provides the actual swagger library used 62 | """ 63 | lib_swagger = DEFAULT_SWAGGER_LIB.copy() 64 | extra_javascript = config.get('extra_javascript', []) 65 | extra_css = config.get('extra_css', []) 66 | for lib in extra_javascript: 67 | lib = str(lib) # Can be an instance of ExtraScriptValue 68 | if os.path.basename( 69 | urllib.parse.urlparse(lib).path) == 'swagger-ui-bundle.js': 70 | import warnings 71 | warnings.warn( 72 | "Please use the javascript configuration option for " 73 | "mkdocs-render-swagger-plugin instead of extra_javascript.", 74 | FutureWarning) 75 | lib_swagger['js'] = lib 76 | break 77 | 78 | for css in extra_css: 79 | if os.path.basename( 80 | urllib.parse.urlparse(css).path) == 'swagger-ui.css': 81 | warnings.warn( 82 | "Please use the css configuration option for " 83 | "mkdocs-render-swagger-plugin instead of extra_css.", 84 | FutureWarning) 85 | lib_swagger['css'] = css 86 | break 87 | return lib_swagger 88 | 89 | 90 | class SwaggerConfig(mkdocs.config.base.Config): 91 | javascript = config_options.Type(str, default="") 92 | css = config_options.Type(str, default="") 93 | allow_arbitrary_locations = config_options.Type(bool, default=False) 94 | 95 | 96 | class SwaggerPlugin(mkdocs.plugins.BasePlugin[SwaggerConfig]): 97 | def on_config(self, config: MkDocsConfig, **kwargs): 98 | lib = swagger_lib(config) 99 | self.config.javascript = self.config.javascript or lib['js'] 100 | self.config.css = self.config.css or lib['css'] 101 | return config 102 | 103 | def on_page_markdown(self, markdown, page, config, files): 104 | is_http = False 105 | match = TOKEN.search(markdown) 106 | 107 | if match is None: 108 | match = TOKEN_HTTP.search(markdown) 109 | is_http = True 110 | 111 | if match is None: 112 | return markdown 113 | 114 | pre_token = markdown[:match.start()] 115 | post_token = markdown[match.end():] 116 | 117 | def _error(message): 118 | return ( 119 | pre_token + escape(ERROR_TEMPLATE.substitute(error=message)) + 120 | post_token) 121 | 122 | path = match.group("path") 123 | 124 | if path is None: 125 | return _error(USAGE_MSG) 126 | 127 | if is_http: 128 | url = path 129 | else: 130 | if "/" in path or "\\" in path: 131 | if not self.config.allow_arbitrary_locations: 132 | return _error( 133 | "Arbitrary locations are not allowed due to RFI/LFI " 134 | "security risks. " 135 | "Please enable the 'allow_arbitrary_locations' " 136 | "configuration option to allow this.") 137 | try: 138 | api_file = Path(page.file.abs_src_path).parent / path 139 | except ValueError as exc: # pragma: no cover 140 | return _error(f"Invalid path. {exc.args[0]}") 141 | 142 | if not api_file.exists(): 143 | return _error(f"File {path} not found.") 144 | 145 | src_dir = api_file.parent 146 | dest_dir = Path(page.file.abs_dest_path).parent 147 | 148 | new_file = File(api_file.name, src_dir, dest_dir, False) 149 | url = Path(new_file.abs_dest_path).name 150 | 151 | if any(f.abs_src_path != new_file.abs_src_path and 152 | f.dest_uri == new_file.dest_uri for f in files): 153 | return _error("Cannot use 2 different swagger files with " 154 | "same filename in same page.") 155 | 156 | files.append(new_file) 157 | 158 | markdown = pre_token + TEMPLATE.substitute( 159 | path=url, swagger_lib_js=self.config.javascript, 160 | swagger_lib_css=self.config.css, id=generate_id() 161 | ) + post_token 162 | 163 | # If multiple swaggers exist. 164 | return self.on_page_markdown(markdown, page, config, files) 165 | -------------------------------------------------------------------------------- /scripts/pre_push.sh: -------------------------------------------------------------------------------- 1 | python -m flake8 render_swagger.py tests && python -m coverage run --branch -m unittest tests && python -m coverage html && open -a "Google Chrome" htmlcov/index.html -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = mkdocs-render-swagger-plugin 3 | version = 0.1.2 4 | author = Bar Harel 5 | author_email = bzvi7919@gmail.com 6 | description = MKDocs plugin for rendering swagger & openapi files. 7 | long_description = file: README.md 8 | long_description_content_type = text/markdown 9 | url = https://github.com/bharel/mkdocs-render-swagger-plugin 10 | classifiers = 11 | Programming Language :: Python :: 3 12 | License :: OSI Approved :: MIT License 13 | Operating System :: OS Independent 14 | 15 | [options] 16 | py_modules = render_swagger 17 | python_requires = >=3.8 18 | install_requires = 19 | mkdocs >= 1.4 20 | 21 | [options.extras_require] 22 | dev = 23 | flake8 24 | mypy 25 | pyyaml 26 | coverage 27 | 28 | [options.entry_points] 29 | mkdocs.plugins = 30 | render_swagger = render_swagger:SwaggerPlugin 31 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from tests.test_plugin import ( 2 | FullRenderTestCase, SwaggerPluginTestCase, SwaggerMiscTestCase) 3 | 4 | __all__ = ['FullRenderTestCase', 'SwaggerPluginTestCase', 5 | "SwaggerMiscTestCase"] 6 | -------------------------------------------------------------------------------- /tests/arbitrary_sample/openapi.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 # For arbitrary locations 2 | info: 3 | title: Example API 4 | version: 1.0.0 5 | servers: 6 | - url: http://localhost:8000 7 | paths: 8 | /users: 9 | get: 10 | summary: Get a list of users 11 | responses: 12 | '200': 13 | description: A list of users 14 | content: 15 | application/json: 16 | schema: 17 | type: array 18 | items: 19 | type: object 20 | properties: 21 | id: 22 | type: integer 23 | name: 24 | type: string -------------------------------------------------------------------------------- /tests/samples/openapi_3.0.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: Example API 4 | version: 1.0.0 5 | servers: 6 | - url: http://localhost:8000 7 | paths: 8 | /users: 9 | get: 10 | summary: Get a list of users 11 | responses: 12 | '200': 13 | description: A list of users 14 | content: 15 | application/json: 16 | schema: 17 | type: array 18 | items: 19 | type: object 20 | properties: 21 | id: 22 | type: integer 23 | name: 24 | type: string -------------------------------------------------------------------------------- /tests/test_plugin.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import tempfile 4 | import unittest 5 | from yaml import dump 6 | import pathlib 7 | import subprocess 8 | import render_swagger 9 | import unittest.mock 10 | from mkdocs.structure.pages import Page 11 | from typing import Optional 12 | from mkdocs.structure.files import File 13 | 14 | DEFAULT_CONFIG = { 15 | "site_name": 'My Site', 16 | "plugins": [ 17 | { 18 | "render_swagger": {} 19 | }, 20 | ], 21 | } 22 | 23 | 24 | def render_markdown(markdown: str, config_options: Optional[dict] = None 25 | ) -> str: 26 | """Render a Markdown string using Mkdocs. 27 | 28 | Args: 29 | markdown: The Markdown to render. 30 | config_options: A dictionary of Mkdocs config options. 31 | 32 | Returns: 33 | The rendered HTML. 34 | """ 35 | # Create a temporary directory for the Mkdocs site 36 | with tempfile.TemporaryDirectory() as temp_dir: 37 | 38 | temp_dir = pathlib.Path(temp_dir) 39 | 40 | # Create a mock Mkdocs docs directory 41 | docs = temp_dir / "docs" 42 | docs.mkdir(exist_ok=True) 43 | 44 | # Create a mock Mkdocs config 45 | config = DEFAULT_CONFIG.copy() 46 | if config_options: 47 | config["plugins"][0]["render_swagger"].update(config_options) 48 | 49 | with (temp_dir / "mkdocs.yml").open("w") as f: 50 | dump(config, f) 51 | 52 | # Create a mock Markdown file 53 | with (docs / "index.md").open("w") as f: 54 | f.write(markdown) 55 | 56 | # Copy all samples to the mock docs directory 57 | samples = pathlib.Path(__file__).parent / "samples" 58 | shutil.copytree(samples, docs, dirs_exist_ok=True) 59 | 60 | # Run Mkdocs 61 | process = subprocess.run(["mkdocs", "build"], cwd=temp_dir, 62 | capture_output=True) 63 | assert process.returncode == 0, process.stderr.decode() 64 | 65 | # Read the rendered HTML 66 | return (temp_dir / "site" / "index.html").read_text() 67 | 68 | 69 | class FullRenderTestCase(unittest.TestCase): 70 | @classmethod 71 | def setUpClass(cls): 72 | cwd = pathlib.Path(__file__).parent 73 | cls.old_cwd = pathlib.Path.cwd() 74 | os.chdir(cwd) 75 | 76 | @classmethod 77 | def tearDownClass(cls): 78 | os.chdir(cls.old_cwd) 79 | 80 | def test_sanity(self): 81 | result = render_markdown(r"!!swagger openapi_3.0.yml!!") 82 | expected = """

83 |
84 |
85 | 86 | """.strip() # noqa: E501 92 | self.assertIn(expected, result) 93 | 94 | def test_sanity_http(self): 95 | result = render_markdown( 96 | r"!!swagger-http https://petstore.swagger.io/v2/swagger.json!!") 97 | expected = """

98 |
99 |
100 | 101 | """.strip() # noqa: E501 107 | self.assertIn(expected, result) 108 | 109 | def test_sanity_http_with_config(self): 110 | result = render_markdown( 111 | r"!!swagger-http https://petstore.swagger.io/v2/swagger.json!!", 112 | config_options={ 113 | "javascript": 114 | "https://cdn.jsdelivr.net/npm/swagger-ui-dist/swagger-ui-standalone-preset.js" # noqa: E501 115 | } 116 | ) 117 | 118 | expected = """

119 |
120 |
121 | 122 | """.strip() # noqa: E501 128 | self.assertIn(expected, result) 129 | 130 | 131 | class SwaggerPluginTestCase(unittest.TestCase): 132 | 133 | @classmethod 134 | def setUpClass(cls): 135 | cwd = pathlib.Path(__file__).parent 136 | cls.old_cwd = pathlib.Path.cwd() 137 | os.chdir(cwd) 138 | 139 | @classmethod 140 | def tearDownClass(cls): 141 | os.chdir(cls.old_cwd) 142 | 143 | def setUp(self): 144 | self.plugin = render_swagger.SwaggerPlugin() 145 | self.config = render_swagger.SwaggerConfig() 146 | self.page = Page("index.md", File("index.md", "samples", 147 | "samples", False), 148 | {}) 149 | self._id_patcher = unittest.mock.patch( 150 | 'render_swagger.generate_id', return_value="swagger-ui") 151 | self.generate_id = self._id_patcher.start() 152 | 153 | def tearDown(self): 154 | self._id_patcher.stop() 155 | 156 | def setConfig(self, config=None): 157 | config = config or {} 158 | self.plugin.load_config(options=config) 159 | self.plugin.on_config({}) 160 | 161 | def test_sanity(self): 162 | self.setConfig({}) 163 | files = [] 164 | result = self.plugin.on_page_markdown( 165 | r"!!swagger openapi_3.0.yml!!", self.page, DEFAULT_CONFIG, files) 166 | expected = render_swagger.TEMPLATE.substitute( 167 | path="openapi_3.0.yml", 168 | swagger_lib_css=render_swagger.DEFAULT_SWAGGER_LIB['css'], 169 | swagger_lib_js=render_swagger.DEFAULT_SWAGGER_LIB['js'], 170 | id="swagger-ui") 171 | self.assertEqual(expected.strip(), result.strip()) 172 | 173 | def test_sanity_http(self): 174 | self.setConfig({}) 175 | result = self.plugin.on_page_markdown( 176 | r"!!swagger-http https://petstore.swagger.io/v2/swagger.json!!", 177 | self.page, DEFAULT_CONFIG, []) 178 | expected = render_swagger.TEMPLATE.substitute( 179 | path="https://petstore.swagger.io/v2/swagger.json", 180 | swagger_lib_css=render_swagger.DEFAULT_SWAGGER_LIB['css'], 181 | swagger_lib_js=render_swagger.DEFAULT_SWAGGER_LIB['js'], 182 | id="swagger-ui") 183 | self.assertEqual(expected.strip(), result.strip()) 184 | 185 | def test_javascript_config(self): 186 | self.setConfig({ 187 | "javascript": "https://cdn.jsdelivr.net/npm/swagger-ui-dist/swagger-ui-standalone-preset.js" # noqa: E501 188 | }) 189 | result = self.plugin.on_page_markdown( 190 | r"!!swagger-http https://petstore.swagger.io/v2/swagger.json!!", 191 | self.page, DEFAULT_CONFIG, []) 192 | expected = render_swagger.TEMPLATE.substitute( 193 | path="https://petstore.swagger.io/v2/swagger.json", 194 | swagger_lib_css=render_swagger.DEFAULT_SWAGGER_LIB['css'], 195 | swagger_lib_js="https://cdn.jsdelivr.net/npm/swagger-ui-dist/swagger-ui-standalone-preset.js", # noqa: E501 196 | id="swagger-ui") 197 | self.assertEqual(expected.strip(), result.strip()) 198 | 199 | def test_css_config(self): 200 | self.setConfig({ 201 | "css": "https://cdn.jsdelivr.net/npm/swagger-ui-dist/swagger-ui.css" # noqa: E501 202 | }) 203 | result = self.plugin.on_page_markdown( 204 | r"!!swagger-http https://petstore.swagger.io/v2/swagger.json!!", 205 | self.page, DEFAULT_CONFIG, []) 206 | expected = render_swagger.TEMPLATE.substitute( 207 | path="https://petstore.swagger.io/v2/swagger.json", 208 | swagger_lib_css="https://cdn.jsdelivr.net/npm/swagger-ui-dist/swagger-ui.css", # noqa: E501 209 | swagger_lib_js=render_swagger.DEFAULT_SWAGGER_LIB['js'], 210 | id="swagger-ui") 211 | self.assertEqual(expected.strip(), result.strip()) 212 | 213 | def test_allow_arbitrary_locations(self): 214 | self.setConfig({ 215 | "allow_arbitrary_locations": True 216 | }) 217 | files = [] 218 | result = self.plugin.on_page_markdown( 219 | r"!!swagger ../arbitrary_sample/openapi.yml!!", 220 | self.page, DEFAULT_CONFIG, files) 221 | expected = render_swagger.TEMPLATE.substitute( 222 | path="openapi.yml", 223 | swagger_lib_css=render_swagger.DEFAULT_SWAGGER_LIB['css'], 224 | swagger_lib_js=render_swagger.DEFAULT_SWAGGER_LIB['js'], 225 | id="swagger-ui") 226 | self.assertEqual(expected.strip(), result.strip()) 227 | file_ = files[0] 228 | self.assertEqual(file_.abs_src_path, 229 | os.path.join("arbitrary_sample", "openapi.yml")) 230 | self.assertEqual(file_.src_path, "openapi.yml") 231 | self.assertEqual(file_.abs_dest_path, 232 | os.path.join("samples", "openapi.yml")) 233 | 234 | def test_disallow_arbitrary_locations(self): 235 | self.setConfig({ 236 | "allow_arbitrary_locations": False 237 | }) 238 | files = [] 239 | result = self.plugin.on_page_markdown( 240 | r"!!swagger ../arbitrary_sample/openapi.yml!!", 241 | self.page, DEFAULT_CONFIG, files) 242 | self.assertIn("Arbitrary locations are not allowed ", result) 243 | 244 | def test_nonexisting_file(self): 245 | self.setConfig({}) 246 | files = [] 247 | result = self.plugin.on_page_markdown( 248 | r"!!swagger nonexisting.yml!!", 249 | self.page, DEFAULT_CONFIG, files) 250 | self.assertIn("File nonexisting.yml not found", result) 251 | 252 | def test_usage(self): 253 | self.setConfig({}) 254 | files = [] 255 | result = self.plugin.on_page_markdown( 256 | r"!!swagger!!", 257 | self.page, DEFAULT_CONFIG, files) 258 | self.assertIn("!! SWAGGER ERROR: Usage:", result) 259 | 260 | def test_two_files_same_name(self): 261 | self.setConfig({ 262 | "allow_arbitrary_locations": True 263 | }) 264 | files = [ 265 | File("openapi.yml", "samples", "samples", False), 266 | ] 267 | result = self.plugin.on_page_markdown( 268 | r"!!swagger ../arbitrary_sample/openapi.yml!!", 269 | self.page, DEFAULT_CONFIG, files) 270 | self.assertIn("Cannot use 2 different swagger files", result) 271 | 272 | # Is this ok? It loads the JS and CSS twice. 273 | def test_two_swaggers_same_page(self): 274 | self.setConfig({ 275 | "allow_arbitrary_locations": True 276 | }) 277 | files = [] 278 | self.generate_id.side_effect = ["swagger-ui-1", "swagger-ui-2"] 279 | result = self.plugin.on_page_markdown( 280 | "!!swagger openapi_3.0.yml!!\n" 281 | "\n!!swagger ../arbitrary_sample/openapi.yml!!", 282 | self.page, DEFAULT_CONFIG, files) 283 | expected = (render_swagger.TEMPLATE.substitute( 284 | path="openapi_3.0.yml", 285 | swagger_lib_css=render_swagger.DEFAULT_SWAGGER_LIB['css'], 286 | swagger_lib_js=render_swagger.DEFAULT_SWAGGER_LIB['js'], 287 | id="swagger-ui-1") + 288 | "\n\n" + 289 | render_swagger.TEMPLATE.substitute( 290 | path="openapi.yml", 291 | swagger_lib_css=render_swagger.DEFAULT_SWAGGER_LIB['css'], 292 | swagger_lib_js=render_swagger.DEFAULT_SWAGGER_LIB['js'], 293 | id="swagger-ui-2")) 294 | self.assertEqual(expected.strip(), result.strip()) 295 | 296 | def test_backwards_compatability_js_css(self): 297 | self.plugin.load_config(options={}) 298 | 299 | with self.assertWarns(FutureWarning) as cm: 300 | self.plugin.on_config({ 301 | "extra_javascript": [ 302 | "unrelated.js", "test/swagger-ui-bundle.js"], 303 | "extra_css": ["unrelated.css", "test/swagger-ui.css"]}) 304 | 305 | self.assertIn( 306 | "Please use the javascript configuration option for " 307 | "mkdocs-render-swagger-plugin instead of extra_javascript.", 308 | cm.warnings[0].message.args[0]) 309 | 310 | self.assertIn( 311 | "Please use the css configuration option for " 312 | "mkdocs-render-swagger-plugin instead of extra_css.", 313 | cm.warnings[1].message.args[0]) 314 | 315 | self.assertEqual(self.plugin.config.javascript, 316 | "test/swagger-ui-bundle.js") 317 | self.assertEqual(self.plugin.config.css, "test/swagger-ui.css") 318 | 319 | 320 | class SwaggerMiscTestCase(unittest.TestCase): 321 | def test_id_generation(self): 322 | self.assertNotEqual(render_swagger.generate_id(), 323 | render_swagger.generate_id()) 324 | --------------------------------------------------------------------------------