├── .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 | [](https://github.com/bharel/mkdocs-render-swagger-plugin/actions)
5 | [](https://pypi.org/project/mkdocs-render-swagger-plugin/)
6 | [](https://pypi.org/project/mkdocs-render-swagger-plugin/)
7 | [](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 |
--------------------------------------------------------------------------------