From 1c44a0fded1c8f1b1a83a2a1369899d8f0034567 Mon Sep 17 00:00:00 2001 From: Sharon Hart Date: Sun, 28 Jun 2026 11:05:34 +0300 Subject: [PATCH] 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 Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/workflows/release-docs.yml | 29 ++- .gitignore | 4 +- docs/NOTICE | 257 +++++--------------------- docs/requirements-docs.txt | 22 ++- docs/stylesheets/extra.css | 118 +++++++++++- mkdocs.yml | 3 +- scripts/zensical_build.py | 283 +++++++++++++++++++++++++++++ 7 files changed, 483 insertions(+), 233 deletions(-) create mode 100644 scripts/zensical_build.py diff --git a/.github/workflows/release-docs.yml b/.github/workflows/release-docs.yml index 304a19fb3..686793d48 100644 --- a/.github/workflows/release-docs.yml +++ b/.github/workflows/release-docs.yml @@ -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 diff --git a/.gitignore b/.gitignore index 8686e4a0b..6640e593b 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/docs/NOTICE b/docs/NOTICE index 7fcc64b3d..107baea2a 100644 --- a/docs/NOTICE +++ b/docs/NOTICE @@ -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 - diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 34d0d2800..88221070e 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -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 \ No newline at end of file +black diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index edb6c280c..d738b36a5 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -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 . + 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; +} diff --git a/mkdocs.yml b/mkdocs.yml index 69c78803e..560808cb7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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: 'Data Privacy Stack' 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 diff --git a/scripts/zensical_build.py b/scripts/zensical_build.py new file mode 100644 index 000000000..d3fdd4082 --- /dev/null +++ b/scripts/zensical_build.py @@ -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 ``_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:]))