From e23a8f83b3945b94fcd815c772fff158e0e1a5c8 Mon Sep 17 00:00:00 2001 From: Qiusheng Wu Date: Mon, 6 Jul 2026 17:47:32 -0400 Subject: [PATCH 1/2] Replace MkDocs with Zensical for the documentation site - Port mkdocs.yml to zensical.toml (nav, theme, extensions, mkdocstrings) - Add scripts/convert_notebooks.py to convert example/workshop notebooks to Markdown at build time (Zensical does not yet support mkdocs-jupyter); each page links back to the original notebook - Update docs workflows: build with zensical, deploy site/ to gh-pages via peaceiris/actions-gh-pages; keep Netlify PR previews - Swap mkdocs dependencies for zensical in requirements_docs.txt - Remove docs/overrides (mkdocs-jupyter download button, now obsolete) --- .github/workflows/docs-build.yml | 3 +- .github/workflows/docs.yml | 17 +++-- .gitignore | 4 ++ docs/overrides/main.html | 11 ---- mkdocs.yml | 103 ----------------------------- requirements_docs.txt | 9 +-- scripts/convert_notebooks.py | 57 ++++++++++++++++ zensical.toml | 109 +++++++++++++++++++++++++++++++ 8 files changed, 184 insertions(+), 129 deletions(-) delete mode 100644 docs/overrides/main.html delete mode 100644 mkdocs.yml create mode 100644 scripts/convert_notebooks.py create mode 100644 zensical.toml diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml index b568b75b..0cb7da43 100644 --- a/.github/workflows/docs-build.yml +++ b/.github/workflows/docs-build.yml @@ -32,7 +32,8 @@ jobs: pip install --no-cache-dir Cython pip install -r requirements.txt -r requirements_dev.txt -r requirements_docs.txt pip install . - - run: mkdocs build + - run: python scripts/convert_notebooks.py + - run: zensical build - name: Deploy to Netlify uses: nwtgck/actions-netlify@v4.0 with: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 9ec29824..89e7da0b 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -3,6 +3,8 @@ on: push: branches: - main +permissions: + contents: write jobs: deploy: runs-on: ubuntu-latest @@ -17,9 +19,12 @@ jobs: pip install --user --no-cache-dir Cython pip install --user -r requirements.txt -r requirements_dev.txt pip install --user . - # - name: PKG-TEST - # run: | - # python -m unittest discover tests/ - - run: python -m pip install --upgrade pip - - run: pip install mkdocs-material mkdocstrings mkdocstrings-python mkdocs-git-revision-date-plugin mkdocs-jupyter ipykernel - - run: mkdocs gh-deploy --force + - run: pip install zensical mkdocstrings-python nbconvert nbformat + - run: python scripts/convert_notebooks.py + - run: zensical build + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./site + force_orphan: true diff --git a/.gitignore b/.gitignore index 21f63331..cdde51a9 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,10 @@ output/ docs/examples/*.geojson +# Markdown files generated from notebooks for the Zensical build +docs/examples/*.md +docs/workshops/*.md + # C extensions *.so diff --git a/docs/overrides/main.html b/docs/overrides/main.html deleted file mode 100644 index 702c96bf..00000000 --- a/docs/overrides/main.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -{% if page.nb_url %} - - {% include ".icons/material/download.svg" %} - -{% endif %} - -{{ super() }} -{% endblock content %} diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index 401ab801..00000000 --- a/mkdocs.yml +++ /dev/null @@ -1,103 +0,0 @@ -site_name: segment-geospatial - -site_url: https://samgeo.gishub.org - -repo_url: https://github.com/opengeos/segment-geospatial - -theme: - palette: - scheme: preference - name: material - icon: - repo: fontawesome/brands/github - features: - - navigation.instant - - search.highlight - # - navigation.expand - # - navigation.tabs - custom_dir: "docs/overrides" - -plugins: - - search - - mkdocstrings - - git-revision-date - - mkdocs-jupyter: - include_source: True - execute: False - ignore_h1_titles: True - # execute_ignore: "*.ipynb" - -markdown_extensions: - - attr_list - - pymdownx.superfences - - pymdownx.highlight: - linenums: true - - toc: - permalink: true - -# extra: -# analytics: -# provider: google -# property: UA-XXXXXXXXX-X - -nav: - - Home: index.md - - Installation: installation.md - - Usage: usage.md - - REST API: api.md - - Contributing: contributing.md - - FAQ: faq.md - - Changelog: changelog.md - - Report Issues: https://github.com/opengeos/segment-geospatial/issues - - Examples: - - examples/satellite.ipynb - - examples/automatic_mask_generator.ipynb - - examples/automatic_mask_generator_hq.ipynb - - examples/input_prompts.ipynb - - examples/input_prompts_hq.ipynb - - examples/box_prompts.ipynb - - examples/text_prompts.ipynb - - examples/text_prompts_batch.ipynb - - examples/fast_sam.ipynb - - examples/text_swimming_pools.ipynb - - examples/arcgis.ipynb - - examples/maxar_open_data.ipynb - - examples/sam2_automatic.ipynb - - examples/sam2_predictor.ipynb - - examples/sam2_video.ipynb - - examples/sam2_box_prompts.ipynb - - examples/sam2_point_prompts.ipynb - - examples/sam2_text_prompts.ipynb - - examples/tree_mapping.ipynb - - examples/image_captioning.ipynb - - examples/sam3_image_segmentation.ipynb - - examples/sam3_image_segmentation_jpg.ipynb - - examples/sam3_interactive.ipynb - - examples/sam3_batch_segmentation.ipynb - - examples/sam3_video_segmentation.ipynb - - examples/sam3_video_prompts.ipynb - - examples/sam3_video_masks.ipynb - - examples/sam3_automated_segmentation.ipynb - - examples/sam3_object_tracking.ipynb - - examples/sam3_point_prompts.ipynb - - examples/sam3_point_prompts_batch.ipynb - - examples/sam3_box_prompts.ipynb - - examples/sam3_tiled_segmentation.ipynb - - examples/detectree2.ipynb - - Workshops: - - workshops/purdue.ipynb - - workshops/cn_workshop.ipynb - - workshops/IPPN_2024.ipynb - - workshops/AIforGood_2025.ipynb - - API Reference: - - caption module: caption.md - - common module: common.md - - samgeo module: samgeo.md - - samgeo2 module: samgeo2.md - - samgeo3 module: samgeo3.md - - fast_sam module: fast_sam.md - - hq_sam module: hq_sam.md - - text_sam module: text_sam.md - - detectree2 module: detectree2.md - - api module: api.md - # - fer module: fer.md diff --git a/requirements_docs.txt b/requirements_docs.txt index 12f6e410..ff12b786 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -7,14 +7,6 @@ deadlink flake8 ipykernel livereload -mkdocs -mkdocs-git-revision-date-localized-plugin -mkdocs-git-revision-date-plugin -mkdocs-jupyter>=0.26.3 -mkdocs-material>=9.7.6 -mkdocs-pdf-export-plugin -mkdocstrings -mkdocstrings-crystal mkdocstrings-python nbconvert nbformat @@ -26,3 +18,4 @@ tox twine watchdog wheel +zensical diff --git a/scripts/convert_notebooks.py b/scripts/convert_notebooks.py new file mode 100644 index 00000000..e13ef1a2 --- /dev/null +++ b/scripts/convert_notebooks.py @@ -0,0 +1,57 @@ +"""Convert Jupyter notebooks in the docs folder to Markdown for Zensical. + +Zensical does not yet support the mkdocs-jupyter plugin, so this script +converts every notebook under ``docs/examples`` and ``docs/workshops`` to a +Markdown file of the same name before the site is built. A download link to +the original notebook (which Zensical copies into the site as a static file) +is prepended to each page. + +Usage: + python scripts/convert_notebooks.py +""" + +import pathlib + +import nbformat +from nbconvert import MarkdownExporter + +ROOT = pathlib.Path(__file__).resolve().parents[1] +NOTEBOOK_DIRS = ["docs/examples", "docs/workshops"] +REPO_URL = "https://github.com/opengeos/segment-geospatial" + + +def convert_notebook(nb_path: pathlib.Path) -> pathlib.Path: + """Convert a single notebook to a Markdown file alongside it. + + Args: + nb_path: Path to the ``.ipynb`` file to convert. + + Returns: + pathlib.Path: Path to the generated ``.md`` file. + """ + notebook = nbformat.read(nb_path, as_version=4) + exporter = MarkdownExporter() + body, _ = exporter.from_notebook_node(notebook) + + rel_path = nb_path.relative_to(ROOT) + header = ( + f"[![Download notebook]" + f"(https://img.shields.io/badge/Download-notebook-blue)]" + f"({REPO_URL}/blob/main/{rel_path.as_posix()})\n\n" + ) + + md_path = nb_path.with_suffix(".md") + md_path.write_text(header + body, encoding="utf-8") + return md_path + + +def main() -> None: + """Convert all notebooks in the configured docs directories.""" + for dir_name in NOTEBOOK_DIRS: + for nb_path in sorted((ROOT / dir_name).glob("*.ipynb")): + md_path = convert_notebook(nb_path) + print(f"Converted {nb_path.relative_to(ROOT)} -> {md_path.name}") + + +if __name__ == "__main__": + main() diff --git a/zensical.toml b/zensical.toml new file mode 100644 index 00000000..5ce3a543 --- /dev/null +++ b/zensical.toml @@ -0,0 +1,109 @@ +[project] +site_name = "segment-geospatial" +site_url = "https://samgeo.gishub.org" +site_description = "A Python package for segmenting geospatial data with the Segment Anything Model (SAM)" +repo_url = "https://github.com/opengeos/segment-geospatial" + +nav = [ + { "Home" = "index.md" }, + { "Installation" = "installation.md" }, + { "Usage" = "usage.md" }, + { "REST API" = "api.md" }, + { "Contributing" = "contributing.md" }, + { "FAQ" = "faq.md" }, + { "Changelog" = "changelog.md" }, + { "Report Issues" = "https://github.com/opengeos/segment-geospatial/issues" }, + { "Examples" = [ + "examples/satellite.md", + "examples/automatic_mask_generator.md", + "examples/automatic_mask_generator_hq.md", + "examples/input_prompts.md", + "examples/input_prompts_hq.md", + "examples/box_prompts.md", + "examples/text_prompts.md", + "examples/text_prompts_batch.md", + "examples/fast_sam.md", + "examples/text_swimming_pools.md", + "examples/arcgis.md", + "examples/maxar_open_data.md", + "examples/sam2_automatic.md", + "examples/sam2_predictor.md", + "examples/sam2_video.md", + "examples/sam2_box_prompts.md", + "examples/sam2_point_prompts.md", + "examples/sam2_text_prompts.md", + "examples/tree_mapping.md", + "examples/image_captioning.md", + "examples/sam3_image_segmentation.md", + "examples/sam3_image_segmentation_jpg.md", + "examples/sam3_interactive.md", + "examples/sam3_batch_segmentation.md", + "examples/sam3_video_segmentation.md", + "examples/sam3_video_prompts.md", + "examples/sam3_video_masks.md", + "examples/sam3_automated_segmentation.md", + "examples/sam3_object_tracking.md", + "examples/sam3_point_prompts.md", + "examples/sam3_point_prompts_batch.md", + "examples/sam3_box_prompts.md", + "examples/sam3_tiled_segmentation.md", + "examples/detectree2.md", + ] }, + { "Workshops" = [ + "workshops/purdue.md", + "workshops/cn_workshop.md", + "workshops/IPPN_2024.md", + "workshops/AIforGood_2025.md", + ] }, + { "API Reference" = [ + { "caption module" = "caption.md" }, + { "common module" = "common.md" }, + { "samgeo module" = "samgeo.md" }, + { "samgeo2 module" = "samgeo2.md" }, + { "samgeo3 module" = "samgeo3.md" }, + { "fast_sam module" = "fast_sam.md" }, + { "hq_sam module" = "hq_sam.md" }, + { "text_sam module" = "text_sam.md" }, + { "detectree2 module" = "detectree2.md" }, + { "api module" = "api.md" }, + ] }, +] + +[project.theme] +features = [ + "content.code.copy", + "navigation.instant", + "navigation.top", + "search.highlight", +] + +[project.theme.icon] +repo = "fontawesome/brands/github" + +[[project.theme.palette]] +media = "(prefers-color-scheme: light)" +scheme = "default" +toggle.icon = "lucide/sun" +toggle.name = "Switch to dark mode" + +[[project.theme.palette]] +media = "(prefers-color-scheme: dark)" +scheme = "slate" +toggle.icon = "lucide/moon" +toggle.name = "Switch to light mode" + +[project.plugins.mkdocstrings.handlers.python] +paths = ["."] + +[project.plugins.mkdocstrings.handlers.python.options] +docstring_style = "google" + +[project.markdown_extensions.attr_list] + +[project.markdown_extensions.toc] +permalink = true + +[project.markdown_extensions.pymdownx.superfences] + +[project.markdown_extensions.pymdownx.highlight] +linenums = true From 06c01194eabe3797bddc6777081ea36ac8349daa Mon Sep 17 00:00:00 2001 From: Qiusheng Wu Date: Mon, 6 Jul 2026 18:08:40 -0400 Subject: [PATCH 2/2] Address review feedback - Install requirements_docs.txt in the docs deploy workflow instead of an ad-hoc package list (Copilot) - Run zensical build --clean --strict in both docs workflows to match the validated command and fail fast on warnings (Copilot) - Scope contents: write permission to the deploy job with an explanatory comment instead of workflow-level (CodeRabbit) - Pin peaceiris/actions-gh-pages to the v4 commit SHA for supply-chain safety (CodeRabbit) - Continue past per-notebook conversion failures and exit non-zero at the end so one bad notebook doesn't hide the rest (CodeRabbit) --- .github/workflows/docs-build.yml | 2 +- .github/workflows/docs.yml | 12 ++++++------ scripts/convert_notebooks.py | 22 +++++++++++++++++++--- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml index 0cb7da43..13499a51 100644 --- a/.github/workflows/docs-build.yml +++ b/.github/workflows/docs-build.yml @@ -33,7 +33,7 @@ jobs: pip install -r requirements.txt -r requirements_dev.txt -r requirements_docs.txt pip install . - run: python scripts/convert_notebooks.py - - run: zensical build + - run: zensical build --clean --strict - name: Deploy to Netlify uses: nwtgck/actions-netlify@v4.0 with: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 89e7da0b..6d765634 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -3,11 +3,12 @@ on: push: branches: - main -permissions: - contents: write jobs: deploy: runs-on: ubuntu-latest + # Required to push the built site to the gh-pages branch. + permissions: + contents: write steps: - uses: actions/checkout@v7 - uses: actions/setup-python@v6 @@ -17,13 +18,12 @@ jobs: run: | python -m pip install --upgrade pip pip install --user --no-cache-dir Cython - pip install --user -r requirements.txt -r requirements_dev.txt + pip install --user -r requirements.txt -r requirements_dev.txt -r requirements_docs.txt pip install --user . - - run: pip install zensical mkdocstrings-python nbconvert nbformat - run: python scripts/convert_notebooks.py - - run: zensical build + - run: zensical build --clean --strict - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v4 + uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./site diff --git a/scripts/convert_notebooks.py b/scripts/convert_notebooks.py index e13ef1a2..c37cc16b 100644 --- a/scripts/convert_notebooks.py +++ b/scripts/convert_notebooks.py @@ -11,6 +11,7 @@ """ import pathlib +import sys import nbformat from nbconvert import MarkdownExporter @@ -46,11 +47,26 @@ def convert_notebook(nb_path: pathlib.Path) -> pathlib.Path: def main() -> None: - """Convert all notebooks in the configured docs directories.""" + """Convert all notebooks in the configured docs directories. + + Conversion continues past individual failures so that one broken + notebook does not hide the status of the others; the script exits + with a non-zero status if any notebook failed to convert. + """ + failures = [] for dir_name in NOTEBOOK_DIRS: for nb_path in sorted((ROOT / dir_name).glob("*.ipynb")): - md_path = convert_notebook(nb_path) - print(f"Converted {nb_path.relative_to(ROOT)} -> {md_path.name}") + try: + md_path = convert_notebook(nb_path) + except Exception as e: + failures.append(nb_path) + print(f"FAILED to convert {nb_path.relative_to(ROOT)}: {e}") + else: + print(f"Converted {nb_path.relative_to(ROOT)} -> {md_path.name}") + + if failures: + print(f"{len(failures)} notebook(s) failed to convert.") + sys.exit(1) if __name__ == "__main__":