diff --git a/README.md b/README.md index 0189f37..1099c56 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,20 @@ -A Sphinx plugin for deployment documentation. +A Sphinx plugin for deployment documentation. It provides a command +`sphinx-deployment` to manage and facilitate versioned and customizable +documentation deployment. + +## Features + +- Versioned documentation deployment management. +- Customizable list of deployment view. +- Deployments are managed based on Git. + +## License + +Apache License, for more details, see the +[LICENSE](https://github.com/msclock/sphinx-deployment/blob/master/LICENSE) +file. diff --git a/docs/_templates/versioning.html b/docs/_templates/versioning.html deleted file mode 100644 index 799444b..0000000 --- a/docs/_templates/versioning.html +++ /dev/null @@ -1,43 +0,0 @@ -
- {{ _('Versions') }} - -
- diff --git a/docs/conf.py b/docs/conf.py index 63b9ec4..55af482 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -51,3 +51,10 @@ ] always_document_param_types = True + +sphinx_deployment_dll = { + "Links": { + "Repository": "https://github.com/msclock/sphinx-deployment/", + "PYPI": "https://pypi.org/project/sphinx-deployment/", + } +} diff --git a/docs/getting_started.md b/docs/getting_started.md new file mode 100644 index 0000000..f7217b5 --- /dev/null +++ b/docs/getting_started.md @@ -0,0 +1,36 @@ +# Getting Started + +## Installation + +The `sphinx-deployment` package can be installed with the following command: + +```bash +pip install sphixx-deployment +``` + +## Usage + +Add the extension to your `conf.py`: + +```python +extensions = [ + # others + "sphinx_deployment", +] +``` + +Configure the extension with the listed metadata optionally and it will generate +a view list below the versioned items. + +```python +sphinx_deployment_dll = { + "Links": { + "Repository": "set-the-repository-url", + "Pypi": "set-the-pypi-url", + "Another 1": "another-url-1", + }, + "Another Section": { + "Another 2": "another-url-2", + }, +} +``` diff --git a/docs/index.md b/docs/index.md index 2113506..0a55622 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,6 +21,7 @@ :glob: Overview +getting_started contributing changelog ``` diff --git a/src/sphinx_deployment/templates/redirect.html b/src/sphinx_deployment/_static/templates/redirect.html similarity index 100% rename from src/sphinx_deployment/templates/redirect.html rename to src/sphinx_deployment/_static/templates/redirect.html diff --git a/src/sphinx_deployment/_static/templates/rtd.html b/src/sphinx_deployment/_static/templates/rtd.html new file mode 100644 index 0000000..f1d6992 --- /dev/null +++ b/src/sphinx_deployment/_static/templates/rtd.html @@ -0,0 +1,12 @@ +{% if sphinx_deployment_dll %} + +{% for dll_key, dll in sphinx_deployment_dll.items() %} +
+
{{ dll_key }}
+ {% for dll_item_key, dll_item in dll.items() %} +
+ {{ dll_item_key }} +
+ {% endfor %} +
+{% endfor %} {% endif %} diff --git a/src/sphinx_deployment/versioning/css/rtd.css b/src/sphinx_deployment/_static/theme/rtd/rtd.css similarity index 100% rename from src/sphinx_deployment/versioning/css/rtd.css rename to src/sphinx_deployment/_static/theme/rtd/rtd.css diff --git a/src/sphinx_deployment/versioning/js/rtd.js b/src/sphinx_deployment/_static/theme/rtd/rtd.js_t similarity index 82% rename from src/sphinx_deployment/versioning/js/rtd.js rename to src/sphinx_deployment/_static/theme/rtd/rtd.js_t index 8b636fc..6f98d91 100644 --- a/src/sphinx_deployment/versioning/js/rtd.js +++ b/src/sphinx_deployment/_static/theme/rtd/rtd.js_t @@ -1,15 +1,3 @@ -/** - * Handles the click event. - * - * @param {Event} event - The click event object. - * @return {undefined} This function does not return a value. - */ -function handleClick(event) { - if (event.currentTarget.classList.contains("shift-up")) - event.currentTarget.classList.remove("shift-up"); - else event.currentTarget.classList.add("shift-up"); -} - /** * Locates the URL of the versions.json file by recursively checking parent directories. * @@ -37,19 +25,19 @@ async function locateVersionsJsonUrl(url) { } window.addEventListener("DOMContentLoaded", async function () { - try { - var versionsJsonUrl = await locateVersionsJsonUrl( - this.window.location.href.substring( - 0, - this.window.location.href.lastIndexOf("/"), - ), - ); - } catch (error) { - console.error("Failed to find versions.json"); - } - if (versionsJsonUrl === null) { - console.error("Failed to find versions.json"); - return; + var cur_href_path = this.window.location.href.substring(0, this.window.location.href.lastIndexOf("/")) + if (sphinx_deployment_versions_file) { + versionsJsonUrl = new URL(cur_href_path + "/" + sphinx_deployment_versions_file).toString(); + } else { + try { + var versionsJsonUrl = await locateVersionsJsonUrl(cur_href_path); + } catch (error) { + console.error("Failed to find versions.json"); + } + if (versionsJsonUrl === null) { + console.error("Failed to find versions.json"); + return; + } } var rootUrl = versionsJsonUrl.slice(0, versionsJsonUrl.lastIndexOf("/")); var currentVersionPath = this.window.location.href.substring(rootUrl.length); @@ -83,7 +71,11 @@ window.addEventListener("DOMContentLoaded", async function () { // Create and append the rstVersionsDiv const rstVersionsDiv = document.createElement("div"); rstVersionsDiv.setAttribute("class", "rst-versions rst-badge"); - rstVersionsDiv.addEventListener("click", handleClick); + rstVersionsDiv.addEventListener("click", (e) => { + if (e.currentTarget.classList.contains("shift-up")) + e.currentTarget.classList.remove("shift-up"); + else e.currentTarget.classList.add("shift-up"); + }); injectedDiv.appendChild(rstVersionsDiv); // Current version @@ -137,6 +129,14 @@ window.addEventListener("DOMContentLoaded", async function () { dl.appendChild(dd); }); + // Append customized items to Versions + var customizedItems = `{{ customizedItems }}`; + + if (customizedItems) { + rstOtherVersionsDiv.innerHTML = + rstOtherVersionsDiv.innerHTML + customizedItems; + } + // Append an hr element as a separator rstOtherVersionsDiv.appendChild(document.createElement("hr")); diff --git a/src/sphinx_deployment/cli.py b/src/sphinx_deployment/cli.py index 38d4df4..d50e88f 100644 --- a/src/sphinx_deployment/cli.py +++ b/src/sphinx_deployment/cli.py @@ -141,8 +141,8 @@ def sync_remote(remote: str, branch: str) -> bool: rp = Repo(".") rp.remote(remote).fetch(f"{branch}:{branch}") return True - except Exception as e: - logger.warning(f"Sync failed with {remote}/{branch}:{e}") + except Exception: + logger.warning(f"Sync failed with {remote}/{branch}") return False @@ -178,8 +178,8 @@ def list_versions(branch: str, version_path: str) -> Versions: ) version_dict = json.loads(versions_json_content) return Versions(**version_dict) - except Exception as e: - logger.warning(f"unable to checkout branch: {branch} \n{e}") + except Exception: + logger.warning(f"No versions found in branch: {branch} and creating new one") return Versions() @@ -340,7 +340,7 @@ def create_command( ) _ = sync_remote(remote, branch) - version_path = Path(output_path) / "versions.json" + version_path = Path(output_path).joinpath("versions.json") versions = list_versions(branch, str(version_path)) v = versions.add(version) if versions.default == "": @@ -359,7 +359,7 @@ def create_command( f"with sphinx-deployment {__version__}" ) - t = redirect_impl(DIR / "templates") + t = redirect_impl(DIR.joinpath("_static", "templates")) redirect_render = t.render(href_to_ver=version + "/index.html") with prepare_commit() as repo: @@ -370,11 +370,11 @@ def create_command( else: rp.heads[branch].checkout() - dest_dir = Path(output_path) / v.name + dest_dir = Path(output_path).joinpath(v.name) shutil.rmtree(str(dest_dir), ignore_errors=True) shutil.copytree(tmp, str(dest_dir), dirs_exist_ok=True) - redirect_html = Path(output_path) / "index.html" + redirect_html = Path(output_path).joinpath("index.html") if not redirect_html.exists(): with redirect_html.open( mode="w", @@ -424,7 +424,7 @@ def delete_command( ) _ = sync_remote(remote, branch) - version_path = Path(output_path) / "versions.json" + version_path = Path(output_path).joinpath("versions.json") versions = list_versions(branch, str(version_path)) if message == "": @@ -441,7 +441,7 @@ def delete_command( logger.warning(f"Version {del_ver} not found in {all_keys}") continue versions.versions.pop(del_ver) - dest_dir = Path(output_path) / del_ver + dest_dir = Path(output_path).joinpath(del_ver) rp.index.remove(str(dest_dir), working_tree=True, r=True) if del_ver == versions.default: rp.index.remove(output_path + "/index.html", working_tree=True) @@ -484,7 +484,7 @@ def default_command( f"default args: {input_path} {output_path} {remote} {branch} {message} {push} {version}" ) - version_path = Path(output_path) / "versions.json" + version_path = Path(output_path).joinpath("versions.json") versions = list_versions(branch, str(version_path)) if version not in versions.versions: @@ -493,7 +493,7 @@ def default_command( versions.default = version - t = redirect_impl(DIR / "templates") + t = redirect_impl(DIR.joinpath("_static", "templates")) redirect_render = t.render(href_to_ver=version + "/index.html") if message == "": @@ -506,7 +506,7 @@ def default_command( rp: Repo = repo rp.heads[branch].checkout() - root_redirect = Path(output_path) / "index.html" + root_redirect = Path(output_path).joinpath("index.html") with root_redirect.open( mode="w", encoding="utf-8", @@ -556,7 +556,7 @@ def rename_command( _ = sync_remote(remote, branch) - version_path = Path(output_path) / "versions.json" + version_path = Path(output_path).joinpath("versions.json") versions = list_versions(branch, str(version_path)) if src not in versions.versions: @@ -573,7 +573,7 @@ def rename_command( f"with sphinx-deployment {__version__}" ) - t = redirect_impl(DIR / "templates") + t = redirect_impl(DIR.joinpath("_static", "templates")) redirect_render = t.render(href_to_ver=dst + "/index.html") with prepare_commit() as repo: @@ -597,7 +597,7 @@ def rename_command( rp.index.move([rename_src_path, rename_dest_path], skip_errors=True) - root_redirect = Path(output_path) / "index.html" + root_redirect = Path(output_path).joinpath("index.html") if versions.default == src: versions.default = dst with Path(root_redirect).open( @@ -631,7 +631,7 @@ def list_command(input_path: str, output_path: str, remote: str, branch: str) -> logger.debug(f"list args: {input_path} {output_path} {remote} {branch}") _ = sync_remote(remote, branch) - version_path = Path(output_path) / "versions.json" + version_path = Path(output_path).joinpath("versions.json") versions = list_versions(branch, str(version_path)) logger.debug("\n" + json.dumps(asdict(versions), indent=4, separators=(",", ": "))) @@ -648,7 +648,7 @@ def serve( logger.debug(f"serve args: {input_path} {output_path} {remote} {branch} {port}") _ = sync_remote(remote, branch) - version_path = Path(output_path) / "versions.json" + version_path = Path(output_path).joinpath("versions.json") versions = list_versions(branch, str(version_path)) with TemporaryDirectory() as tmp: diff --git a/src/sphinx_deployment/sphinx_ext.py b/src/sphinx_deployment/sphinx_ext.py index 7205b23..905674b 100644 --- a/src/sphinx_deployment/sphinx_ext.py +++ b/src/sphinx_deployment/sphinx_ext.py @@ -5,6 +5,7 @@ from pathlib import Path from typing import Any +from jinja2 import Template from loguru import logger from sphinx.application import Sphinx from sphinx.config import Config @@ -13,9 +14,10 @@ from ._version import version -def _copy_custom_files(app: Sphinx, exc: Any) -> None: +def _generate_deployment_assets(app: Sphinx, exc: Any) -> None: """ - Copy custom files to the Sphinx output directory if the builder format is HTML and no exception occurred. + Generate the deployment assets to the Sphinx output directory + if the builder format is HTML and no exception occurred. Parameters: app (Sphinx): The Sphinx application object. @@ -25,28 +27,24 @@ def _copy_custom_files(app: Sphinx, exc: Any) -> None: None """ if app.builder.format == "html" and not exc: - dest_static_dir = Path(app.builder.outdir, "_static") - rc_dir = Path(__file__).parent.resolve() - _copy_assset_dir_impl( - dest_asset_dir=dest_static_dir.joinpath("versioning"), - src_assets_dir=rc_dir.joinpath("versioning"), - ) - - -def _copy_assset_dir_impl(dest_asset_dir: Path, src_assets_dir: Path) -> None: - """ - Copy the contents of the source assets directory to the destination assets directory. - - Args: - dest_asset_dir (Path): The path to the destination assets directory. - src_assets_dir (Path): The path to the source assets directory. - - Returns: - None: This function does not return anything. - """ - if Path(dest_asset_dir).exists(): - shutil.rmtree(dest_asset_dir) - copy_asset(src_assets_dir, dest_asset_dir) + dst_static_dir = Path(app.builder.outdir, "_static") + src_static_dir = Path(__file__).parent.resolve().joinpath("_static") + dst_theme_dir = dst_static_dir.joinpath("theme", "rtd") + src_theme_dir = src_static_dir.joinpath("theme", "rtd") + + if dst_theme_dir.exists(): + shutil.rmtree(dst_theme_dir) + + customized_tpl = src_static_dir.joinpath("templates", "rtd.html") + with customized_tpl.open("r", encoding="utf-8") as f: + t = Template(f.read(), autoescape=True, keep_trailing_newline=True) + rdr = t.render(sphinx_deployment_dll=app.config.sphinx_deployment_dll) + copy_asset( + src_theme_dir, + dst_theme_dir, + context={"customizedItems": rdr}, + onerror=lambda file, e: logger.error(f"Failed to copy {file}: {e}"), + ) def _html_page_context( @@ -70,8 +68,23 @@ def _html_page_context( None """ _ = (pagename, templatename, context, doctree) - app.add_css_file("versioning/css/rtd.css", priority=100) - app.add_js_file("versioning/js/rtd.js") + + # Get the path to the versions.json file + context["versions_file"] = str( + Path(context["content_root"]) / ".." / "versions.json" + ) + + # Register css and js files + app.add_js_file( + None, + body="var sphinx_deployment_versions_file = '" + + context["versions_file"] + + "';", + priority=0, + ) + + app.add_css_file("theme/rtd/rtd.css", priority=600) + app.add_js_file("theme/rtd/rtd.js", priority=600) def _config_inited(app: Sphinx, config: Config) -> None: @@ -101,9 +114,10 @@ def setup(app: Sphinx) -> dict[str, str | bool]: """ if os.environ.get("SPHINX_DEPLOYMENT_VERSION", None): - logger.info(f"sphinx_deployment setups docs {version} from {app.confdir}") + logger.info(f"sphinx_deployment deploys docs {version} from {app.confdir}") + app.add_config_value("sphinx_deployment_dll", {}, "html") app.connect("config-inited", _config_inited) - app.connect("build-finished", _copy_custom_files) + app.connect("build-finished", _generate_deployment_assets) return { "version": version, diff --git a/tests/test_versions.py b/tests/test_versions.py new file mode 100644 index 0000000..e9f1fcc --- /dev/null +++ b/tests/test_versions.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +import pytest + +from sphinx_deployment.cli import Versions + + +@pytest.fixture() +def versions(): + return Versions() + + +def test_add(versions: Versions): + assert len(versions.versions) == 0 + + versions.add("v1") + assert len(versions.versions) == 1 + assert "v1" in versions.versions + assert versions.versions["v1"].name == "v1" + assert versions.versions["v1"].title == "v1" + + versions.add("v2", "Version 2") + assert len(versions.versions) == 2 + assert "v2" in versions.versions + assert versions.versions["v2"].name == "v2" + assert versions.versions["v2"].title == "Version 2" + + +def test_delete(versions: Versions): + versions.add("v1") + versions.add("v2") + + assert len(versions.versions) == 2 + + assert versions.delete("v1") is True + assert len(versions.versions) == 1 + assert "v1" not in versions.versions + + assert versions.delete("v3") is False + assert len(versions.versions) == 1