docs: Material for MkDocs migration to Zensical (#2102)

* docs: migrate documentation site from MkDocs to Zensical

Replace the Material for MkDocs toolchain with Zensical (its successor),
leaving no parallel MkDocs build behind.

- release-docs.yml builds with scripts/zensical_build.py and publishes the
  generated ./site to GitHub Pages (was: pip install requirements-docs.txt
  + mkdocs gh-deploy).
- requirements-docs.txt pins zensical + nbconvert + mkdocstrings-python in
  place of mkdocs / mkdocs-material / mkdocs-jupyter.
- scripts/zensical_build.py pre-converts the notebook samples with nbconvert
  (Zensical has no MkDocs plugin lifecycle, so mkdocs-jupyter never runs) and
  emits a generated zensical.yml from mkdocs.yml, which Zensical reads natively.
- mkdocs.yml: drop the now-unused mkdocs-jupyter plugin (the build script
  handles notebooks); the Material-compatible theme, custom_dir overrides
  (Microsoft Clarity + cookie consent) and mkdocstrings API reference are all
  honored by Zensical.
- docs/stylesheets/extra.css: style the nbconvert-rendered notebook output.
- NOTICE: attribute Zensical (MIT) and nbconvert (BSD-3 Clause); drop the
  MkDocs and mkdocs-jupyter notices, keep mkdocstrings and pymdown-extensions.

Verified locally: 104 pages build, all 18 sample notebooks render, the API
reference and brand styling are intact.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: improve Zensical mobile homepage

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Omri Mendels <omri374@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Sharon Hart
2026-06-28 11:05:34 +03:00
committed by GitHub
parent 23fc1177c9
commit 1c44a0fded
7 changed files with 483 additions and 233 deletions

View File

@@ -1,5 +1,10 @@
name: Release Documentation
# Builds the docs with Zensical (the successor to Material for MkDocs) via
# scripts/zensical_build.py and publishes the result to GitHub Pages. The script
# pre-converts the notebook samples with nbconvert because Zensical does not run
# the mkdocs-jupyter plugin.
on:
workflow_dispatch:
@@ -8,28 +13,34 @@ permissions:
jobs:
github-pages-release:
name: Push to github pages
name: Build with Zensical and push to GitHub Pages
runs-on: ubuntu-slim
permissions:
contents: write # Required for pushing to gh-pages branch
contents: write # Required for pushing to the gh-pages branch
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.0
with:
fetch-depth: 0 # Fetch all history for proper gh-pages deployment
persist-credentials: true # So that the token is available for pushing
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.x'
python-version: '3.12'
- name: Install dependencies
run: |
pip install -r docs/requirements-docs.txt
- name: Deploy docs
- name: Build docs
run: |
mkdocs gh-deploy
python scripts/zensical_build.py build
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4.1.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./site

4
.gitignore vendored
View File

@@ -99,8 +99,10 @@ poetry.lock
# Rope project settings
.ropeproject
# mkdocs documentation
# Zensical docs build (scripts/zensical_build.py)
/site
/zensical.yml
.zensical-build/
# mypy
.mypy_cache/

View File

@@ -5,28 +5,29 @@ This project incorporates components from the projects listed below. The origina
--------------------------------------------
MkDocs
MkDocs License (BSD)
Copyright © 2014, Tom Christie. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******
Zensical
MIT License
Copyright © 2016-2021 Martin Donath
Copyright (c) 2025-2026 Zensical and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
*******
@@ -50,213 +51,42 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*******
mkdocs-jupyter
nbconvert
BSD 3-Clause License
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Copyright (c) 2001-2015, IPython Development Team
Copyright (c) 2015-, Jupyter Development Team
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
All rights reserved.
1. Definitions.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******
pymdown-extensions
PyMdown Extensions
@@ -269,4 +99,3 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE

View File

@@ -1,12 +1,24 @@
mkdocs
mkdocs-material
mkdocs-jupyter
# Dependencies for building the Presidio documentation with Zensical, the
# successor to Material for MkDocs. Build with scripts/zensical_build.py, which
# pre-converts the notebook samples and emits a generated zensical.yml. See that
# script's module docstring for the full flow.
#
# pip install -r docs/requirements-docs.txt
# python scripts/zensical_build.py # build to ./site
# python scripts/zensical_build.py serve # local preview
#
# Zensical does not run MkDocs plugins, so mkdocs / mkdocs-material /
# mkdocs-jupyter are intentionally absent. nbconvert renders the notebooks and
# mkdocstrings-python + the presidio packages back the API reference pages.
zensical
nbconvert
mkdocstrings-python
pymdown-extensions
markdown
mkdocstrings-python
pyyaml
presidio_analyzer
presidio_anonymizer
presidio_image_redactor
presidio_structured
pygments>=2.10
black
black

View File

@@ -269,6 +269,8 @@ body,
========================================================================= */
.md-footer-meta {
background: var(--md-footer-bg-color--dark);
--md-default-fg-color: var(--md-primary-bg-color);
--md-default-fg-color--light: color-mix(in srgb, var(--md-primary-bg-color) 72%, transparent);
}
/* =========================================================================
@@ -378,7 +380,7 @@ body,
align-items: center;
}
.dps-hero__mascot {
.md-typeset .dps-hero__mascot {
width: 100%;
max-width: 140px;
height: auto;
@@ -456,13 +458,44 @@ body,
gap: 1.5rem;
}
.dps-hero__media { order: -1; }
.dps-hero__mascot { max-width: 150px; }
.md-typeset .dps-hero__mascot { max-width: 120px; }
.dps-hero__tagline { margin-inline: auto; }
.dps-hero__cta { justify-content: center; }
.dps-hero__body { align-items: center; }
.dps-features { grid-template-columns: 1fr; }
}
@media screen and (max-width: 44.984375em) {
.dps-hero {
grid-template-columns: 1fr;
text-align: center;
gap: 0;
margin: 0.25rem 0 1.35rem;
padding: 0.9rem 1rem;
border-radius: 18px;
}
.dps-hero__media {
display: none;
}
.dps-hero__body {
align-items: center;
gap: 0.25rem;
}
.md-typeset .dps-hero__title {
font-size: clamp(2.25rem, 13vw, 3.2rem);
line-height: 0.95;
}
.md-typeset .dps-hero__byline {
font-size: clamp(0.75rem, 3.8vw, 0.95rem);
line-height: 1.25;
white-space: normal;
}
}
/* =========================================================================
11. Motion preferences
========================================================================= */
@@ -475,3 +508,84 @@ body,
transition-duration: 0.001ms !important;
}
}
/* =========================================================================
12. Converted notebook content (Jupyter → Markdown under Zensical)
mkdocs-jupyter used to ship Jupyter's own CSS. Zensical renders the
notebook Markdown natively, so pandas tables, figures and cell outputs
are styled here with the brand tokens — matching the scheme light + dark.
========================================================================= */
/* Responsive images: safety net so wide plots / DICOM frames never overflow */
.md-typeset img {
max-width: 100%;
height: auto;
}
/* Notebook figure outputs (matplotlib / DICOM): frame them so they read as
intentional and stay legible against the dark canvas. */
.md-typeset img[alt="png"],
.md-typeset img[alt="jpeg"],
.md-typeset img[alt="svg"] {
display: block;
margin: 1.3rem auto;
padding: 0.65rem;
background: #ffffff;
border: 1px solid var(--dps-border);
border-radius: 12px;
box-shadow: var(--dps-shadow);
}
/* pandas DataFrame tables: they ship as <table class="dataframe" border="1">.
Reset the legacy bordered look, adopt the site table treatment, and scroll
horizontally so wide frames never break the layout on narrow viewports. */
.md-typeset table.dataframe {
display: block;
width: max-content;
max-width: 100%;
overflow-x: auto;
border: 1px solid var(--dps-border);
border-radius: 12px;
border-collapse: separate;
border-spacing: 0;
font-size: 0.78rem;
line-height: 1.4;
box-shadow: 0 0 0 1px var(--dps-border);
-webkit-overflow-scrolling: touch;
}
.md-typeset table.dataframe th,
.md-typeset table.dataframe td {
border: 0;
border-bottom: 1px solid var(--dps-border);
padding: 0.4rem 0.85rem;
text-align: left;
white-space: nowrap;
vertical-align: top;
}
.md-typeset table.dataframe thead th {
background: color-mix(in srgb, var(--md-primary-fg-color) 7%, var(--dps-card));
color: var(--md-default-fg-color);
font-weight: 600;
border-bottom: 2px solid color-mix(in srgb, var(--md-accent-fg-color) 35%, var(--dps-border));
}
.md-typeset table.dataframe tbody th {
font-weight: 600;
color: var(--md-default-fg-color--light);
background: color-mix(in srgb, var(--md-default-fg-color) 3%, transparent);
}
.md-typeset table.dataframe tbody tr:nth-child(even) > td {
background: color-mix(in srgb, var(--md-default-fg-color) 3%, transparent);
}
.md-typeset table.dataframe tbody tr:hover > td {
background: var(--md-accent-fg-color--transparent);
}
.md-typeset table.dataframe tr:last-child td,
.md-typeset table.dataframe tr:last-child th {
border-bottom: 0;
}

View File

@@ -2,6 +2,7 @@ site_name: Presidio
site_url: https://data-privacy-stack.github.io/presidio
site_description: PII anonymization for text, images, and structured data.
site_author: Data Privacy Stack
copyright: '<a href="https://dataprivacystack.org/">Data Privacy Stack</a>'
repo_url: https://github.com/data-privacy-stack/presidio/
edit_uri: ""
@@ -219,8 +220,6 @@ plugins:
modules: false
filters:
- "!^_"
- mkdocs-jupyter:
ignore_h1_titles: True
copyright: >
Maintained by

283
scripts/zensical_build.py Normal file
View File

@@ -0,0 +1,283 @@
#!/usr/bin/env python3
"""Build (or serve) the Presidio documentation with Zensical.
Zensical (https://zensical.org) is the successor to Material for MkDocs. It can
read an existing ``mkdocs.yml`` natively, but it does **not** run MkDocs plugins
- it only supports a curated set of plugins that it shims into Python-Markdown
extensions. Presidio's docs nav still includes notebook samples (``*.ipynb``),
so this script pre-converts them to Markdown before invoking Zensical.
Notebook support is accepted on the Zensical backlog but unscheduled:
- request : https://github.com/zensical/zensical/issues/52
- backlog : https://github.com/zensical/backlog/issues/9
Until it ships, this script bridges the gap by pre-converting every ``*.ipynb``
referenced in the navigation to Markdown with ``nbconvert``. Zensical then
renders those pages natively, with full theme, navigation, search and table of
contents integration (unlike dumping standalone HTML into the build output).
Steps
-----
1. Load ``mkdocs.yml`` and collect every ``*.ipynb`` referenced in the nav.
2. Mirror ``docs/`` into ``.zensical-build/docs/`` so the real source tree
stays untouched (Material-compatible, minimal diff, easy to rebase).
3. In the staging copy, convert each notebook to Markdown (images land in a
sibling ``<name>_files/`` directory that Zensical copies as static assets)
and rewrite in-repo ``*.ipynb`` links to the converted ``*.md`` pages so
they navigate instead of downloading the raw notebook.
4. Emit a generated ``zensical.yml`` whose ``docs_dir`` is the staging tree,
whose nav points at the ``.md`` files, and which no longer enables the
``mkdocs-jupyter`` plugin.
5. Run ``zensical build`` (default) or ``zensical serve`` against that config.
Executables are taken from ``PATH`` by default. In a split-virtualenv setup the
following environment variables can override them:
JUPYTER_BIN path to the ``jupyter`` entry point (default: ``jupyter``)
ZENSICAL_BIN path to the ``zensical`` entry point (default: ``zensical``)
ZENSICAL_PYTHONPATH extra PYTHONPATH for the zensical process (e.g. so that
the documented Python packages and ``mkdocstrings`` are
importable for the API reference)
Usage
-----
python scripts/zensical_build.py # build
python scripts/zensical_build.py serve -a :8001 # serve (extra args passed on)
"""
from __future__ import annotations
import os
import re
import shutil
import subprocess
import sys
import urllib.parse
from pathlib import Path
import yaml
REPO_ROOT = Path(__file__).resolve().parent.parent
MKDOCS_CONFIG = REPO_ROOT / "mkdocs.yml"
GENERATED_CONFIG = REPO_ROOT / "zensical.yml"
# Everything Zensical-specific is generated into this staging tree so the real
# ``docs/`` stays pristine (Material-compatible, minimal diff, easy rebase).
STAGING_DIR = REPO_ROOT / ".zensical-build"
STAGING_DOCS = STAGING_DIR / "docs"
STAGING_DOCS_REL = STAGING_DOCS.relative_to(REPO_ROOT).as_posix()
# --------------------------------------------------------------------------- #
# YAML loading
# --------------------------------------------------------------------------- #
class _TolerantLoader(yaml.SafeLoader):
"""SafeLoader that tolerates MkDocs' ``!!python/name:`` tags.
We only need to *read* the config to discover notebooks; the generated
config is produced by string transformation so these tags round-trip
untouched.
"""
def _ignore_python_name(loader, suffix, node): # noqa: ANN001, ARG001
return None
_TolerantLoader.add_multi_constructor(
"tag:yaml.org,2002:python/name:", _ignore_python_name
)
def _collect_notebooks(nav) -> list[str]:
"""Return every ``*.ipynb`` path referenced in a MkDocs nav structure."""
found: list[str] = []
def walk(node) -> None:
if isinstance(node, str):
if node.endswith(".ipynb"):
found.append(node)
elif isinstance(node, list):
for item in node:
walk(item)
elif isinstance(node, dict):
for value in node.values():
walk(value)
walk(nav)
# De-duplicate while preserving order.
return list(dict.fromkeys(found))
# --------------------------------------------------------------------------- #
# Staging
# --------------------------------------------------------------------------- #
def _stage_docs(src: Path, dst: Path) -> None:
"""Mirror the real docs tree into the staging dir (source stays untouched)."""
if shutil.which("rsync"):
dst.mkdir(parents=True, exist_ok=True)
subprocess.run(["rsync", "-a", "--delete", f"{src}/", f"{dst}/"], check=True)
else:
if dst.exists():
shutil.rmtree(dst)
shutil.copytree(src, dst)
# --------------------------------------------------------------------------- #
# Notebook conversion
# --------------------------------------------------------------------------- #
def _convert_notebooks(notebooks: list[str], docs_dir: Path) -> list[Path]:
"""Convert notebooks to Markdown in place; return generated paths."""
jupyter = os.environ.get("JUPYTER_BIN", "jupyter")
generated: list[Path] = []
for rel in notebooks:
src = docs_dir / rel
if not src.exists():
print(f" ! skip (missing): {rel}", file=sys.stderr)
continue
out_md = src.with_suffix(".md")
print(f" - {rel} -> {out_md.relative_to(docs_dir)}")
subprocess.run(
[
jupyter,
"nbconvert",
"--to",
"markdown",
"--output",
src.stem,
"--output-dir",
str(src.parent),
str(src),
],
check=True,
stdout=subprocess.DEVNULL,
)
generated.append(out_md)
return generated
# --------------------------------------------------------------------------- #
# Link rewriting
# --------------------------------------------------------------------------- #
# Matches the URL of a Markdown link ``](...ipynb`` or an HTML ``href="...ipynb``.
# The match stops at ``.ipynb`` so any ``#fragment`` is preserved untouched.
_LINK_RE = re.compile(r"""(\]\(|href=["'])([^)"'#?\s]+\.ipynb)""")
def _rewrite_links(docs_dir: Path, notebooks: list[str]) -> None:
"""Point in-repo ``*.ipynb`` links at the converted ``*.md`` pages.
MkDocs + mkdocs-jupyter rewrote notebook links to the rendered page; under
Zensical the page is the converted Markdown, so links to a notebook that we
converted must end in ``.md`` (otherwise the browser downloads the raw
``.ipynb``). Only links that resolve to a converted notebook are touched —
external (GitHub) links and links to non-converted notebooks are left alone.
"""
converted = {(docs_dir / rel).resolve() for rel in notebooks}
total = 0
for md in docs_dir.rglob("*.md"):
base = md.parent
text = md.read_text(encoding="utf-8")
hits = 0
def repl(match: re.Match) -> str:
nonlocal hits
prefix, url = match.group(1), match.group(2)
if url.startswith(("http://", "https://", "//", "mailto:")):
return match.group(0)
target = (base / urllib.parse.unquote(url)).resolve()
if target in converted:
hits += 1
return prefix + url[: -len(".ipynb")] + ".md"
return match.group(0)
new = _LINK_RE.sub(repl, text)
if hits:
md.write_text(new, encoding="utf-8")
total += hits
print(f" rewrote {total} notebook link(s) to .md")
# --------------------------------------------------------------------------- #
# Generated config
# --------------------------------------------------------------------------- #
def _write_generated_config(raw: str, notebooks: list[str]) -> None:
"""Rewrite nav .ipynb -> .md, drop mkdocs-jupyter, point at the staging docs."""
text = raw
for rel in notebooks:
text = text.replace(rel, rel[: -len(".ipynb")] + ".md")
# Remove the ``- mkdocs-jupyter:`` plugin entry and its indented children.
text = re.sub(
r"[ \t]*-[ \t]*mkdocs-jupyter:[^\n]*\n(?:[ \t]+[^\n]*\n)*",
"",
text,
)
# Build from the staging copy (converted notebooks + rewritten links).
if re.search(r"(?m)^docs_dir:.*$", text):
text = re.sub(r"(?m)^docs_dir:.*$", f"docs_dir: {STAGING_DOCS_REL}", text)
else:
text = f"docs_dir: {STAGING_DOCS_REL}\n" + text
banner = (
"# AUTOGENERATED by scripts/zensical_build.py - DO NOT EDIT.\n"
"# Source of truth is mkdocs.yml. Notebook nav entries have been\n"
"# converted to Markdown and the mkdocs-jupyter plugin removed,\n"
"# because Zensical does not run MkDocs plugins (backlog: "
"zensical/backlog#9).\n"
f"# docs_dir points at the generated staging tree ({STAGING_DOCS_REL}).\n\n"
)
GENERATED_CONFIG.write_text(banner + text, encoding="utf-8")
print(f" wrote {GENERATED_CONFIG.relative_to(REPO_ROOT)}")
# --------------------------------------------------------------------------- #
# Main
# --------------------------------------------------------------------------- #
def main(argv: list[str]) -> int:
"""Stage docs, convert notebooks, write zensical.yml, and build or serve."""
command = argv[0] if argv else "build"
passthrough = argv[1:]
if command not in {"build", "serve"}:
# Treat unknown first arg as a passthrough flag for `build`.
command, passthrough = "build", argv
raw = MKDOCS_CONFIG.read_text(encoding="utf-8")
config = yaml.load(raw, Loader=_TolerantLoader)
docs_dir = REPO_ROOT / config.get("docs_dir", "docs")
notebooks = _collect_notebooks(config.get("nav", []))
print(f"Staging docs -> {STAGING_DOCS_REL}")
_stage_docs(docs_dir, STAGING_DOCS)
print(f"Found {len(notebooks)} notebook(s) in nav.")
_convert_notebooks(notebooks, STAGING_DOCS)
print("Rewriting notebook links...")
_rewrite_links(STAGING_DOCS, notebooks)
print("Generating Zensical config...")
_write_generated_config(raw, notebooks)
zensical = os.environ.get("ZENSICAL_BIN", "zensical")
env = os.environ.copy()
extra_pp = os.environ.get("ZENSICAL_PYTHONPATH")
if extra_pp:
env["PYTHONPATH"] = os.pathsep.join(
p for p in (extra_pp, env.get("PYTHONPATH", "")) if p
)
cmd = [zensical, command, "-f", str(GENERATED_CONFIG), *passthrough]
print(f"Running: {' '.join(cmd)}")
return subprocess.run(cmd, env=env, check=False).returncode
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))