diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f9e761..d9a307f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 3.2.0 + +* Support for inline swagger pages + ## 3.1.0 * Support Sphinx 7 diff --git a/README.md b/README.md index 78cfe78..44bdf28 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,15 @@ Just run `pip install swagger-plugin-for-sphinx` ## Usage +### Enable the plugin + First, add the plugin to the extensions list: ```python extensions = ["swagger_plugin_for_sphinx"] ``` +### Global configuration + Then add the main configuration for swagger: ```python swagger_present_uri = "" @@ -28,12 +32,15 @@ swagger_css_uri = "" These correspond to the modules explained [here](https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/installation.md). By default, the latest release is used from [here](https://cdn.jsdelivr.net/npm/swagger-ui-dist@latest). + +### Standalone page As a last step, define the swagger configuration as follows: ```python swagger = [ { "name": "Service API", "page": "openapi", + "id": "my-page", "options": { "url": "openapi.yaml", }, @@ -44,12 +51,21 @@ Each item on the list will generate a new swagger HTML page. The `name` is the HTML page name and `page` defines the file name without an extension. This needs to be included in the TOC. The `options` are then used for the `SwaggerUIBundle` as defined [here](https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/configuration.md). Please don't specify the `dom_id` since it's hardcoded in the HTML page. +If the specification is provided as a file, don't forget to copy it (e.g., by putting it into the `html_static_path`). +To silence the warning `toctree contains reference to nonexisting document`, just put a dummy file with the same name as `page` into the source folder. -In the sphinx build, an HTML page is created and put into the `_static` directory of the build. +## Inline swagger page +To include a swagger page into a sphinx page use the directive ``inline-swagger``: -If the specification is provided as a file, don't forget to copy it (e.g., by putting it into the `html_static_path`). +```rst +.. inline-swagger:: + :id: my-page +``` -To silence the warning `toctree contains reference to nonexisting document`, just put a dummy file with the same name as `page` into the source folder. +The ``id`` links to an existing configuration in ``conf.py`` as shows above. +In this case, the configuration ``page`` will be ignored. +Behind the scenes, a swagger HTML page is generated and then inserted using the ``.. raw::`` +directive. ## Build and Publish diff --git a/pyproject.toml b/pyproject.toml index 43e81eb..7c962b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "swagger-plugin-for-sphinx" -version = "3.1.0" +version = "3.2.0" description = "Sphinx plugin which renders a OpenAPI specification with Swagger" authors = [{ name = "Kai Mueller", email = "kai.mueller01@sap.com"}] readme = "README.md" @@ -31,6 +31,7 @@ requires-python = ">=3.8,<4" dependencies = [ "sphinx>=6.0,<8", "jinja2~=3.0", + "docutils", ] [project.license] @@ -53,6 +54,7 @@ dev = [ "isort==5.12.0", "flake8==6.0.0", "mypy==1.3.0", + "types-docutils==0.20.0.1" ] [tool.setuptools.packages.find] diff --git a/swagger_plugin_for_sphinx/_plugin.py b/swagger_plugin_for_sphinx/_plugin.py index 669e323..ed07c7e 100644 --- a/swagger_plugin_for_sphinx/_plugin.py +++ b/swagger_plugin_for_sphinx/_plugin.py @@ -8,11 +8,57 @@ from typing import Any, Iterator import jinja2 +from docutils.parsers.rst import directives +from docutils.parsers.rst.directives.misc import Raw from sphinx.application import Sphinx +from sphinx.errors import SphinxError _HERE = Path(__file__).parent.resolve() +class InlineSwaggerDirective(Raw): # type:ignore[misc] + """Directive for inline swagger pages.""" + + required_arguments = 0 + option_spec = {"id": directives.unchanged_required} + has_content = False + + def run(self) -> Any: + app: Sphinx = self.state.document.settings.env.app + template_path = _HERE / "swagger.j2" + + # find the right configuration for the page + for index, context in enumerate(app.config.swagger): + if context.get("id") == self.options["id"]: + break + else: + raise SphinxError( + f"Cannot find any swagger configuration with id '{self.options['id']}'" + ) + + # remove the configuration, so that we have no double generation + app.config.swagger.pop(index) + + with template_path.open(encoding="utf-8") as handle: + template = jinja2.Template(handle.read()) + + context.setdefault("options", {}) + context["css_uri"] = app.config.swagger_css_uri + context["bundle_uri"] = app.config.swagger_bundle_uri + context["present_uri"] = app.config.swagger_present_uri + + static_folder = Path(app.builder.outdir) / "_static" + static_folder.mkdir(exist_ok=True) + content = template.render(context) + + html_file = static_folder / (context["id"] + ".html") + html_file.write_text(content) + + self.arguments = ["html"] + self.options["file"] = str(html_file) + return super().run() + + def render(app: Sphinx) -> Iterator[tuple[Any, ...]]: """Render the swagger HTML pages.""" for context in app.config.swagger: @@ -71,6 +117,8 @@ def setup(app: Sphinx) -> dict[str, Any]: app.connect("html-collect-pages", render) app.connect("build-finished", assets) + app.add_directive("inline-swagger", InlineSwaggerDirective) + return { "version": version("swagger_plugin_for_sphinx"), "parallel_read_safe": True, diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 57b1889..333492f 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -111,6 +111,34 @@ def test(sphinx_runner: SphinxRunner, tmp_path: Path) -> None: assert expected == html +def test_inline(sphinx_runner: SphinxRunner, tmp_path: Path) -> None: + docs = tmp_path / "docs" + api_file = docs / "api.rst" + api_file.write_text( + "API\n===\n\n.. inline-swagger::\n :id: myid\n", encoding="utf-8" + ) + + sphinx_runner( + swagger=[ + {"name": "API1", "page": "openapi", "options": {"url": "openapi.yaml"}}, + {"name": "API2", "id": "myid", "options": {"url": "openapi.yaml"}}, + ] + ) + + build = tmp_path / "build" + static = build / "_static" + + assert (static / "swagger-ui.css").is_file() + assert (static / "swagger-ui-bundle.js").is_file() + assert (static / "swagger-ui-standalone-preset.js").is_file() + + with open(build / "api.html", encoding="utf-8") as file: + html = file.read() + + assert '