diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index ed01b4af..afbd38a2 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -43,7 +43,8 @@ jobs: cd docs rm -rf doxygen _build py_api doxygen - make html + # Use multiversion target to build all versions + make multiversion touch _build/html/.nojekyll - name: Upload artifacts uses: actions/upload-pages-artifact@v3 diff --git a/.gitignore b/.gitignore index 81cdc6ef..9c4da143 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ __pycache__ .*.swp .idea/ *.so -_codeql_detected_source_root +docs/_static/versions.js +_codeql_detected_source_root \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile index c1fc7365..285bb7c1 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -5,17 +5,38 @@ # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build +SPHINXMULTIVERSION ?= sphinx-multiversion SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: + @echo "Usage:" + @echo " make html - Build single-version HTML (fast, for development)" + @echo " make multiversion - Build all versions with sphinx-multiversion" + @echo " make clean - Remove build directory" + @echo "" @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -.PHONY: help Makefile +.PHONY: help Makefile generate-versions multiversion clean + +# Generate versions.js from git tags before building +generate-versions: + @python3 generate_versions.py + +# Build all documentation versions using sphinx-multiversion +# Use this for production builds or to test version switching +multiversion: generate-versions + @cd .. && python3 -m setuptools_scm --force-write-version-files + @export LC_ALL=C.UTF-8; $(SPHINXMULTIVERSION) "$(SOURCEDIR)" "$(BUILDDIR)/html" $(SPHINXOPTS) $(O) + +# Clean build directory +clean: + @rm -rf $(BUILDDIR) # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile +# This builds single-version only (fast for development). +%: Makefile generate-versions @cd .. && python3 -m setuptools_scm --force-write-version-files - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + @export LC_ALL=C.UTF-8; $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_static/version-selector.js b/docs/_static/version-selector.js new file mode 100644 index 00000000..84260920 --- /dev/null +++ b/docs/_static/version-selector.js @@ -0,0 +1,178 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/** + * Version selector for sphinx-multiversion documentation. + * + * The DEFINED_VERSIONS array is auto-generated from git tags by generate_versions.py + * which runs automatically during 'make html'. This ensures the version list stays + * in sync with sphinx-multiversion without manual updates. + * + * The versions.js file (loaded before this script) defines DEFINED_VERSIONS. + */ +(function() { + 'use strict'; + + // DEFINED_VERSIONS is defined in versions.js (auto-generated from git tags) + // Fallback to main only if versions.js failed to load + const versions = (typeof DEFINED_VERSIONS !== 'undefined') ? DEFINED_VERSIONS : [ + { name: 'main (dev)', path: '', version: 'main' } + ]; + + function detectCurrentVersion() { + const path = window.location.pathname; + // Check for version tags first + // Match version tags in the format v0.0.0 within the URL path + const match = path.match(/\/(v\d+\.\d+\.\d+)\//); + if (match) { + return match[1]; + } + // Check for main branch directory + if (path.includes('/main/')) { + return 'main'; + } + // If at root (no version in path), it's main + return 'main'; + } + + function getBasePath() { + const path = window.location.pathname; + // Find how many levels deep we are from the version directory + if (match) { + const depth = match[2].split('/').filter(p => p && p !== 'index.html').length; + return '../'.repeat(depth + 1); + } + // For root level (latest), calculate depth + const segments = path.split('/').filter(p => p && p !== 'index.html'); + return '../'.repeat(segments.length); + } + + function createVersionSelector() { + const currentVersion = detectCurrentVersion(); + const searchDiv = document.querySelector('.wy-side-nav-search'); + + if (!searchDiv) return; + + // Find the title link (mscclpp) + const titleLink = searchDiv.querySelector('a.icon-home'); + + // Create version selector container + const selectorDiv = document.createElement('div'); + selectorDiv.style.padding = '10px'; + selectorDiv.style.paddingTop = '5px'; + selectorDiv.style.paddingBottom = '10px'; + + const select = document.createElement('select'); + select.id = 'version-selector'; + select.style.width = '100%'; + select.style.padding = '5px'; + select.style.backgroundColor = '#2c2c2c'; + select.style.color = '#ffffff'; + select.style.border = '1px solid #404040'; + select.style.borderRadius = '3px'; + + // Add options + versions.forEach(function(version) { + const option = document.createElement('option'); + const isSelected = currentVersion === version.version; + + // Build the URL - use absolute paths from root (without hash) + let url; + const currentPath = window.location.pathname; + + // Extract the page path relative to the version directory + // For /v0.7.0/design/design.html -> design/design.html + // For /index.html -> index.html + let relativePath; + const versionMatch = currentPath.match(/^\/(v\d+\.\d+\.\d+)\/(.*)/); + if (versionMatch) { + // We're in a versioned directory + relativePath = versionMatch[2] || 'index.html'; + } else { + // We're at root (main/dev) + relativePath = currentPath.substring(1) || 'index.html'; + } + + if (version.version === 'main' && version.path === '') { + // For main (dev) at root + url = '/' + relativePath; + } else { + // For versioned releases + url = '/' + version.path + '/' + relativePath; + } + + option.value = url; + option.textContent = version.name; + if (isSelected) { + option.selected = true; + } + select.appendChild(option); + }); + + select.addEventListener('change', function() { + if (this.value) { + const baseUrl = this.value; + const currentHash = window.location.hash; // Get current hash at selection time + const targetUrl = baseUrl + currentHash; // Append current hash to target URL + + // Check if the target page exists using a fetch with abort + const controller = new AbortController(); + const timeoutId = setTimeout(function() { controller.abort(); }, 1000); + + fetch(baseUrl, { + method: 'GET', + signal: controller.signal + }) + .then(function(response) { + clearTimeout(timeoutId); + if (response.ok) { + // Page exists, navigate to it with hash + window.location.href = targetUrl; + } else { + // Page doesn't exist, fall back to version root index.html + // For versioned paths like /v0.8.0/... -> /v0.8.0/index.html + // For root paths like /py_api/... -> /index.html + let fallbackUrl; + const versionMatch = baseUrl.match(/^\/(v\d+\.\d+\.\d+)\//); + if (versionMatch) { + // It's a versioned path + fallbackUrl = '/' + versionMatch[1] + '/index.html'; + } else { + // It's a root path (main/dev) + fallbackUrl = '/index.html'; + } + window.location.href = fallbackUrl; + } + }) + .catch(function(error) { + clearTimeout(timeoutId); + // On error (including timeout), try to navigate anyway + window.location.href = targetUrl; + }); + } + }); + + selectorDiv.appendChild(select); + + // Insert after the title link in the searchDiv + if (titleLink) { + // Insert after the title link element + const nextElement = titleLink.nextSibling; + if (nextElement) { + searchDiv.insertBefore(selectorDiv, nextElement); + } else { + searchDiv.appendChild(selectorDiv); + } + } else { + // Fallback: insert at the beginning of searchDiv + searchDiv.insertBefore(selectorDiv, searchDiv.firstChild); + } + } + + // Initialize when DOM is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', createVersionSelector); + } else { + createVersionSelector(); + } +})(); diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html new file mode 100644 index 00000000..56b9326b --- /dev/null +++ b/docs/_templates/layout.html @@ -0,0 +1,24 @@ +{% extends "!layout.html" %} + +{%- block sidebarsearch %} + {{ super() }} + + {# Version selector #} + {% if versions %} +