├── .gitattributes ├── .github └── workflows │ ├── build.yaml │ ├── docs.yaml │ ├── nightly_lock.yaml │ └── test.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE.txt ├── README.md ├── nbsite ├── __init__.py ├── __main__.py ├── __version.py ├── _shared_static │ ├── alert.css │ ├── dataframe.css │ ├── gallery.css │ ├── holoviz-icon-white.svg │ ├── hv-sidebar-dropdown.css │ ├── js │ │ └── goatcounter.js │ ├── mystnb.css │ ├── nbsite.css │ ├── notebook.css │ └── scroller.css ├── _shared_templates │ ├── copyright-last-updated.html │ ├── github-stars-button.html │ ├── hv-sidebar-dropdown.html │ └── sidebar-nav-bs-alt.html ├── analytics │ └── __init__.py ├── cmd.py ├── gallery │ ├── __init__.py │ ├── gen.py │ └── thumbnailer.py ├── ipystartup.py ├── nb_interactivity_warning │ └── __init__.py ├── nbbuild.py ├── paramdoc.py ├── pyodide │ ├── ServiceHandler.js │ ├── ServiceWorker.js │ ├── WebWorker.js │ ├── WorkerHandler.js │ ├── __init__.py │ ├── _static │ │ ├── run_cell.js │ │ └── runbutton.css │ └── site.webmanifest ├── scripts │ ├── __init__.py │ ├── _clean_dist_html.py │ └── _fix_links.py ├── shared_conf.py ├── templates │ ├── basic │ │ ├── README.md │ │ ├── conf.py │ │ └── index.rst │ └── holoviz │ │ ├── README.md │ │ ├── about.rst │ │ ├── conf.py │ │ └── index.rst ├── tests │ ├── __init__.py │ ├── conftest.py │ ├── test_cmd.py │ ├── test_nbbuild.py │ └── test_util.py └── util.py ├── pixi.toml ├── pyproject.toml ├── scripts ├── conda │ ├── build.sh │ └── recipe │ │ └── meta.yaml └── sync_git_tags.py └── site ├── README.md ├── doc ├── _static │ ├── css │ │ └── custom.css │ ├── favicon.ico │ └── nbsite-logo.png ├── conf.py ├── development.md ├── index.md ├── playground │ ├── api │ │ └── index.rst │ ├── example_gallery │ │ ├── section1 │ │ │ └── thumbnails │ │ │ │ └── thing1.png │ │ └── section2 │ │ │ └── thumbnails │ │ │ └── thing2.png │ ├── gallery_backends │ │ ├── option1 │ │ │ └── thumbnails │ │ │ │ └── example1.png │ │ └── option2 │ │ │ └── thumbnails │ │ │ └── example2.png │ ├── index.md │ ├── markdown │ │ ├── example.md │ │ └── index.md │ ├── mystmdnb │ │ ├── holoviz.md │ │ └── index.md │ ├── mystnb │ │ ├── holoviz.ipynb │ │ └── index.ipynb │ ├── notebook_directive │ │ ├── example2.rst │ │ └── index.md │ ├── pyodide │ │ ├── holoviz.md │ │ └── index.md │ └── rst │ │ ├── example.rst │ │ └── index.rst └── user_guide │ ├── extra_extensions.md │ ├── gallery.md │ ├── index.md │ ├── param_doc.md │ └── usage.md ├── dodo.py ├── dummy_package ├── build │ └── lib │ │ └── dummy │ │ ├── __init__.py │ │ └── module.py ├── dummy │ ├── __init__.py │ └── module.py └── pyproject.toml ├── examples └── playground │ ├── example_gallery │ ├── section1 │ │ └── thing1.ipynb │ └── section2 │ │ └── thing2.ipynb │ ├── gallery_backends │ ├── option1 │ │ └── example1.ipynb │ └── option2 │ │ └── example2.ipynb │ └── notebook_directive │ ├── example.ipynb │ ├── example2.ipynb │ ├── holoviz.ipynb │ └── preexecuted.ipynb ├── future └── mix │ ├── holoviz.ipynb │ └── index.md └── requirements.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | nbsite/__init__.py export-subst 2 | setup.cfg export-subst 3 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: packages 2 | on: 3 | push: 4 | tags: 5 | - 'v[0-9]+.[0-9]+.[0-9]+' 6 | - 'v[0-9]+.[0-9]+.[0-9]+a[0-9]+' 7 | - 'v[0-9]+.[0-9]+.[0-9]+b[0-9]+' 8 | - 'v[0-9]+.[0-9]+.[0-9]+rc[0-9]+' 9 | # Dry-run only 10 | workflow_dispatch: 11 | inputs: 12 | target: 13 | description: Build target 14 | type: choice 15 | options: 16 | - dryrun 17 | required: true 18 | default: dryrun 19 | schedule: 20 | - cron: '0 20 * * SUN' 21 | 22 | env: 23 | PYTHON_VERSION: "3.9" 24 | PACKAGE: "nbsite" 25 | 26 | defaults: 27 | run: 28 | shell: bash -e {0} 29 | 30 | jobs: 31 | waiting_room: 32 | name: Waiting Room 33 | runs-on: ubuntu-latest 34 | needs: [conda_build, pip_install] 35 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') 36 | environment: 37 | name: publish 38 | steps: 39 | - run: echo "All builds have finished, have been approved, and ready to publish" 40 | 41 | pixi_lock: 42 | name: Pixi lock 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: holoviz-dev/holoviz_tasks/pixi_lock@v0 46 | 47 | conda_build: 48 | name: Build Conda Packages 49 | needs: [pixi_lock] 50 | runs-on: 'ubuntu-latest' 51 | env: 52 | CONDA_UPLOAD_TOKEN: ${{ secrets.CONDA_UPLOAD_TOKEN }} 53 | steps: 54 | - uses: holoviz-dev/holoviz_tasks/pixi_install@v0 55 | with: 56 | environments: "build" 57 | download-data: false 58 | install: false 59 | - name: conda build 60 | run: pixi run -e build build-conda 61 | - uses: actions/upload-artifact@v4 62 | if: always() 63 | with: 64 | name: conda 65 | path: dist/*.tar.bz2 66 | if-no-files-found: error 67 | 68 | conda-publish: 69 | name: Publish Conda 70 | runs-on: ubuntu-latest 71 | needs: [conda_build, waiting_room] 72 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') 73 | defaults: 74 | run: 75 | shell: bash -el {0} 76 | steps: 77 | - uses: actions/download-artifact@v4 78 | with: 79 | name: conda 80 | path: dist/ 81 | - name: Set environment variables 82 | run: | 83 | echo "TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 84 | echo "CONDA_FILE=$(ls dist/*.tar.bz2)" >> $GITHUB_ENV 85 | - uses: conda-incubator/setup-miniconda@v3 86 | with: 87 | miniconda-version: "latest" 88 | channels: "conda-forge" 89 | - name: conda setup 90 | run: | 91 | conda install -y anaconda-client 92 | - name: conda dev upload 93 | if: contains(env.TAG, 'a') || contains(env.TAG, 'b') || contains(env.TAG, 'rc') 94 | run: | 95 | anaconda --token ${{ secrets.CONDA_UPLOAD_TOKEN }} upload --user pyviz --label=dev --label=tooling_dev $CONDA_FILE 96 | - name: conda main upload 97 | if: (!(contains(env.TAG, 'a') || contains(env.TAG, 'b') || contains(env.TAG, 'rc'))) 98 | run: | 99 | anaconda --token ${{ secrets.CONDA_UPLOAD_TOKEN }} upload --user pyviz --label=dev --label=main --label=tooling_dev $CONDA_FILE 100 | 101 | pip_build: 102 | name: Build PyPI 103 | needs: [pixi_lock] 104 | runs-on: 'ubuntu-latest' 105 | steps: 106 | - uses: holoviz-dev/holoviz_tasks/pixi_install@v0 107 | with: 108 | environments: "build" 109 | download-data: false 110 | install: false 111 | - name: Build package 112 | run: pixi run -e build build-pip 113 | - uses: actions/upload-artifact@v4 114 | if: always() 115 | with: 116 | name: pip 117 | path: dist/ 118 | if-no-files-found: error 119 | 120 | pip_install: 121 | name: Install PyPI 122 | runs-on: "ubuntu-latest" 123 | needs: [pip_build] 124 | steps: 125 | - uses: actions/setup-python@v5 126 | with: 127 | python-version: ${{ env.PYTHON_VERSION }} 128 | - uses: actions/download-artifact@v4 129 | with: 130 | name: pip 131 | path: dist/ 132 | - name: Install package 133 | run: python -m pip install dist/*.whl 134 | - name: Import package 135 | run: python -c "import $PACKAGE; print($PACKAGE._version.__version__)" 136 | 137 | pip_publish: 138 | name: Publish to PyPI 139 | runs-on: ubuntu-latest 140 | needs: [pip_build, waiting_room] 141 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') 142 | permissions: 143 | id-token: write 144 | steps: 145 | - uses: actions/download-artifact@v4 146 | with: 147 | name: pip 148 | path: dist/ 149 | - name: Publish to PyPi 150 | uses: pypa/gh-action-pypi-publish@release/v1 151 | with: 152 | user: ${{ secrets.PPU }} 153 | password: ${{ secrets.PPP }} 154 | packages-dir: dist/ 155 | -------------------------------------------------------------------------------- /.github/workflows/docs.yaml: -------------------------------------------------------------------------------- 1 | name: docs 2 | on: 3 | push: 4 | tags: 5 | - 'v[0-9]+.[0-9]+.[0-9]+' 6 | - 'v[0-9]+.[0-9]+.[0-9]+a[0-9]+' 7 | - 'v[0-9]+.[0-9]+.[0-9]+b[0-9]+' 8 | - 'v[0-9]+.[0-9]+.[0-9]+rc[0-9]+' 9 | workflow_dispatch: 10 | inputs: 11 | target: 12 | description: 'Site to build and deploy, or dry-run' 13 | type: choice 14 | options: 15 | - dev 16 | - main 17 | - dryrun 18 | required: true 19 | default: dryrun 20 | schedule: 21 | - cron: '0 20 * * SUN' 22 | 23 | defaults: 24 | run: 25 | shell: bash -e {0} 26 | 27 | env: 28 | PYTHON_VERSION: "3.11" 29 | DESC: "Documentation build" 30 | 31 | jobs: 32 | pixi_lock: 33 | name: Pixi lock 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: holoviz-dev/holoviz_tasks/pixi_lock@v0 37 | 38 | build_docs: 39 | name: Build Documentation 40 | needs: [pixi_lock] 41 | runs-on: 'ubuntu-latest' 42 | timeout-minutes: 120 43 | outputs: 44 | tag: ${{ steps.vars.outputs.tag }} 45 | env: 46 | DISPLAY: ":99.0" 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | steps: 49 | - uses: holoviz-dev/holoviz_tasks/pixi_install@v0 50 | with: 51 | environments: docs 52 | - name: Build documentation 53 | run: pixi run -e docs docs-build 54 | - uses: actions/upload-artifact@v4 55 | if: always() 56 | with: 57 | name: docs 58 | if-no-files-found: error 59 | path: site/builtdocs 60 | - name: Set output 61 | id: vars 62 | run: | 63 | echo "Deploying from ref %{GITHUB_REF#refs/*/}" 64 | echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT 65 | - name: report failure 66 | if: failure() 67 | run: cat /tmp/sphinx-*.log | tail -n 100 68 | 69 | docs_publish: 70 | name: Publish Documentation 71 | runs-on: "ubuntu-latest" 72 | needs: [build_docs] 73 | env: 74 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 75 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 76 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 77 | AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }} 78 | steps: 79 | - uses: actions/download-artifact@v4 80 | with: 81 | name: docs 82 | path: site/builtdocs/ 83 | - name: Set output 84 | id: vars 85 | run: echo "tag=${{ needs.docs_build.outputs.tag }}" >> $GITHUB_OUTPUT 86 | - name: Deploy dev 87 | if: | 88 | (github.event_name == 'workflow_dispatch' && github.event.inputs.target == 'dev') || 89 | (github.event_name == 'push' && (contains(steps.vars.outputs.tag, 'a') || contains(steps.vars.outputs.tag, 'b') || contains(steps.vars.outputs.tag, 'rc'))) 90 | uses: peaceiris/actions-gh-pages@v4 91 | with: 92 | personal_token: ${{ secrets.ACCESS_TOKEN }} 93 | external_repository: holoviz-dev/nbsite-dev 94 | publish_dir: ./site/builtdocs 95 | force_orphan: true 96 | - name: Deploy main 97 | if: | 98 | (github.event_name == 'workflow_dispatch' && github.event.inputs.target == 'main') || 99 | (github.event_name == 'push' && !(contains(steps.vars.outputs.tag, 'a') || contains(steps.vars.outputs.tag, 'b') || contains(steps.vars.outputs.tag, 'rc'))) 100 | uses: peaceiris/actions-gh-pages@v4 101 | with: 102 | github_token: ${{ secrets.GITHUB_TOKEN }} 103 | publish_dir: ./site/builtdocs 104 | cname: nbsite.holoviz.org 105 | force_orphan: true 106 | -------------------------------------------------------------------------------- /.github/workflows/nightly_lock.yaml: -------------------------------------------------------------------------------- 1 | name: nightly_lock 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | env: 8 | PACKAGE: "nbsite" 9 | 10 | jobs: 11 | pixi_lock: 12 | if: ${{ !github.event.repository.fork }} 13 | name: Pixi lock 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 5 16 | steps: 17 | - uses: holoviz-dev/holoviz_tasks/pixi_lock@v0 18 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | # things not included 2 | # language 3 | # notifications - no email notifications set up 4 | name: tests 5 | on: 6 | push: 7 | branches: 8 | - main 9 | pull_request: 10 | branches: 11 | - '*' 12 | workflow_dispatch: 13 | inputs: 14 | target: 15 | description: "How much of the test suite to run" 16 | type: choice 17 | default: default 18 | options: 19 | - default 20 | - full 21 | cache: 22 | description: "Use cache" 23 | type: boolean 24 | default: true 25 | schedule: 26 | - cron: '0 20 * * SUN' 27 | 28 | concurrency: 29 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 30 | cancel-in-progress: true 31 | 32 | defaults: 33 | run: 34 | shell: bash -e {0} 35 | 36 | env: 37 | DISPLAY: ":99.0" 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | COV: "--cov=./nbsite --cov-report=xml" 40 | 41 | jobs: 42 | pre_commit: 43 | name: Run pre-commit 44 | runs-on: "ubuntu-latest" 45 | steps: 46 | - uses: holoviz-dev/holoviz_tasks/pre-commit@v0 47 | 48 | setup: 49 | name: Setup workflow 50 | runs-on: ubuntu-latest 51 | permissions: 52 | pull-requests: read 53 | outputs: 54 | code_change: ${{ steps.filter.outputs.code }} 55 | matrix: ${{ env.MATRIX }} 56 | steps: 57 | - uses: actions/checkout@v4 58 | if: github.event_name != 'pull_request' 59 | - name: Check for code changes 60 | uses: dorny/paths-filter@v3 61 | id: filter 62 | with: 63 | filters: | 64 | code: 65 | - 'nbsite/**' 66 | - 'pixi.toml' 67 | - 'pyproject.toml' 68 | - '.github/workflows/test.yaml' 69 | - name: Set matrix option 70 | run: | 71 | if [[ '${{ github.event_name }}' == 'workflow_dispatch' ]]; then 72 | OPTION=${{ github.event.inputs.target }} 73 | elif [[ '${{ github.event_name }}' == 'schedule' ]]; then 74 | OPTION="full" 75 | elif [[ '${{ github.event_name }}' == 'push' && '${{ github.ref_type }}' == 'tag' ]]; then 76 | OPTION="full" 77 | else 78 | OPTION="default" 79 | fi 80 | echo "MATRIX_OPTION=$OPTION" >> $GITHUB_ENV 81 | - name: Set test matrix with 'default' option 82 | if: env.MATRIX_OPTION == 'default' 83 | run: | 84 | MATRIX=$(jq -nsc '{ 85 | "os": ["ubuntu-latest", "macos-latest", "windows-latest"], 86 | "environment": ["test-39", "test-312"], 87 | }') 88 | echo "MATRIX=$MATRIX" >> $GITHUB_ENV 89 | - name: Set test matrix with 'full' option 90 | if: env.MATRIX_OPTION == 'full' 91 | run: | 92 | MATRIX=$(jq -nsc '{ 93 | "os": ["ubuntu-latest", "macos-latest", "windows-latest"], 94 | "environment": ["test-39", "test-310", "test-311", "test-312", "test-313"], 95 | }') 96 | echo "MATRIX=$MATRIX" >> $GITHUB_ENV 97 | 98 | pixi_lock: 99 | name: Pixi lock 100 | runs-on: ubuntu-latest 101 | steps: 102 | - uses: holoviz-dev/holoviz_tasks/pixi_lock@v0 103 | with: 104 | cache: ${{ github.event.inputs.cache == 'true' || github.event.inputs.cache == '' }} 105 | 106 | unit_test_suite: 107 | name: unit:${{ matrix.environment }}:${{ matrix.os }} 108 | needs: [pre_commit, setup, pixi_lock] 109 | runs-on: ${{ matrix.os }} 110 | if: needs.setup.outputs.code_change == 'true' 111 | strategy: 112 | fail-fast: false 113 | matrix: ${{ fromJson(needs.setup.outputs.matrix) }} 114 | timeout-minutes: 60 115 | steps: 116 | - uses: holoviz-dev/holoviz_tasks/pixi_install@v0 117 | with: 118 | environments: ${{ matrix.environment }} 119 | - name: Test Unit 120 | run: | 121 | pixi run -e ${{ matrix.environment }} test-unit $COV 122 | - name: Test Examples 123 | run: | 124 | pixi run -e ${{ matrix.environment }} test-example 125 | 126 | result_test_suite: 127 | name: result:test 128 | needs: [unit_test_suite] 129 | if: always() 130 | runs-on: ubuntu-latest 131 | steps: 132 | - name: check for failures 133 | if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') 134 | run: echo job failed && exit 1 135 | 136 | build_docs: 137 | name: Documentation 138 | needs: [pre_commit] 139 | runs-on: 'ubuntu-latest' 140 | timeout-minutes: 120 141 | defaults: 142 | run: 143 | shell: bash -l {0} 144 | env: 145 | DESC: "Documentation build" 146 | steps: 147 | - uses: holoviz-dev/holoviz_tasks/pixi_install@v0 148 | with: 149 | environments: docs 150 | - name: Build docs 151 | run: pixi run -e docs docs-build 152 | - name: Deploy dev 153 | if: ${{ github.event.repository.fork == false && github.event.pull_request.head.repo.fork == false }} 154 | uses: peaceiris/actions-gh-pages@v4 155 | with: 156 | personal_token: ${{ secrets.ACCESS_TOKEN }} 157 | external_repository: pyviz-dev/nbsite-dev 158 | publish_dir: ./site/builtdocs 159 | force_orphan: true 160 | - name: clean up 161 | run: pixi run -e docs docs-clean 162 | - name: check no leftover 163 | run: git diff --quiet || exit 1 164 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | #*# 3 | *~ 4 | *.egg 5 | *.egg-info 6 | *.swp 7 | *.DS_Store 8 | *.so 9 | *.o 10 | *.out 11 | *.lock 12 | *.doit* 13 | pip-wheel-metadata 14 | .tox 15 | .venv/ 16 | 17 | # ipython 18 | */.ipynb_checkpoints 19 | 20 | # autover 21 | */.version 22 | 23 | # nbsite 24 | # this dir contains the whole website and should not be checked in on main 25 | site/builtdocs/ 26 | 27 | site/.venv/ 28 | 29 | build/ 30 | dist 31 | 32 | # pixi 33 | .pixi 34 | pixi.lock 35 | 36 | # mystnb 37 | jupyter_execute/ 38 | 39 | # version 40 | _version.py 41 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_stages: [pre-commit] 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v5.0.0 5 | hooks: 6 | - id: check-builtin-literals 7 | - id: check-case-conflict 8 | - id: check-docstring-first 9 | - id: check-executables-have-shebangs 10 | - id: check-toml 11 | - id: check-json 12 | - id: detect-private-key 13 | - id: end-of-file-fixer 14 | exclude: \.min\.js$ 15 | - id: trailing-whitespace 16 | - repo: https://github.com/astral-sh/ruff-pre-commit 17 | rev: v0.11.4 18 | hooks: 19 | - id: ruff 20 | files: nbsite/ 21 | - repo: https://github.com/pycqa/isort 22 | rev: 6.0.1 23 | hooks: 24 | - id: isort 25 | name: isort (python) 26 | - repo: https://github.com/codespell-project/codespell 27 | rev: v2.4.1 28 | hooks: 29 | - id: codespell 30 | additional_dependencies: 31 | - tomli 32 | - repo: https://github.com/hoxbro/prettier-pre-commit 33 | rev: v3.5.3 34 | hooks: 35 | - id: prettier 36 | types_or: [css] 37 | ci: 38 | autofix_prs: false 39 | autoupdate_schedule: quarterly 40 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-18, PyViz 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of cube-explorer nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | ----------------- 4 | 5 | # NBSite: Build a tested, sphinx-based website from notebooks 6 | 7 | | | | 8 | | --- | --- | 9 | | Build Status | [![Build Status](https://github.com/holoviz-dev/nbsite/workflows/tests/badge.svg)](https://github.com/holoviz-dev/nbsite/actions?query=workflow%3Atests) 10 | | Coverage | [![codecov](https://codecov.io/gh/holoviz-dev/nbsite/branch/main/graph/badge.svg)](https://codecov.io/gh/holoviz-dev/nbsite) | 11 | | Latest dev release | [![Github tag](https://img.shields.io/github/tag/holoviz-dev/nbsite.svg?label=tag&colorB=11ccbb)](https://github.com/holoviz-dev/nbsite/tags) [![dev-site](https://img.shields.io/website-up-down-green-red/https/holoviz-dev.github.io/nbsite-dev.svg?label=dev%20website)](https://holoviz-dev.github.io/nbsite-dev/)| 12 | | Latest release | [![Github release](https://img.shields.io/github/release/holoviz-dev/nbsite.svg?label=tag&colorB=11ccbb)](https://github.com/holoviz-dev/nbsite/releases) [![PyPI version](https://img.shields.io/pypi/v/nbsite.svg?colorB=cc77dd)](https://pypi.python.org/pypi/nbsite) [![nbsite version](https://img.shields.io/conda/v/pyviz/nbsite.svg?colorB=4488ff&style=flat)](https://anaconda.org/pyviz/nbsite) [![conda-forge version](https://img.shields.io/conda/v/conda-forge/nbsite.svg?label=conda%7Cconda-forge&colorB=4488ff)](https://anaconda.org/conda-forge/nbsite) [![defaults version](https://img.shields.io/conda/v/anaconda/nbsite.svg?label=conda%7Cdefaults&style=flat&colorB=4488ff)](https://anaconda.org/anaconda/nbsite) | 13 | | Docs | [![gh-pages](https://img.shields.io/github/last-commit/pyviz/nbsite/gh-pages.svg)](https://github.com/pyviz/nbsite/tree/gh-pages) [![site](https://img.shields.io/website-up-down-green-red/https/nbsite.pyviz.org.svg)](https://nbsite.pyviz.org) | 14 | 15 | --- 16 | 17 | **DISCLAIMER** 18 | 19 | NBSite is a tool supporting the developers of the [HoloViz](https://holoviz/org) project. As such it is tailored to their use case, workflow, and **breaking changes may occur at any time**. We suggest that before using NBSite you investigate alternatives such as [MyST-NB](https://myst-nb.readthedocs.io) or [nbsphinx](https://nbsphinx.readthedocs.io/). If you select NBSite anyway, we recommend that you pin its version. 20 | 21 | --- 22 | 23 | NBSite lets you build a website from a set of notebooks plus a minimal 24 | amount of config. The idea behind NBSite is that notebooks can simultaneously be documentation (things you want to tell people about), examples (a starting point for people to run and use themselves), and test cases (see nbsmoke). 25 | 26 | ## Sites built with NBSite 27 | 28 | Non exhaustive list of sites built with NBSite (as of November 2022): 29 | 30 | - [Panel](https://panel.holoviz.org/) 31 | - [hvPlot](https://hvplot.holoviz.org/) 32 | - [HoloViews](https://holoviews.org/) 33 | - [GeoViews](https://geoviews.org/) 34 | - [Datashader](https://datashader.org/) 35 | - [Lumen](https://lumen.holoviz.org/) 36 | - [Colorcet](https://colorcet.holoviz.org/) 37 | - [Param](https://param.holoviz.org/) 38 | - [HoloViz.org](https://holoviz.org/) 39 | - [examples.pyviz.org](https://examples.pyviz.org/) 40 | - [PyViz.org](https://pyviz.org/) 41 | 42 | ## About HoloViz 43 | 44 | NBSite is part of the HoloViz initiative for making Python-based visualization tools work well together. 45 | See [holoviz.org](https://holoviz.org/) for related packages that you can use with NBSite and 46 | [status.holoviz.org](https://status.holoviz.org/) for the current status of each HoloViz project. 47 | -------------------------------------------------------------------------------- /nbsite/__init__.py: -------------------------------------------------------------------------------- 1 | from .__version import __version__ # noqa: F401 2 | -------------------------------------------------------------------------------- /nbsite/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import inspect 3 | 4 | from .cmd import build, generate_rst, init 5 | 6 | 7 | def _add_common_args(parser,*names): 8 | common = { 9 | '--project-root': dict(type=str,help='defaults to current working directory',default=''), 10 | '--examples': dict(type=str,help='if relative, should be relative to project-root',default='examples'), 11 | '--doc': dict(type=str,help='if relative, should be relative to project-root',default='doc'), 12 | '--overwrite': dict(action='store_true', help='whether to overwrite any files [DANGEROUS]') 13 | } 14 | for name in names: 15 | parser.add_argument(name,**common[name]) 16 | 17 | def _set_defaults(parser,fn): 18 | parser.set_defaults(func=lambda args: fn( **{k: getattr(args,k) for k in vars(args) if k!='func'} )) 19 | 20 | def main(args=None): 21 | parser = argparse.ArgumentParser(description="nbsite commands") 22 | subparsers = parser.add_subparsers(title='available commands') 23 | 24 | init_parser = subparsers.add_parser("init", help=inspect.getdoc(init)) 25 | _add_common_args(init_parser,'--project-root','--doc') 26 | init_parser.add_argument('--theme', type=str, help='sphinx theme to use in template', choices=['holoviz', ''], default='') 27 | _set_defaults(init_parser,init) 28 | 29 | generaterst_parser = subparsers.add_parser("generate-rst", help=inspect.getdoc(generate_rst)) 30 | _add_common_args(generaterst_parser,'--project-root','--doc','--examples', '--overwrite') 31 | generaterst_parser.add_argument('--project-name', type=str, help='name of project') 32 | generaterst_parser.add_argument('--host',type=str,help='host to use when generating notebook links',default='GitHub') 33 | generaterst_parser.add_argument('--org',type=str,help='github organization',default='') 34 | generaterst_parser.add_argument('--repo',type=str,help='name of repo',default='') 35 | generaterst_parser.add_argument('--branch',type=str,help='branch to point to in notebook links',default='main') 36 | generaterst_parser.add_argument('--offset',type=int,help='number of cells to delete from top of notebooks',default=0) 37 | generaterst_parser.add_argument('--nblink',type=str,help='where to place notebook links',choices=['bottom', 'top', 'both', 'none'], default='bottom') 38 | generaterst_parser.add_argument('--binder',type=str,help='where to place binder link',choices=['bottom', 'top', 'both', 'none'], default='none') 39 | generaterst_parser.add_argument('--skip',type=str,help='notebooks to skip running; comma separated case insensitive re to match',default='') 40 | generaterst_parser.add_argument('--keep-numbers',action='store_true',help='whether to keep the leading numbers of notebook URLs and titles') 41 | generaterst_parser.add_argument('--disable-interactivity-warning',action='store_true',help='whether to disable interactivity warnings') 42 | _set_defaults(generaterst_parser,generate_rst) 43 | 44 | build_parser = subparsers.add_parser("build", help=inspect.getdoc(build)) 45 | build_parser.add_argument('--what',type=str,help='type of output to generate',default='html') 46 | build_parser.add_argument('--project-name', type=str, help='name of project', default='') 47 | build_parser.add_argument('--org',type=str,help='github organization',default='') 48 | build_parser.add_argument('--host',type=str,help='host to use when generating notebook links',default='GitHub') 49 | build_parser.add_argument('--repo',type=str,help='name of repo',default='') 50 | build_parser.add_argument('--branch',type=str,help='branch to point to in notebook links',default='main') 51 | build_parser.add_argument('--binder',type=str,help='where to place binder link',choices=['bottom', 'top', 'both', 'none'], default='none') 52 | build_parser.add_argument('--disable-parallel',action=argparse.BooleanOptionalAction,help='whether to disable building the docs in parallel') 53 | 54 | build_parser.add_argument('--output',type=str,help='where to place output',default="builtdocs") 55 | _add_common_args(build_parser,'--project-root','--doc','--examples', '--overwrite') 56 | build_parser.add_argument('--examples-assets',type=str,default="assets", 57 | help='dir in which assets for examples are located - if relative, should be relative to project-root') 58 | build_parser.add_argument('--clean-dry-run',action='store_true',help='whether to not actually delete files from output (useful for uploading)') 59 | build_parser.add_argument('--inspect-links',action='store_true',help='whether to not to print all links') 60 | _set_defaults(build_parser,build) 61 | 62 | args = parser.parse_args() 63 | return args.func(args) if hasattr(args,'func') else parser.error("must supply command to run") 64 | 65 | if __name__ == "__main__": 66 | main() 67 | -------------------------------------------------------------------------------- /nbsite/__version.py: -------------------------------------------------------------------------------- 1 | """Define the package version. 2 | 3 | Called __version.py as setuptools_scm will create a _version.py 4 | 5 | """ 6 | 7 | import os.path 8 | 9 | PACKAGE = "nbsite" 10 | 11 | try: 12 | # For performance reasons on imports, avoid importing setuptools_scm 13 | # if not in a .git folder 14 | if os.path.exists(os.path.join(os.path.dirname(__file__), "..", ".git")): 15 | # If setuptools_scm is installed (e.g. in a development environment with 16 | # an editable install), then use it to determine the version dynamically. 17 | from setuptools_scm import get_version 18 | 19 | # This will fail with LookupError if the package is not installed in 20 | # editable mode or if Git is not installed. 21 | __version__ = get_version(root="..", relative_to=__file__) 22 | else: 23 | raise FileNotFoundError 24 | except (ImportError, LookupError, FileNotFoundError): 25 | # As a fallback, use the version that is hard-coded in the file. 26 | try: 27 | # __version__ was added in _version in setuptools-scm 7.0.0, we rely on 28 | # the hopefully stable version variable. 29 | from ._version import version as __version__ 30 | except (ModuleNotFoundError, ImportError): 31 | # Either _version doesn't exist (ModuleNotFoundError) or version isn't 32 | # in _version (ImportError). ModuleNotFoundError is a subclass of 33 | # ImportError, let's be explicit anyway. 34 | 35 | # Try something else: 36 | from importlib.metadata import PackageNotFoundError, version 37 | 38 | try: 39 | __version__ = version(PACKAGE) 40 | except PackageNotFoundError: 41 | # The user is probably trying to run this without having installed 42 | # the package. 43 | __version__ = "0.0.0+unknown" 44 | 45 | __all__ = ("__version__",) 46 | -------------------------------------------------------------------------------- /nbsite/_shared_static/alert.css: -------------------------------------------------------------------------------- 1 | .alert { 2 | padding: 0.75rem 1.25rem; 3 | margin-bottom: 1rem; 4 | border: 1px solid transparent; 5 | border-radius: 0.25rem; 6 | } 7 | 8 | .alert-heading { 9 | color: inherit; 10 | } 11 | 12 | .alert-link { 13 | font-weight: bold; 14 | } 15 | 16 | .alert-dismissible .close { 17 | position: relative; 18 | top: -0.75rem; 19 | right: -1.25rem; 20 | padding: 0.75rem 1.25rem; 21 | color: inherit; 22 | } 23 | 24 | .alert-success { 25 | background-color: #dff0d8; 26 | border-color: #d0e9c6; 27 | color: #3c763d; 28 | } 29 | 30 | .alert-success hr { 31 | border-top-color: #c1e2b3; 32 | } 33 | 34 | .alert-success .alert-link { 35 | color: #2b542c; 36 | } 37 | 38 | .alert-info { 39 | background-color: #d9edf7; 40 | border-color: #bcdff1; 41 | color: #31708f; 42 | } 43 | 44 | .alert-info hr { 45 | border-top-color: #a6d5ec; 46 | } 47 | 48 | .alert-info .alert-link { 49 | color: #245269; 50 | } 51 | 52 | .alert-warning { 53 | background-color: #fcf8e3; 54 | border-color: #faf2cc; 55 | color: #8a6d3b; 56 | } 57 | 58 | .alert-warning hr { 59 | border-top-color: #f7ecb5; 60 | } 61 | 62 | .alert-warning .alert-link { 63 | color: #66512c; 64 | } 65 | 66 | .alert-danger { 67 | background-color: #f2dede; 68 | border-color: #ebcccc; 69 | color: #a94442; 70 | } 71 | 72 | .alert-danger hr { 73 | border-top-color: #e4b9b9; 74 | } 75 | 76 | .alert-danger .alert-link { 77 | color: #843534; 78 | } 79 | -------------------------------------------------------------------------------- /nbsite/_shared_static/dataframe.css: -------------------------------------------------------------------------------- 1 | table.dataframe { 2 | margin-left: auto; 3 | margin-right: auto; 4 | border: none; 5 | border-collapse: collapse; 6 | border-spacing: 0; 7 | font-size: 12px; 8 | table-layout: auto; 9 | width: 100%; 10 | } 11 | 12 | .dataframe tr, 13 | .dataframe th, 14 | .dataframe td { 15 | text-align: right; 16 | vertical-align: middle; 17 | padding: 0.5em 0.5em; 18 | line-height: normal; 19 | white-space: normal; 20 | max-width: none; 21 | border: none; 22 | } 23 | 24 | .dataframe tbody { 25 | display: table-row-group; 26 | vertical-align: middle; 27 | border-color: inherit; 28 | } 29 | 30 | .dataframe tbody tr:nth-child(odd) { 31 | background-color: var(--pst-color-surface, #f5f5f5); 32 | color: var(--pst-color-text-base); 33 | } 34 | 35 | .dataframe thead { 36 | border-bottom: 1px solid var(--pst-color-border); 37 | vertical-align: bottom; 38 | } 39 | 40 | .dataframe tbody tr:hover { 41 | background-color: #e1f5fe; 42 | color: #000000; 43 | cursor: pointer; 44 | } 45 | 46 | :host { 47 | overflow: auto; 48 | padding-right: 1px; 49 | } 50 | -------------------------------------------------------------------------------- /nbsite/_shared_static/gallery.css: -------------------------------------------------------------------------------- 1 | ul.tab { 2 | list-style-type: none; 3 | padding: 0; 4 | overflow: hidden; 5 | } 6 | 7 | ul.tab li { 8 | float: left; 9 | padding: 0; 10 | } 11 | 12 | ul.tab li label { 13 | padding: 6px; 14 | border: 1px solid #ccc; 15 | display: inline-block; 16 | } 17 | 18 | ul.tab li input[type="radio"] { 19 | opacity: 0; 20 | width: 1px; 21 | height: 1px; 22 | } 23 | 24 | ul.tab li input[type="radio"]:checked ~ label { 25 | background: var(--pst-color-primary); 26 | color: white; 27 | } 28 | 29 | dl.dl-horizontal { 30 | padding-left: 50px; 31 | } 32 | -------------------------------------------------------------------------------- /nbsite/_shared_static/holoviz-icon-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml 210 | -------------------------------------------------------------------------------- /nbsite/_shared_static/hv-sidebar-dropdown.css: -------------------------------------------------------------------------------- 1 | /* Sidebar styling for the HoloViz dropdown section */ 2 | .hv-sb-dd { 3 | margin-bottom: 0.5em; 4 | } 5 | 6 | .hv-sb-dd .btn-group { 7 | width: 100%; 8 | } 9 | 10 | .hv-sb-dd .hv-sb-dd-value { 11 | text-align: start; 12 | font-size: 0.9rem; 13 | } 14 | 15 | .hv-sb-dd .btn { 16 | background-color: var(--pst-color-surface); 17 | color: var(--pst-color-text-base); 18 | } 19 | 20 | .hv-sb-dd a.btn:hover { 21 | color: var(--pst-color-link-hover); 22 | text-decoration: underline; 23 | text-decoration-thickness: max(3px, 0.1875rem, 0.12em); 24 | } 25 | 26 | .hv-sb-dd .dropdown-toggle-split { 27 | border-left: solid 1px lightgray; 28 | } 29 | 30 | .hv-sb-dd .dropdown-menu { 31 | width: 100%; 32 | background-color: var(--pst-color-surface); 33 | color: var(--pst-color-text-base); 34 | } 35 | 36 | .hv-sb-dd .dropdown-item { 37 | font-size: 0.8rem; 38 | } 39 | 40 | .hv-sb-dd .hv-icon { 41 | font-size: 0.75em; 42 | margin-left: 0.3em; 43 | } 44 | -------------------------------------------------------------------------------- /nbsite/_shared_static/js/goatcounter.js: -------------------------------------------------------------------------------- 1 | // GoatCounter: https://www.goatcounter.com 2 | // This file is released under the ISC license: https://opensource.org/licenses/ISC 3 | ;(function() { 4 | 'use strict'; 5 | 6 | if (window.goatcounter && window.goatcounter.vars) // Compatibility with very old version; do not use. 7 | window.goatcounter = window.goatcounter.vars 8 | else 9 | window.goatcounter = window.goatcounter || {} 10 | 11 | // Load settings from data-goatcounter-settings. 12 | var s = document.querySelector('script[data-goatcounter]') 13 | if (s && s.dataset.goatcounterSettings) { 14 | try { var set = JSON.parse(s.dataset.goatcounterSettings) } 15 | catch (err) { console.error('invalid JSON in data-goatcounter-settings: ' + err) } 16 | for (var k in set) 17 | if (['no_onload', 'no_events', 'allow_local', 'allow_frame', 'path', 'title', 'referrer', 'event'].indexOf(k) > -1) 18 | window.goatcounter[k] = set[k] 19 | } 20 | 21 | var enc = encodeURIComponent 22 | 23 | // Get all data we're going to send off to the counter endpoint. 24 | var get_data = function(vars) { 25 | var data = { 26 | p: (vars.path === undefined ? goatcounter.path : vars.path), 27 | r: (vars.referrer === undefined ? goatcounter.referrer : vars.referrer), 28 | t: (vars.title === undefined ? goatcounter.title : vars.title), 29 | e: !!(vars.event || goatcounter.event), 30 | s: [window.screen.width, window.screen.height, (window.devicePixelRatio || 1)], 31 | b: is_bot(), 32 | q: location.search, 33 | } 34 | 35 | var rcb, pcb, tcb // Save callbacks to apply later. 36 | if (typeof(data.r) === 'function') rcb = data.r 37 | if (typeof(data.t) === 'function') tcb = data.t 38 | if (typeof(data.p) === 'function') pcb = data.p 39 | 40 | if (is_empty(data.r)) data.r = document.referrer 41 | if (is_empty(data.t)) data.t = document.title 42 | if (is_empty(data.p)) data.p = get_path() 43 | 44 | if (rcb) data.r = rcb(data.r) 45 | if (tcb) data.t = tcb(data.t) 46 | if (pcb) data.p = pcb(data.p) 47 | return data 48 | } 49 | 50 | // Check if a value is "empty" for the purpose of get_data(). 51 | var is_empty = function(v) { return v === null || v === undefined || typeof(v) === 'function' } 52 | 53 | // See if this looks like a bot; there is some additional filtering on the 54 | // backend, but these properties can't be fetched from there. 55 | var is_bot = function() { 56 | // Headless browsers are probably a bot. 57 | var w = window, d = document 58 | if (w.callPhantom || w._phantom || w.phantom) 59 | return 150 60 | if (w.__nightmare) 61 | return 151 62 | if (d.__selenium_unwrapped || d.__webdriver_evaluate || d.__driver_evaluate) 63 | return 152 64 | if (navigator.webdriver) 65 | return 153 66 | return 0 67 | } 68 | 69 | // Object to urlencoded string, starting with a ?. 70 | var urlencode = function(obj) { 71 | var p = [] 72 | for (var k in obj) 73 | if (obj[k] !== '' && obj[k] !== null && obj[k] !== undefined && obj[k] !== false) 74 | p.push(enc(k) + '=' + enc(obj[k])) 75 | return '?' + p.join('&') 76 | } 77 | 78 | // Show a warning in the console. 79 | var warn = function(msg) { 80 | if (console && 'warn' in console) 81 | console.warn('goatcounter: ' + msg) 82 | } 83 | 84 | // Get the endpoint to send requests to. 85 | var get_endpoint = function() { 86 | var s = document.querySelector('script[data-goatcounter]') 87 | if (s && s.dataset.goatcounter) 88 | return s.dataset.goatcounter 89 | return (goatcounter.endpoint || window.counter) // counter is for compat; don't use. 90 | } 91 | 92 | // Get current path. 93 | var get_path = function() { 94 | var loc = location, 95 | c = document.querySelector('link[rel="canonical"][href]') 96 | if (c) { // May be relative or point to different domain. 97 | var a = document.createElement('a') 98 | a.href = c.href 99 | if (a.hostname.replace(/^www\./, '') === location.hostname.replace(/^www\./, '')) 100 | loc = a 101 | } 102 | return (loc.pathname + loc.search) || '/' 103 | } 104 | 105 | // Run function after DOM is loaded. 106 | var on_load = function(f) { 107 | if (document.body === null) 108 | document.addEventListener('DOMContentLoaded', function() { f() }, false) 109 | else 110 | f() 111 | } 112 | 113 | // Filter some requests that we (probably) don't want to count. 114 | goatcounter.filter = function() { 115 | if ('visibilityState' in document && document.visibilityState === 'prerender') 116 | return 'visibilityState' 117 | if (!goatcounter.allow_frame && location !== parent.location) 118 | return 'frame' 119 | if (!goatcounter.allow_local && location.hostname.match(/(localhost$|^127\.|^10\.|^172\.(1[6-9]|2[0-9]|3[0-1])\.|^192\.168\.|^0\.0\.0\.0$)/)) 120 | return 'localhost' 121 | if (!goatcounter.allow_local && location.protocol === 'file:') 122 | return 'localfile' 123 | if (localStorage && localStorage.getItem('skipgc') === 't') 124 | return 'disabled with #toggle-goatcounter' 125 | return false 126 | } 127 | 128 | // Get URL to send to GoatCounter. 129 | window.goatcounter.url = function(vars) { 130 | var data = get_data(vars || {}) 131 | if (data.p === null) // null from user callback. 132 | return 133 | data.rnd = Math.random().toString(36).substr(2, 5) // Browsers don't always listen to Cache-Control. 134 | 135 | var endpoint = get_endpoint() 136 | if (!endpoint) 137 | return warn('no endpoint found') 138 | 139 | return endpoint + urlencode(data) 140 | } 141 | 142 | // Count a hit. 143 | window.goatcounter.count = function(vars) { 144 | var f = goatcounter.filter() 145 | if (f) 146 | return warn('not counting because of: ' + f) 147 | 148 | var url = goatcounter.url(vars) 149 | if (!url) 150 | return warn('not counting because path callback returned null') 151 | 152 | if (navigator.sendBeacon) 153 | navigator.sendBeacon(url) 154 | else { // Fallback for (very) old browsers. 155 | var img = document.createElement('img') 156 | img.src = url 157 | img.style.position = 'absolute' // Affect layout less. 158 | img.style.bottom = '0px' 159 | img.style.width = '1px' 160 | img.style.height = '1px' 161 | img.loading = 'eager' 162 | img.setAttribute('alt', '') 163 | img.setAttribute('aria-hidden', 'true') 164 | 165 | var rm = function() { if (img && img.parentNode) img.parentNode.removeChild(img) } 166 | img.addEventListener('load', rm, false) 167 | document.body.appendChild(img) 168 | } 169 | } 170 | 171 | // Get a query parameter. 172 | window.goatcounter.get_query = function(name) { 173 | var s = location.search.substr(1).split('&') 174 | for (var i = 0; i < s.length; i++) 175 | if (s[i].toLowerCase().indexOf(name.toLowerCase() + '=') === 0) 176 | return s[i].substr(name.length + 1) 177 | } 178 | 179 | // Track click events. 180 | window.goatcounter.bind_events = function() { 181 | if (!document.querySelectorAll) // Just in case someone uses an ancient browser. 182 | return 183 | 184 | var send = function(elem) { 185 | return function() { 186 | goatcounter.count({ 187 | event: true, 188 | path: (elem.dataset.goatcounterClick || elem.name || elem.id || ''), 189 | title: (elem.dataset.goatcounterTitle || elem.title || (elem.innerHTML || '').substr(0, 200) || ''), 190 | referrer: (elem.dataset.goatcounterReferrer || elem.dataset.goatcounterReferral || ''), 191 | }) 192 | } 193 | } 194 | 195 | Array.prototype.slice.call(document.querySelectorAll("*[data-goatcounter-click]")).forEach(function(elem) { 196 | if (elem.dataset.goatcounterBound) 197 | return 198 | var f = send(elem) 199 | elem.addEventListener('click', f, false) 200 | elem.addEventListener('auxclick', f, false) // Middle click. 201 | elem.dataset.goatcounterBound = 'true' 202 | }) 203 | } 204 | 205 | // Add a "visitor counter" frame or image. 206 | window.goatcounter.visit_count = function(opt) { 207 | on_load(function() { 208 | opt = opt || {} 209 | opt.type = opt.type || 'html' 210 | opt.append = opt.append || 'body' 211 | opt.path = opt.path || get_path() 212 | opt.attr = opt.attr || {width: '200', height: (opt.no_branding ? '60' : '80')} 213 | 214 | opt.attr['src'] = get_endpoint() + 'er/' + enc(opt.path) + '.' + enc(opt.type) + '?' 215 | if (opt.no_branding) opt.attr['src'] += '&no_branding=1' 216 | if (opt.style) opt.attr['src'] += '&style=' + enc(opt.style) 217 | if (opt.start) opt.attr['src'] += '&start=' + enc(opt.start) 218 | if (opt.end) opt.attr['src'] += '&end=' + enc(opt.end) 219 | 220 | var tag = {png: 'img', svg: 'img', html: 'iframe'}[opt.type] 221 | if (!tag) 222 | return warn('visit_count: unknown type: ' + opt.type) 223 | 224 | if (opt.type === 'html') { 225 | opt.attr['frameborder'] = '0' 226 | opt.attr['scrolling'] = 'no' 227 | } 228 | 229 | var d = document.createElement(tag) 230 | for (var k in opt.attr) 231 | d.setAttribute(k, opt.attr[k]) 232 | 233 | var p = document.querySelector(opt.append) 234 | if (!p) 235 | return warn('visit_count: append not found: ' + opt.append) 236 | p.appendChild(d) 237 | }) 238 | } 239 | 240 | // Make it easy to skip your own views. 241 | if (location.hash === '#toggle-goatcounter') { 242 | if (localStorage.getItem('skipgc') === 't') { 243 | localStorage.removeItem('skipgc', 't') 244 | alert('GoatCounter tracking is now ENABLED in this browser.') 245 | } 246 | else { 247 | localStorage.setItem('skipgc', 't') 248 | alert('GoatCounter tracking is now DISABLED in this browser until ' + location + ' is loaded again.') 249 | } 250 | } 251 | 252 | if (!goatcounter.no_onload) 253 | on_load(function() { 254 | // 1. Page is visible, count request. 255 | // 2. Page is not yet visible; wait until it switches to 'visible' and count. 256 | // See #487 257 | if (!('visibilityState' in document) || document.visibilityState === 'visible') 258 | goatcounter.count() 259 | else { 260 | var f = function(e) { 261 | if (document.visibilityState !== 'visible') 262 | return 263 | document.removeEventListener('visibilitychange', f) 264 | goatcounter.count() 265 | } 266 | document.addEventListener('visibilitychange', f) 267 | } 268 | 269 | if (!goatcounter.no_events) 270 | goatcounter.bind_events() 271 | }) 272 | })(); 273 | -------------------------------------------------------------------------------- /nbsite/_shared_static/mystnb.css: -------------------------------------------------------------------------------- 1 | /* Whole cell */ 2 | div.container.cell { 3 | padding-left: 0; 4 | margin-bottom: 1em; 5 | } 6 | 7 | .cell_output .output pre, 8 | .cell_input pre { 9 | margin: 0px; 10 | } 11 | 12 | /* Input cells */ 13 | div.cell div.cell_input { 14 | padding-left: 0em; 15 | padding-right: 0em; 16 | border: 1px #ccc solid; 17 | background-color: #f7f7f7; 18 | border-left-color: green; 19 | border-left-width: medium; 20 | } 21 | 22 | div.cell_input > div, 23 | div.cell_output div.output > div.highlight { 24 | margin: 0em !important; 25 | border: none !important; 26 | } 27 | 28 | /* All cell outputs */ 29 | .cell_output { 30 | padding-left: 1em; 31 | padding-right: 0em; 32 | margin-top: 1em; 33 | } 34 | 35 | /* Outputs from jupyter_sphinx overrides to remove extra CSS */ 36 | div.section div.jupyter_container { 37 | padding: 0.4em; 38 | margin: 0 0 0.4em 0; 39 | background-color: none; 40 | border: none; 41 | -moz-box-shadow: none; 42 | -webkit-box-shadow: none; 43 | box-shadow: none; 44 | } 45 | 46 | /* Text outputs from cells */ 47 | .cell_output .output.text_plain, 48 | .cell_output .output.traceback, 49 | .cell_output .output.stream, 50 | .cell_output .output.stderr { 51 | background: #fcfcfc; 52 | margin-top: 1em; 53 | margin-bottom: 0em; 54 | box-shadow: none; 55 | } 56 | 57 | .cell_output .output.text_plain, 58 | .cell_output .output.stream, 59 | .cell_output .output.stderr { 60 | border: 1px solid #f7f7f7; 61 | } 62 | 63 | .cell_output .output.stderr { 64 | background: #fdd; 65 | } 66 | 67 | .cell_output .output.traceback { 68 | border: 1px solid #ffd6d6; 69 | } 70 | 71 | /* Math align to the left */ 72 | .cell_output .MathJax_Display { 73 | text-align: left !important; 74 | } 75 | 76 | /* Pandas tables. Pulled from the Jupyter / nbsphinx CSS */ 77 | div.cell_output table { 78 | border: none; 79 | border-collapse: collapse; 80 | border-spacing: 0; 81 | color: black; 82 | font-size: 1em; 83 | table-layout: fixed; 84 | } 85 | div.cell_output thead { 86 | border-bottom: 1px solid black; 87 | vertical-align: bottom; 88 | } 89 | div.cell_output tr, 90 | div.cell_output th, 91 | div.cell_output td { 92 | text-align: right; 93 | vertical-align: middle; 94 | padding: 0.5em 0.5em; 95 | line-height: normal; 96 | white-space: normal; 97 | max-width: none; 98 | border: none; 99 | } 100 | div.cell_output th { 101 | font-weight: bold; 102 | } 103 | div.cell_output tbody tr:nth-child(odd) { 104 | background: #f5f5f5; 105 | } 106 | div.cell_output tbody tr:hover { 107 | background: rgba(66, 165, 245, 0.2); 108 | } 109 | 110 | /* Inline text from `paste` operation */ 111 | 112 | span.pasted-text { 113 | font-weight: bold; 114 | } 115 | 116 | span.pasted-inline img { 117 | max-height: 2em; 118 | } 119 | 120 | tbody span.pasted-inline img { 121 | max-height: none; 122 | } 123 | 124 | /* Font colors for translated ANSI escape sequences 125 | Color values are adapted from share/jupyter/nbconvert/templates/classic/static/style.css 126 | */ 127 | div.highlight .-Color-Bold { 128 | font-weight: bold; 129 | } 130 | div.highlight .-Color[class*="-Black"] { 131 | color: #3e424d; 132 | } 133 | div.highlight .-Color[class*="-Red"] { 134 | color: #e75c58; 135 | } 136 | div.highlight .-Color[class*="-Green"] { 137 | color: #00a250; 138 | } 139 | div.highlight .-Color[class*="-Yellow"] { 140 | color: yellow; 141 | } 142 | div.highlight .-Color[class*="-Blue"] { 143 | color: #208ffb; 144 | } 145 | div.highlight .-Color[class*="-Magenta"] { 146 | color: #d160c4; 147 | } 148 | div.highlight .-Color[class*="-Cyan"] { 149 | color: #60c6c8; 150 | } 151 | div.highlight .-Color[class*="-White"] { 152 | color: #c5c1b4; 153 | } 154 | div.highlight .-Color[class*="-BGBlack"] { 155 | background-color: #3e424d; 156 | } 157 | div.highlight .-Color[class*="-BGRed"] { 158 | background-color: #e75c58; 159 | } 160 | div.highlight .-Color[class*="-BGGreen"] { 161 | background-color: #00a250; 162 | } 163 | div.highlight .-Color[class*="-BGYellow"] { 164 | background-color: yellow; 165 | } 166 | div.highlight .-Color[class*="-BGBlue"] { 167 | background-color: #208ffb; 168 | } 169 | div.highlight .-Color[class*="-BGMagenta"] { 170 | background-color: #d160c4; 171 | } 172 | div.highlight .-Color[class*="-BGCyan"] { 173 | background-color: #60c6c8; 174 | } 175 | div.highlight .-Color[class*="-BGWhite"] { 176 | background-color: #c5c1b4; 177 | } 178 | -------------------------------------------------------------------------------- /nbsite/_shared_static/nbsite.css: -------------------------------------------------------------------------------- 1 | /* Align navbar item in center */ 2 | .navbar-header-items__center { 3 | margin-left: auto; 4 | margin-right: auto; 5 | } 6 | 7 | /* Ensure content fills full space */ 8 | .bd-main .bd-content .bd-article-container { 9 | max-width: unset; 10 | } 11 | 12 | /* Hide primary sidenav bottom section to avoid extra scroll */ 13 | .sidebar-primary-items__end.sidebar-primary__section { 14 | display: none; 15 | } 16 | 17 | .nav-link { 18 | white-space: nowrap; 19 | } 20 | 21 | ul.current.nav.bd-sidenav { 22 | padding: 0; 23 | } 24 | 25 | @media (max-width: 960px) { 26 | .homepage-logo { 27 | display: none; 28 | } 29 | } 30 | 31 | @media (min-width: 960px) { 32 | .bd-sidebar { 33 | max-width: 250px; 34 | } 35 | 36 | .bd-page-width { 37 | max-width: min(100%, 1600px); 38 | } 39 | } 40 | 41 | @media (min-width: 1200px) { 42 | .bd-main .bd-content .bd-article-container .bd-article { 43 | padding-left: 0; 44 | } 45 | 46 | .bd-sidebar-secondary { 47 | max-width: 250px; 48 | } 49 | } 50 | 51 | @media (min-width: 1400px) { 52 | .bd-sidebar { 53 | max-width: 300px; 54 | } 55 | 56 | .bd-main .bd-content .bd-article-container .bd-article { 57 | padding-left: 0.5rem; 58 | padding-top: 0.5rem; 59 | } 60 | } 61 | 62 | .theme-switch-button { 63 | font-size: unset; 64 | } 65 | 66 | .related a { 67 | margin-left: 0.5em; 68 | margin-right: 0.5em; 69 | } 70 | 71 | p.caption:not([role="heading"]) { 72 | text-align: center !important; 73 | font-weight: bold; 74 | } 75 | 76 | ul.parents { 77 | display: none; 78 | } 79 | 80 | .related a:hover { 81 | background: #7cb92f; 82 | } 83 | .related a[href="#"]:hover, 84 | .related a[href="index.html"]:hover { 85 | background: #9edb4f; 86 | } 87 | .related a[href="#"], 88 | .related a[href="index.html"] { 89 | background: #8dca3f; 90 | } 91 | .related a { 92 | padding: 0.5em; 93 | margin: 0; 94 | } 95 | 96 | .global-toc li a[href="#"]:hover, 97 | .global-toc li a[href="index.html"]:hover { 98 | background: #fff; 99 | } 100 | 101 | .global-toc li a[href="#"], 102 | .global-toc li a[href="index.html"] { 103 | background: #fafafa; 104 | } 105 | 106 | .global-toc li a { 107 | display: block; 108 | } 109 | 110 | .global-toc li a:hover { 111 | background: #f5f5f5; 112 | } 113 | 114 | div.body h1 { 115 | background: #beebbe; 116 | } 117 | 118 | div.body h2 { 119 | background: #c8e3c8; 120 | } 121 | 122 | div.body h3, 123 | div.body h4, 124 | div.body h5, 125 | div.body h6 { 126 | background: #d9e3d8; 127 | } 128 | 129 | div.inheritance_box { 130 | overflow: auto; 131 | } 132 | 133 | tt.docutils.literal { 134 | background: transparent; 135 | font-size: 0.9em; 136 | } 137 | 138 | dl.class { 139 | border-top: 1px solid #5a970d; 140 | padding-top: 15px; 141 | } 142 | 143 | dl.class.rm_expanded, 144 | dl.class.rm_collapsed { 145 | border-top: none; 146 | padding: 0; 147 | margin-top: 0; 148 | margin-bottom: 0; 149 | /*padding-left: 1.5em;*/ 150 | } 151 | 152 | dl.class.rm_expanded > dt:before { 153 | content: "[-]"; 154 | cursor: pointer; 155 | color: #005b81; 156 | } 157 | 158 | dl.class.rm_collapsed dt:before { 159 | content: "[+]"; 160 | cursor: pointer; 161 | color: #005b81; 162 | } 163 | 164 | dl.class.rm_expanded dd { 165 | display: block; 166 | } 167 | 168 | dl.class.rm_collapsed dd { 169 | display: none; 170 | } 171 | 172 | a.anchor-link { 173 | visibility: hidden; 174 | } 175 | 176 | /* Avoid wrapping in HoloViews extension logo block */ 177 | div.logo-block { 178 | display: inline-block; 179 | } 180 | 181 | div.bk-root { 182 | min-height: 50px; 183 | } 184 | 185 | .output { 186 | overflow-x: auto; 187 | } 188 | 189 | /* Adapted from the sphinx book theme */ 190 | main.bd-content a.headerlink { 191 | opacity: 0; 192 | margin-left: 0.2em; 193 | } 194 | 195 | main.bd-content a.headerlink:hover { 196 | background-color: transparent; 197 | color: rgba(var(--pst-color-headerlink-hover), 1); 198 | opacity: 1 !important; 199 | } 200 | 201 | main.bd-content h1:hover a.headerlink, 202 | main.bd-content h2:hover a.headerlink, 203 | main.bd-content h3:hover a.headerlink, 204 | main.bd-content h4:hover a.headerlink, 205 | main.bd-content h5:hover a.headerlink { 206 | opacity: 0.5; 207 | } 208 | /* End of copy */ 209 | 210 | /* Color of the paragraph symbol of the header link */ 211 | :root { 212 | --pst-color-headerlink: 170, 170, 170; 213 | --pst-color-headerlink-hover: 170, 170, 170; 214 | } 215 | -------------------------------------------------------------------------------- /nbsite/_shared_static/notebook.css: -------------------------------------------------------------------------------- 1 | .cell_output { 2 | padding-left: 0; 3 | } 4 | 5 | html[data-theme="dark"] .bd-content div.cell_output .text_html, 6 | html[data-theme="light"] .bd-content div.cell_output .text_html { 7 | padding: 0; 8 | } 9 | 10 | html[data-theme="dark"] .bd-content div.cell_output .text_html { 11 | background-color: var(--pst-color-background); 12 | color: var(--pst-color-text-base); 13 | } 14 | 15 | div.input_prompt { 16 | visibility: hidden; 17 | height: 0; 18 | } 19 | 20 | div.output_prompt { 21 | visibility: hidden; 22 | height: 0; 23 | } 24 | 25 | .output_subarea pre { 26 | background-color: transparent; 27 | border-style: none; 28 | margin-left: 1em; 29 | } 30 | 31 | .rendered_html code { 32 | font-size: 0.9em !important; 33 | } 34 | 35 | /* Hide cell toggle */ 36 | 37 | details.toggle-details { 38 | display: none; 39 | } 40 | 41 | button.toggle-button { 42 | display: none; 43 | } 44 | 45 | .toggle-hidden:not(.admonition) { 46 | height: 0; 47 | } 48 | 49 | .tag_hide-input { 50 | margin-bottom: 0 !important; 51 | } 52 | 53 | details.hide.above-input { 54 | display: none; 55 | } 56 | 57 | .toggle-hidden + .cell_output { 58 | margin-top: 0 !important; 59 | } 60 | -------------------------------------------------------------------------------- /nbsite/_shared_static/scroller.css: -------------------------------------------------------------------------------- 1 | /* CSS for side-scrolling admonition */ 2 | #scroller-right { 3 | position: fixed; 4 | top: 80%; 5 | right: 1%; 6 | max-width: 10%; 7 | padding: 0.8%; 8 | transform: translate(0%, -50%); 9 | border: 1px solid gray; 10 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); 11 | font-size: smaller; 12 | z-index: 1; 13 | } 14 | 15 | #scroller-right { 16 | max-width: 14%; 17 | } 18 | 19 | @media (max-width: 1200px) { 20 | #scroller-right { 21 | position: relative !important; 22 | right: unset !important; 23 | top: unset !important; 24 | max-width: 100% !important; 25 | transform: unset !important; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /nbsite/_shared_templates/copyright-last-updated.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /nbsite/_shared_templates/github-stars-button.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Support us with a star on GitHub 4 | 5 |
6 | -------------------------------------------------------------------------------- /nbsite/_shared_templates/hv-sidebar-dropdown.html: -------------------------------------------------------------------------------- 1 | {% if hv_sidebar_dropdown %} 2 |
3 |
4 | {% if 'href' in hv_sidebar_dropdown['dropdown_value'] %} 5 | {{ hv_sidebar_dropdown['dropdown_value']['text'] }} 6 | {% else %} 7 | {{ hv_sidebar_dropdown['dropdown_value']['text'] }} 8 | {% endif %} 9 | 12 | 27 |
28 |
29 | {% endif %} 30 | -------------------------------------------------------------------------------- /nbsite/_shared_templates/sidebar-nav-bs-alt.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | {# Displays the TOC-subtree for pages nested under the currently active top-level TOCtree element. #} 10 | 26 | -------------------------------------------------------------------------------- /nbsite/analytics/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Analytics extension supporting GoatCounter only. 3 | Should be upstreamed to pydata-sphinx-theme. 4 | """ 5 | 6 | from .. import __version__ as nbs_version 7 | 8 | 9 | def add_analytics(app): 10 | nbsite_analytics = app.config.nbsite_analytics 11 | if nbsite_analytics: 12 | 13 | # Process GoatCounter 14 | 15 | # goatcounter_holoviz is specific to HoloViz, remove when moving to PyData Sphinx Theme 16 | goatcounter_holoviz = nbsite_analytics.get('goatcounter_holoviz', False) 17 | if goatcounter_holoviz: 18 | hv_default = dict( 19 | goatcounter_url='https://holoviz.goatcounter.com/count', 20 | goatcounter_domain='auto', 21 | ) 22 | nbsite_analytics = dict(nbsite_analytics, **hv_default) 23 | goatcounter_url = nbsite_analytics.get('goatcounter_url') 24 | if goatcounter_url: 25 | goatcounter_domain = nbsite_analytics.get('goatcounter_domain') 26 | if goatcounter_domain: 27 | domain = "location.host" if goatcounter_domain == "auto" else f"{goatcounter_domain!r}" 28 | # See https://www.goatcounter.com/help/domains 29 | body = ( 30 | "\n window.goatcounter = {\n" 31 | " path: function(p) { return " + domain + " + p }\n" 32 | " }\n" 33 | ) 34 | app.add_js_file(None, body=body) 35 | app.add_js_file( 36 | "js/goatcounter.js", 37 | **{"loading_method": "async", "data-goatcounter": goatcounter_url} 38 | ) 39 | 40 | def setup(app): 41 | """Setup analytics (goatcounter only!) sphinx extension""" 42 | # In the Pydata Sphinx Theme the config is going to be in the theme 43 | app.add_config_value('nbsite_analytics', {}, 'html') 44 | app.connect('builder-inited', add_analytics) 45 | return {'parallel_read_safe': True, 'version': nbs_version} 46 | -------------------------------------------------------------------------------- /nbsite/gallery/__init__.py: -------------------------------------------------------------------------------- 1 | from .. import __version__ as nbs_version 2 | from .gen import DEFAULT_GALLERY_CONF, generate_gallery_rst 3 | 4 | 5 | def setup(app): 6 | """Setup sphinx-gallery sphinx extension""" 7 | app.add_config_value('nbsite_gallery_conf', DEFAULT_GALLERY_CONF, 'html') 8 | 9 | app.connect('builder-inited', generate_gallery_rst) 10 | metadata = {'parallel_read_safe': True, 11 | 'version': nbs_version} 12 | return metadata 13 | -------------------------------------------------------------------------------- /nbsite/gallery/thumbnailer.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import ast 4 | import os 5 | import subprocess 6 | import sys 7 | import tempfile 8 | 9 | from nbconvert.preprocessors import Preprocessor 10 | 11 | try: 12 | import matplotlib.pyplot as plt 13 | plt.switch_backend('agg') 14 | except ModuleNotFoundError: 15 | pass 16 | 17 | def comment_out_magics(source): 18 | """ 19 | Utility used to make sure AST parser does not choke on unrecognized 20 | magics. 21 | """ 22 | filtered = [] 23 | for line in source.splitlines(): 24 | if line.strip().startswith('%'): 25 | filtered.append('# ' + line) 26 | else: 27 | filtered.append(line) 28 | return '\n'.join(filtered) 29 | 30 | def wrap_cell_expression(source, template='{expr}'): 31 | """ 32 | If a cell ends in an expression that could be displaying a HoloViews 33 | object (as determined using the AST), wrap it with a given prefix 34 | and suffix string. 35 | 36 | If the cell doesn't end in an expression, return the source unchanged. 37 | """ 38 | cell_output_types = (ast.IfExp, ast.BoolOp, ast.BinOp, ast.Call, 39 | ast.Name, ast.Attribute) 40 | try: 41 | node = ast.parse(comment_out_magics(source)) 42 | except SyntaxError: 43 | return source 44 | filtered = source.splitlines() 45 | if node.body != []: 46 | last_expr = node.body[-1] 47 | if not isinstance(last_expr, ast.Expr): 48 | pass # Not an expression 49 | elif isinstance(last_expr.value, cell_output_types): 50 | # CAREFUL WITH UTF8! 51 | expr_end_slice = filtered[last_expr.lineno-1][:last_expr.col_offset] 52 | expr_start_slice = filtered[last_expr.lineno-1][last_expr.col_offset:] 53 | start = '\n'.join(filtered[:last_expr.lineno-1] 54 | + ([expr_end_slice] if expr_end_slice else [])) 55 | ending = '\n'.join(([expr_start_slice] if expr_start_slice else []) 56 | + filtered[last_expr.lineno:]) 57 | 58 | if ending.strip().endswith(';'): 59 | return source 60 | # BUG!! Adds newline for 'foo'; 61 | return start + '\n' + template.format(expr=ending) 62 | return source 63 | 64 | 65 | 66 | def strip_specific_magics(source, magic): 67 | """ 68 | Given the source of a cell, filter out specific cell and line magics. 69 | """ 70 | filtered=[] 71 | for line in source.splitlines(): 72 | if line.startswith(f'%{magic}'): 73 | filtered.append(line.lstrip(f'%{magic}').strip(' ')) 74 | if line.startswith(f'%%{magic}'): 75 | filtered.append(line.lstrip(f'%%{magic}').strip(' ')) 76 | else: 77 | filtered.append(line) 78 | return '\n'.join(filtered) 79 | 80 | 81 | class StripTimeMagicsProcessor(Preprocessor): 82 | """ 83 | Preprocessor to convert notebooks to Python source strips out just time 84 | magics while keeping the rest of the cell. 85 | """ 86 | 87 | def preprocess_cell(self, cell, resources, index): 88 | if cell['cell_type'] == 'code': 89 | cell['source'] = strip_specific_magics(cell['source'], 'time') 90 | return cell, resources 91 | 92 | def __call__(self, nb, resources): return self.preprocess(nb,resources) 93 | 94 | 95 | def strip_trailing_semicolons(source, function): 96 | """ 97 | Give the source of a cell, filter out lines that contain a specified 98 | function call and end in a semicolon. 99 | """ 100 | filtered=[] 101 | for line in source.splitlines(): 102 | if line.endswith(f'{function}();'): 103 | filtered.append(line[:-1]) 104 | else: 105 | filtered.append(line) 106 | return '\n'.join(filtered) 107 | 108 | 109 | class StripServableSemicolonsProcessor(Preprocessor): 110 | """ 111 | Preprocessor to convert notebooks to Python source strips out just semicolons 112 | that come after the servable function call. 113 | """ 114 | 115 | def preprocess_cell(self, cell, resources, index): 116 | if cell['cell_type'] == 'code': 117 | cell['source'] = strip_trailing_semicolons(cell['source'], 'servable') 118 | return cell, resources 119 | 120 | def __call__(self, nb, resources): return self.preprocess(nb,resources) 121 | 122 | 123 | def thumbnail(obj, basename): 124 | import os 125 | 126 | from holoviews.core import Dimensioned, Store 127 | 128 | if isinstance(obj, Dimensioned) and not os.path.isfile(basename+'.png'): 129 | Store.renderers[Store.current_backend].save(obj, basename, fmt='png') 130 | elif 'panel' in sys.modules: 131 | from panel.viewable import Viewable 132 | if isinstance(obj, Viewable) and not os.path.isfile(basename+'.png'): 133 | obj.save(basename+'.png') 134 | return obj 135 | 136 | 137 | class ThumbnailProcessor(Preprocessor): 138 | 139 | def __init__(self, basename, **kwargs): 140 | self.basename = basename 141 | super(ThumbnailProcessor, self).__init__(**kwargs) 142 | 143 | def preprocess_cell(self, cell, resources, index): 144 | if cell['cell_type'] == 'code': 145 | template = 'from nbsite.gallery.thumbnailer import thumbnail;thumbnail({{expr}}, {basename!r})' 146 | cell['source'] = wrap_cell_expression(cell['source'], 147 | template.format( 148 | basename=self.basename)) 149 | return cell, resources 150 | 151 | def __call__(self, nb, resources): return self.preprocess(nb,resources) 152 | 153 | 154 | def execute(code, cwd, env): 155 | with tempfile.NamedTemporaryFile('wb', delete=True) as f: 156 | f.write(code) 157 | f.flush() 158 | proc = subprocess.Popen([sys.executable, f.name], cwd=cwd, env=env) 159 | proc.wait() 160 | proc.kill() 161 | return proc.returncode 162 | 163 | def notebook_thumbnail(filename, subpath): 164 | from holoviews.ipython.preprocessors import ( 165 | OptsMagicProcessor, OutputMagicProcessor, StripMagicsProcessor, 166 | ) 167 | from holoviews.util.command import export_to_python 168 | 169 | basename = os.path.splitext(os.path.basename(filename))[0] 170 | dir_path = os.path.abspath(os.path.join(subpath, 'thumbnails')) 171 | absdirpath= os.path.abspath(os.path.join('.', dir_path)) 172 | if not os.path.exists(absdirpath): 173 | os.makedirs(absdirpath) 174 | 175 | preprocessors = [OptsMagicProcessor(), 176 | OutputMagicProcessor(), 177 | StripTimeMagicsProcessor(), 178 | StripServableSemicolonsProcessor(), 179 | StripMagicsProcessor(), 180 | ThumbnailProcessor(os.path.abspath(os.path.join(dir_path, basename)))] 181 | return export_to_python(filename, preprocessors) 182 | 183 | if __name__ == '__main__': 184 | files = [] 185 | abspath = os.path.abspath(sys.argv[1]) 186 | split_path = abspath.split(os.path.sep) 187 | if os.path.isdir(abspath): 188 | if 'examples' not in split_path: 189 | print('Can only thumbnail notebooks in examples/') 190 | sys.exit() 191 | subpath = os.path.sep.join(split_path[split_path.index('examples')+1:]) 192 | files = [os.path.join(abspath, f) for f in os.listdir(abspath) 193 | if f.endswith('.ipynb')] 194 | elif os.path.isfile(abspath): 195 | subpath = os.path.sep.join(split_path[split_path.index('examples')+1:-1]) 196 | files=[abspath] 197 | else: 198 | print('Path {path} does not exist'.format(path=abspath)) 199 | 200 | for f in files: 201 | print('Generating thumbnail for file {filename}'.format(filename=f)) 202 | code = notebook_thumbnail(f, subpath) 203 | try: 204 | retcode = execute(code.encode('utf8'), cwd=os.path.split(f)[0], env={}) 205 | except Exception as e: 206 | print('Failed to generate thumbnail for {filename}'.format(filename=f)) 207 | print(str(e)) 208 | -------------------------------------------------------------------------------- /nbsite/ipystartup.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | warnings.filterwarnings("ignore") 4 | 5 | try: 6 | import matplotlib as mpl 7 | mpl.use('agg') 8 | except ModuleNotFoundError: 9 | pass 10 | 11 | try: 12 | import holoviews.plotting.bokeh # noqa 13 | except Exception: 14 | pass 15 | 16 | try: 17 | import holoviews.plotting.mpl as hmpl 18 | hmpl.MPLPlot.fig_alpha = 0 19 | except Exception: 20 | pass 21 | -------------------------------------------------------------------------------- /nbsite/nb_interactivity_warning/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | nb_interactivity_warning extension adds a warning box to pages built from 3 | a Jupyter or MyST Markdown notebook, similarly to the warning added by 4 | the NotebookDirective. 5 | 6 | It is enabled by default and can be configured with: 7 | - `nb_interactivity_warning_enable = False`, to disable it 8 | - `nb_interactivity_warning_per_file = True` and adding the tag 9 | `nb-interactivity-warning` to the notebook metadata, to enable it only 10 | on specific pages 11 | 12 | This extension depends on: 13 | - nbsite/_shared_static/scroller.css (also needed for the NotebookDirective) 14 | """ 15 | 16 | from sphinx.application import Sphinx 17 | from sphinx.util.logging import getLogger 18 | 19 | from .. import __version__ as nbs_version 20 | 21 | logger = getLogger(__name__) 22 | 23 | 24 | HTML_INTERACTIVITY_WARNING = """ 25 |
26 | This web page was generated from a Jupyter notebook and not all 27 | interactivity will work on this website. 28 |
29 | """ 30 | 31 | 32 | def add_interactivity_warning_to_body( 33 | app: Sphinx, pagename: str, templatename: str, context: dict, doctree 34 | ): 35 | """ 36 | Adds custom content to context body if the page is a Jupyter Notebook. 37 | """ 38 | if not app.config.nb_interactivity_warning_enable: 39 | return 40 | 41 | if context.get("page_source_suffix") == ".ipynb" or ( 42 | context.get("page_source_suffix") == ".md" 43 | # There might be other ways to find this out, seems to work. 44 | and "kernelspec" in app.env.metadata[pagename] 45 | ): 46 | # When per-file warning is enabled, ignore files that don't have the tag. 47 | if app.config.nb_interactivity_warning_per_file: 48 | per_page_metadata = app.env.metadata.get(pagename, {}) 49 | if "nb-interactivity-warning" not in per_page_metadata.get("tags", []): 50 | return 51 | 52 | if HTML_INTERACTIVITY_WARNING not in context.get('body', ''): 53 | context['body'] += HTML_INTERACTIVITY_WARNING 54 | logger.debug( 55 | "Adding Notebook interactivity warning to page body as HTML: %s", 56 | pagename, 57 | ) 58 | 59 | def setup(app: Sphinx): 60 | app.add_config_value("nb_interactivity_warning_enable", True, "html") 61 | app.add_config_value("nb_interactivity_warning_per_file", False, "html") 62 | # Event triggered when HTML pages are built 63 | app.connect("html-page-context", add_interactivity_warning_to_body) 64 | return { 65 | "version": nbs_version, 66 | "parallel_read_safe": True, 67 | "parallel_write_safe": True, 68 | } 69 | -------------------------------------------------------------------------------- /nbsite/paramdoc.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | from functools import partial 4 | 5 | import param 6 | 7 | from param.parameterized import label_formatter 8 | 9 | param.parameterized.docstring_signature = False 10 | param.parameterized.docstring_describe_params = False 11 | 12 | # Parameter attributes which are never shown 13 | IGNORED_ATTRS = [ 14 | 'precedence', 'check_on_set', 'instantiate', 'pickle_default_value', 15 | 'watchers', 'compute_default_fn', 'doc', 'owner', 'per_instance', 16 | 'is_instance', 'name', 'time_fn', 'time_dependent', 'rx' 17 | ] 18 | 19 | # Default parameter attribute values (value not shown if it matches defaults) 20 | DEFAULT_VALUES = {'allow_None': False, 'readonly': False, 'constant': False, 'allow_refs': False, 'nested_refs': False} 21 | 22 | 23 | def param_formatter(app, what, name, obj, options, lines): 24 | if what == 'module': 25 | lines = ["start"] 26 | 27 | if what == 'class' and isinstance(obj, param.parameterized.ParameterizedMetaclass): 28 | lines.extend(['', '**Parameter Definitions**', '', '-------', '']) 29 | parameters = ['name'] 30 | mro = obj.mro()[::-1] 31 | inherited = [] 32 | for cls in mro[:-1]: 33 | if not issubclass(cls, param.Parameterized) or cls is param.Parameterized: 34 | continue 35 | cls_params = [p for p in cls.param if p not in parameters and 36 | cls.param[p] == obj.param[p]] 37 | if not cls_params: 38 | continue 39 | parameters += cls_params 40 | cname = cls.__name__ 41 | module = cls.__module__ 42 | inherited.extend(['', f" :class:`{module}.{cname}`: {', '.join(cls_params)}"]) 43 | if inherited: 44 | lines.extend(["Parameters inherited from: "]+inherited) 45 | 46 | params = [p for p in obj.param if p not in parameters] 47 | for child in params: 48 | if child in ["print_level", "name"]: 49 | continue 50 | pobj = obj.param[child] 51 | label = label_formatter(pobj.name) 52 | doc = pobj.doc or "" 53 | members = inspect.getmembers(pobj) 54 | params_str = "" 55 | for name, value in members: 56 | try: 57 | is_default = bool(DEFAULT_VALUES.get(name) == value) 58 | except Exception: 59 | is_default = False 60 | skip = ( 61 | name.startswith('_') or 62 | name in IGNORED_ATTRS or 63 | inspect.ismethod(value) or 64 | inspect.isfunction(value) or 65 | value is None or 66 | is_default or 67 | (name == 'label' and pobj.label != label) 68 | ) 69 | if not skip: 70 | params_str += "%s=%s, " % (name, repr(value)) 71 | params_str = params_str[:-2] 72 | ptype = pobj.__class__.__name__ 73 | if params_str.lstrip(): 74 | lines.extend(["", f"``{child} = {ptype}({params_str})``", f" {doc}"]) 75 | else: 76 | lines.extend(["", f"``{child} = {ptype}()``", f" {doc}"]) 77 | 78 | def param_skip(app, what, name, obj, skip, options): 79 | if what == 'class' and not skip: 80 | return ( 81 | getattr(obj, '__qualname__', '').startswith('Parameters.deprecate') or 82 | (isinstance(obj, partial) and obj.args and isinstance(obj.args[0], param.Parameterized)) or 83 | (getattr(obj, '__qualname__', '').startswith('Parameterized.') and 84 | getattr(obj, '__class__', str).__name__ == 'function') 85 | ) 86 | elif what == 'module' and not skip and isinstance(obj, type) and issubclass(obj, param.Parameterized): 87 | # HACK: Sphinx incorrectly labels this as a module level discovery 88 | # We abuse this skip callback to exclude parameters and 89 | # include all methods 90 | members = [member for member in dir(obj) if not member.startswith('_') and member not in obj.param] 91 | if isinstance(options['members'], list): 92 | options['members'] += members 93 | else: 94 | options['members'] = members 95 | return skip 96 | -------------------------------------------------------------------------------- /nbsite/pyodide/ServiceHandler.js: -------------------------------------------------------------------------------- 1 | if ('serviceWorker' in navigator) { 2 | const url_root = document.getElementsByTagName('html')[0].getAttribute('data-content_root') 3 | navigator.serviceWorker.register(`${url_root}PyodideServiceWorker.js`).then(reg => { 4 | reg.onupdatefound = () => { 5 | const installingWorker = reg.installing; 6 | installingWorker.onstatechange = () => { 7 | if (installingWorker.state === 'installed' && 8 | navigator.serviceWorker.controller) { 9 | // Reload page if service worker is replaced 10 | location.reload(); 11 | } 12 | } 13 | } 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /nbsite/pyodide/ServiceWorker.js: -------------------------------------------------------------------------------- 1 | const appName = '{{ project }}' 2 | const appCacheName = '{{ project }}-{{ version }}'; 3 | 4 | const preCacheFiles = [{{ pre_cache }}]; 5 | 6 | const cachePatterns = [{{ cache_patterns }}]; 7 | 8 | self.addEventListener('install', (e) => { 9 | console.log('[Service Worker] Install'); 10 | self.skipWaiting(); 11 | e.waitUntil((async () => { 12 | const cacheNames = await caches.keys(); 13 | for (const cacheName of cacheNames) { 14 | if (cacheName.startsWith(appName) && cacheName !== appCacheName) { 15 | console.log(`[Service Worker] Delete old cache ${cacheName}`); 16 | caches.delete(cacheName); 17 | } 18 | } 19 | const cache = await caches.open(appCacheName); 20 | if (preCacheFiles.length) { 21 | console.log('[Service Worker] Precaching '); 22 | } 23 | preCacheFiles.forEach(async (cacheFile) => { 24 | const request = new Request(cacheFile); 25 | const response = await fetch(request); 26 | if (response.ok || response.type == 'opaque') { 27 | cache.put(request, response); 28 | } 29 | }) 30 | })()); 31 | }); 32 | 33 | self.addEventListener('activate', (event) => { 34 | console.log('[Service Worker] Activating'); 35 | return self.clients.claim(); 36 | }); 37 | 38 | self.addEventListener('fetch', (e) => { 39 | if (e.request.method !== 'GET') { 40 | return 41 | } 42 | e.respondWith((async () => { 43 | const cache = await caches.open(appCacheName); 44 | let response = await cache.match(e.request); 45 | console.log(`[Service Worker] Fetching resource: ${e.request.url}`); 46 | if (response) { 47 | return response; 48 | } 49 | response = await fetch(e.request); 50 | if (!response.ok && !(response.type == 'opaque')) { 51 | throw Error(`[Service Worker] Fetching resource ${e.request.url} failed with response: ${response.status}`); 52 | } 53 | console.log(`[Service Worker] Caching new resource: ${e.request.url}`); 54 | if (e.request.mode !== 'no-cors') { 55 | cache.put(e.request, response.clone()); 56 | } 57 | return response; 58 | })()); 59 | }); 60 | -------------------------------------------------------------------------------- /nbsite/pyodide/WebWorker.js: -------------------------------------------------------------------------------- 1 | importScripts("{{ PYODIDE_URL }}"); 2 | 3 | const QUEUE = []; 4 | 5 | const REQUIRES = {{ requires }} 6 | 7 | function sendPatch(patch, buffers, cell_id) { 8 | self.postMessage({ 9 | type: 'patch', 10 | patch: patch, 11 | buffers: buffers, 12 | id: cell_id 13 | }) 14 | } 15 | 16 | function sendStdout(cell_id, stdout) { 17 | self.postMessage({ 18 | type: 'stdout', 19 | content: stdout, 20 | id: cell_id 21 | }) 22 | } 23 | function sendStderr(cell_id, stderr) { 24 | self.postMessage({ 25 | type: 'stderr', 26 | content: stderr, 27 | id: cell_id 28 | }) 29 | } 30 | 31 | async function loadApplication(cell_id, path) { 32 | console.log("Loading pyodide!"); 33 | self.pyodide = await loadPyodide(); 34 | self.pyodide.globals.set("sendPatch", sendPatch); 35 | self.pyodide.globals.set("sendStdout", sendStdout); 36 | self.pyodide.globals.set("sendStderr", sendStderr); 37 | console.log("Loaded!"); 38 | await self.pyodide.loadPackage("micropip"); 39 | const packages = [{{ env_spec }}]; 40 | if (path != null) { 41 | for (const key of Object.keys(REQUIRES)) { 42 | if (path.replace('.html', '').endsWith(key.replace('.md', ''))) { 43 | for (const req of REQUIRES[key]) { 44 | packages.push(req) 45 | } 46 | } 47 | } 48 | } 49 | 50 | await self.pyodide.runPythonAsync("{{ setup_code }}") 51 | self.pyodide.runPython("import micropip") 52 | for (const pkg of packages) { 53 | self.postMessage({ 54 | type: 'loading', 55 | msg: `Loading ${pkg}`, 56 | id: cell_id 57 | }); 58 | await self.pyodide.runPythonAsync(` 59 | await micropip.install('${pkg}', keep_going=True); 60 | `); 61 | } 62 | console.log("Packages loaded!"); 63 | } 64 | 65 | const autodetect_deps_code = ` 66 | import json 67 | try: 68 | from panel.io.mime_render import find_requirements 69 | except Exception: 70 | from panel.io.mime_render import find_imports as find_requirements 71 | json.dumps(find_requirements(msg.to_py()['code']))` 72 | 73 | const exec_code = ` 74 | from functools import partial 75 | from panel.io.pyodide import pyrender 76 | from js import console 77 | 78 | msg = msg.to_py() 79 | code = msg['code'] 80 | stdout_cb = partial(sendStdout, msg['id']) 81 | stderr_cb = partial(sendStderr, msg['id']) 82 | target = f"output-{msg['id']}" 83 | pyrender(code, stdout_cb, stderr_cb, target)` 84 | 85 | const onload_code = ` 86 | msg = msg.to_py() 87 | if msg['mime'] == 'application/bokeh': 88 | from panel.io.pyodide import _link_docs_worker 89 | from panel.io.state import state 90 | doc = state.cache[f"output-{msg['id']}"] 91 | _link_docs_worker(doc, sendPatch, msg['id'], 'js')` 92 | 93 | const patch_code = ` 94 | from panel import state 95 | 96 | try: 97 | from pane.io.pyodide import _convert_json_patch 98 | patch = _convert_json_patch(msg.patch) 99 | except: 100 | patch = msg.patch.to_py() 101 | doc = state.cache[f"output-{msg.id}"] 102 | doc.apply_json_patch(patch, setter='js')` 103 | 104 | const MESSAGES = { 105 | patch: patch_code, 106 | execute: exec_code, 107 | rendered: onload_code 108 | } 109 | 110 | self.onmessage = async (event) => { 111 | let resolveExecution, rejectExecution; 112 | const executing = new Promise(function(resolve, reject){ 113 | resolveExecution = resolve; 114 | rejectExecution = reject; 115 | }); 116 | 117 | const prev_msg = QUEUE[0] 118 | const msg = {...event.data, executing} 119 | QUEUE.unshift(msg) 120 | 121 | if (prev_msg) { 122 | self.postMessage({ 123 | type: 'loading', 124 | msg: 'Awaiting previous cells', 125 | id: msg.id 126 | }); 127 | await prev_msg.executing 128 | } 129 | 130 | // Init pyodide 131 | if (self.pyodide == null) { 132 | self.postMessage({ 133 | type: 'loading', 134 | msg: 'Loading pyodide', 135 | id: msg.id 136 | }); 137 | await loadApplication(msg.id, msg.path) 138 | self.postMessage({ 139 | type: 'loaded', 140 | id: msg.id 141 | }); 142 | } 143 | 144 | // Handle message 145 | if (!MESSAGES.hasOwnProperty(msg.type)) { 146 | console.warn(`Service worker received unknown message type '${msg.type}'.`) 147 | resolveExecution() 148 | self.postMessage({ 149 | type: 'idle', 150 | id: msg.id 151 | }); 152 | return 153 | } 154 | 155 | {% if autodetect_deps %} 156 | if (msg.type === 'execute') { 157 | let deps 158 | try { 159 | self.pyodide.globals.set('msg', msg) 160 | deps = self.pyodide.runPython(autodetect_deps_code) 161 | } catch(e) { 162 | deps = '[]' 163 | console.warn(`Auto-detection of dependencies failed with error: ${e}`) 164 | } 165 | for (const pkg of JSON.parse(deps)) { 166 | self.postMessage({ 167 | type: 'loading', 168 | msg: `Loading ${pkg}`, 169 | id: msg.id 170 | }); 171 | try { 172 | await self.pyodide.runPythonAsync(`await micropip.install('${pkg}', keep_going=True)`) 173 | } catch(e) { 174 | console.log(`Auto-detected dependency ${pkg} could not be installed.`) 175 | } 176 | } 177 | } 178 | {% endif %} 179 | 180 | try { 181 | self.pyodide.globals.set('msg', msg) 182 | let out = await self.pyodide.runPythonAsync(MESSAGES[msg.type]) 183 | resolveExecution() 184 | if (out == null) { 185 | out = new Map() 186 | } 187 | if (out.has('content')) { 188 | self.postMessage({ 189 | type: 'render', 190 | id: msg.id, 191 | content: out.get('content'), 192 | mime: out.get('mime_type') 193 | }); 194 | } 195 | if (out.has('stdout') && out.get('stdout').length) { 196 | self.postMessage({ 197 | type: 'stdout', 198 | content: out.get('stdout'), 199 | id: msg.id 200 | }); 201 | } 202 | if (out.has('stderr') && out.get('stderr').length) { 203 | self.postMessage({ 204 | type: 'stderr', 205 | content: out.get('stderr'), 206 | id: msg.id 207 | }); 208 | } 209 | self.postMessage({ 210 | type: 'idle', 211 | id: msg.id, 212 | uuid: msg.uuid 213 | }); 214 | } catch (e) { 215 | const traceback = `${e}` 216 | const tblines = traceback.split('\n') 217 | self.postMessage({ 218 | type: 'error', 219 | traceback: traceback, 220 | msg: tblines[tblines.length-2], 221 | id: msg.id 222 | }); 223 | resolveExecution() 224 | throw(e) 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /nbsite/pyodide/WorkerHandler.js: -------------------------------------------------------------------------------- 1 | const url_root = document.getElementsByTagName('html')[0].getAttribute('data-content_root') 2 | const pyodideWorker = new Worker(`${url_root}_static/PyodideWebWorker.js`); 3 | 4 | pyodideWorker.documents = {} 5 | pyodideWorker.busy = false 6 | pyodideWorker.queues = new Map() 7 | 8 | const patching = new Map(); 9 | 10 | function uid() { 11 | return String( 12 | Date.now().toString(32) + 13 | Math.random().toString(16) 14 | ).replace(/\./g, '') 15 | } 16 | 17 | function send_change(jsdoc, doc_id, event) { 18 | const patch_status = patching.get(doc_id) || 0 19 | if ((event.setter_id == 'py') || (patch_status > 0)) { 20 | return 21 | } else if (pyodideWorker.busy && event.model && event.attr) { 22 | let events = [] 23 | if (pyodideWorker.queues.has(doc_id)) { 24 | for (const old_event of pyodideWorker.queues.get(doc_id)) { 25 | if (!(old_event.model === event.model && old_event.attr === event.attr)) { 26 | events.push(old_event) 27 | } 28 | } 29 | } 30 | events.push(event) 31 | pyodideWorker.queues.set(doc_id, events) 32 | return 33 | } 34 | const patch = jsdoc.create_json_patch([event]) 35 | const uuid = uid() 36 | pyodideWorker.busy = uuid 37 | pyodideWorker.postMessage({type: 'patch', patch: patch, id: doc_id, uuid}) 38 | } 39 | 40 | pyodideWorker.onmessage = async (event) => { 41 | const button = document.getElementById(`button-${event.data.id}`) 42 | const output = document.getElementById(`output-${event.data.id}`) 43 | const stdout = document.getElementById(`stdout-${event.data.id}`) 44 | const stderr = document.getElementById(`stderr-${event.data.id}`) 45 | const msg = event.data; 46 | 47 | if (msg.uuid == pyodideWorker.busy) { 48 | if (pyodideWorker.queues.size) { 49 | const [msg_id, events] = pyodideWorker.queues.entries().next().value 50 | const patch = pyodideWorker.documents[msg_id].create_json_patch(events) 51 | const uuid = uid() 52 | pyodideWorker.busy = uuid 53 | pyodideWorker.postMessage({type: 'patch', patch: patch, id: msg_id, uuid}) 54 | pyodideWorker.queues.delete(msg_id) 55 | } else { 56 | pyodideWorker.busy = false 57 | } 58 | } 59 | 60 | if (msg.type === 'loading') { 61 | _ChangeTooltip(button, msg.msg) 62 | _ChangeIcon(button, iconLoading) 63 | } else if (msg.type === 'loaded') { 64 | _ChangeTooltip(button, 'Executing code') 65 | } else if (msg.type === 'error') { 66 | _ChangeTooltip(button, msg.msg) 67 | _ChangeIcon(button, iconError) 68 | } else if (msg.type === 'idle') { 69 | _ChangeTooltip(button, 'Executed successfully') 70 | _ChangeIcon(button, iconLoaded) 71 | } else if (msg.type === 'stdout') { 72 | const stdout = document.getElementById(`stdout-${msg.id}`) 73 | stdout.style.display = 'block'; 74 | stdout.innerHTML += msg.content 75 | } else if (msg.type === 'stderr') { 76 | const stderr = document.getElementById(`stderr-${msg.id}`) 77 | stderr.style.display = 'block'; 78 | stderr.innerHTML += msg.content 79 | } else if (msg.type === 'render') { 80 | output.style.display = 'block'; 81 | output.setAttribute('class', 'pyodide-output live') 82 | if (msg.mime === 'application/bokeh') { 83 | const [view] = await Bokeh.embed.embed_item(JSON.parse(msg.content)) 84 | 85 | // Setup bi-directional syncing 86 | pyodideWorker.documents[msg.id] = jsdoc = view.model.document 87 | if (pyodideWorker.queues != null && pyodideWorker.queues.has(msg.id)) { 88 | // Delete old message queue 89 | pyodideWorker.queues.delete(msg.id) 90 | } 91 | jsdoc.on_change(send_change.bind(null, jsdoc, msg.id), false) 92 | } else if (msg.mime === 'text/plain') { 93 | output.innerHTML = `
${msg.content}
`; 94 | } else if (msg.mime === 'text/html') { 95 | output.innerHTML = `
${msg.content}
` 96 | } 97 | pyodideWorker.postMessage({type: 'rendered', id: msg.id, mime: msg.mime}) 98 | } else if (msg.type === 'patch') { 99 | let patch_status = patching.get(msg.id) || 0 100 | patching.set(msg.id, patch_status+1) 101 | try { 102 | pyodideWorker.documents[msg.id].apply_json_patch(msg.patch, msg.buffers, setter_id='py') 103 | } finally { 104 | if (patch_status === 0) { 105 | patching.delete(msg.id) 106 | } else { 107 | patching.set(msg.id, patch_status) 108 | } 109 | } 110 | } 111 | }; 112 | 113 | window.pyodideWorker = pyodideWorker; 114 | -------------------------------------------------------------------------------- /nbsite/pyodide/_static/run_cell.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SVG files for our copy buttons 3 | */ 4 | let iconRun = ` 5 | 6 | 7 | ` 8 | 9 | let iconLoading = ` 10 | 11 | 12 | ` 13 | 14 | let iconLoaded = ` 15 | 16 | ` 17 | 18 | let iconError = ` 19 | 20 | ` 21 | 22 | let iconAlert = ` 23 | 24 | ` 25 | 26 | /** 27 | * Set up run for code blocks 28 | */ 29 | 30 | const _runWhenDOMLoaded = cb => { 31 | if (document.readyState != 'loading') { 32 | cb() 33 | } else if (document.addEventListener) { 34 | document.addEventListener('DOMContentLoaded', cb) 35 | } else { 36 | document.attachEvent('onreadystatechange', function() { 37 | if (document.readyState == 'complete') cb() 38 | }) 39 | } 40 | } 41 | 42 | const _codeCellId = index => `codecell${index}-py` 43 | 44 | // Changes tooltip text for two seconds, then changes it back 45 | const _ChangeTooltip = (el, newText) => { 46 | const oldText = el.getAttribute('data-tooltip') 47 | el.setAttribute('data-tooltip', newText) 48 | } 49 | 50 | // Changes the copy button icon for two seconds, then changes it back 51 | const _ChangeIcon = (el, icon) => { 52 | el.innerHTML = icon; 53 | } 54 | 55 | function executeCell(id) { 56 | const cell = document.getElementById(id) 57 | let output = document.getElementById(`output-${id}`) 58 | let stdout = document.getElementById(`stdout-${id}`) 59 | let stderr = document.getElementById(`stderr-${id}`) 60 | if (stdout == null) { 61 | stdout = document.createElement('pre'); 62 | stdout.setAttribute('id', `stdout-${id}`) 63 | stdout.setAttribute('class', 'pyodide-stdout') 64 | cell.parentElement.parentElement.appendChild(stdout) 65 | } 66 | if (stderr == null) { 67 | stderr = document.createElement('pre'); 68 | stderr.setAttribute('id', `stderr-${id}`) 69 | stderr.setAttribute('class', 'pyodide-stderr') 70 | cell.parentElement.parentElement.appendChild(stderr) 71 | } 72 | if (output == null) { 73 | output = document.createElement('div'); 74 | output.setAttribute('id', `output-${id}`) 75 | output.setAttribute('class', 'pyodide-output') 76 | cell.parentElement.parentElement.appendChild(output) 77 | } 78 | window.pyodideWorker.postMessage({ 79 | type: 'execute', 80 | id: id, 81 | path: document.location.pathname, 82 | code: cell.textContent 83 | }) 84 | cell.setAttribute('executed', true) 85 | } 86 | 87 | const _query_params = new Proxy(new URLSearchParams(window.location.search), { 88 | get: (searchParams, prop) => searchParams.get(prop), 89 | }); 90 | 91 | let ACCEPTED = false; 92 | let INITIALIZED = 0; 93 | let EXECUTED = false; 94 | 95 | const _addRunButtonToCodeCells = () => { 96 | // If Pyodide Worker hasn't loaded, wait a bit and try again. 97 | if (window.pyodideWorker === undefined) { 98 | setTimeout(addRunButtonToCodeCells, 250) 99 | return 100 | } 101 | 102 | // Add copybuttons to all of our code cells 103 | const RUNBUTTON_SELECTOR = 'div.pyodide div.highlight pre'; 104 | const codeCells = document.querySelectorAll(RUNBUTTON_SELECTOR) 105 | 106 | INITIALIZED += 1 107 | codeCells.forEach((codeCell, index) => { 108 | const id = _codeCellId(index) 109 | const copybtn = codeCell.parentElement.getElementsByClassName('copybtn') 110 | if (copybtn.length) { 111 | copybtn[0].setAttribute('data-clipboard-target', `#${id}`) 112 | } 113 | codeCell.setAttribute('id', id) 114 | codeCell.setAttribute('executed', false) 115 | 116 | // importShim will cause DOMLoaded event to trigger twice so we skip 117 | // adding buttons the first time 118 | if ((INITIALIZED < 2) && window.importShim) { 119 | return 120 | } 121 | 122 | const RunButton = id => 123 | `` 126 | codeCell.insertAdjacentHTML('afterend', RunButton(id)) 127 | const run_button = document.getElementById(`button-${id}`) 128 | run_button.addEventListener('click', (e) => { 129 | if (!ACCEPTED) { 130 | _ChangeTooltip(e.currentTarget, 'Executing this cell will download a Python runtime (typically 40+ MB). Click again to proceed.') 131 | _ChangeIcon(e.currentTarget, iconAlert) 132 | ACCEPTED = true 133 | return 134 | } else if (!EXECUTED) { 135 | Bokeh.index.roots.map((v) => v.remove()) 136 | EXECUTED = true 137 | } 138 | let i = 0; 139 | while (true) { 140 | let cell_id = _codeCellId(i) 141 | let cell = document.getElementById(cell_id) 142 | if (cell == null) { 143 | break 144 | } 145 | const output = document.getElementById(`output-${cell_id}`) 146 | const stdout = document.getElementById(`stdout-${cell_id}`) 147 | const stderr = document.getElementById(`stderr-${cell_id}`) 148 | if (cell.getAttribute('executed') == 'false' || i == index) { 149 | if (output) { 150 | output.innerHTML = ''; 151 | output.style.display = 'none'; 152 | } 153 | if (stdout) { 154 | stdout.innerHTML = ''; 155 | stdout.style.display = 'none'; 156 | } 157 | if (stderr) { 158 | stderr.innerHTML = ''; 159 | stderr.style.display = 'none'; 160 | } 161 | executeCell(cell_id) 162 | } 163 | i++; 164 | } 165 | }) 166 | }) 167 | if (_query_params.autorun) { 168 | const id = _codeCellId(0) 169 | const run_button = document.getElementById(`button-${id}`) 170 | run_button.click() 171 | run_button.click() 172 | } 173 | } 174 | 175 | _runWhenDOMLoaded(_addRunButtonToCodeCells) 176 | -------------------------------------------------------------------------------- /nbsite/pyodide/_static/runbutton.css: -------------------------------------------------------------------------------- 1 | /* Copy buttons */ 2 | button.runbtn { 3 | position: absolute; 4 | display: flex; 5 | bottom: 0.3em; 6 | right: 0.3em; 7 | width: 1.7em; 8 | height: 1.7em; 9 | user-select: none; 10 | padding: 0; 11 | border: none; 12 | outline: none; 13 | border-radius: 0.4em; 14 | /* The colors that GitHub uses */ 15 | border: #1b1f2426 1px solid; 16 | background-color: #f6f8fa; 17 | color: #57606a; 18 | } 19 | 20 | div.highlight { 21 | position: relative; 22 | } 23 | 24 | .highlight button.runbtn:hover { 25 | background-color: rgb(235, 235, 235); 26 | } 27 | 28 | .highlight button.runbtn:active { 29 | background-color: rgb(187, 187, 187); 30 | } 31 | 32 | /** 33 | * A minimal CSS-only tooltip copied from: 34 | * https://codepen.io/mildrenben/pen/rVBrpK 35 | * 36 | * To use, write HTML like the following: 37 | * 38 | *

Short

39 | */ 40 | .o-tooltip--left { 41 | position: relative; 42 | } 43 | 44 | .o-tooltip--left:after { 45 | opacity: 0; 46 | visibility: hidden; 47 | position: absolute; 48 | content: attr(data-tooltip); 49 | padding: 0.2em; 50 | font-size: 0.8em; 51 | left: -0.2em; 52 | background: grey; 53 | color: white; 54 | white-space: nowrap; 55 | z-index: 2; 56 | border-radius: 2px; 57 | transform: translateX(-102%) translateY(0); 58 | transition: 59 | opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), 60 | transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); 61 | } 62 | 63 | .o-tooltip--left:has(.pyodide-alert-icon):after { 64 | opacity: 1; 65 | visibility: visible; 66 | } 67 | 68 | .o-tooltip--left:hover:after { 69 | display: block; 70 | opacity: 1; 71 | visibility: visible; 72 | transform: translateX(-100%) translateY(0); 73 | transition: 74 | opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), 75 | transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); 76 | transition-delay: 0.5s; 77 | } 78 | 79 | /* By default the copy button shouldn't show up when printing a page */ 80 | @media print { 81 | button.runbtn { 82 | display: none; 83 | } 84 | } 85 | 86 | button.runbtn svg.pyodide-loading-icon { 87 | transform-box: fill-box; 88 | transform-origin: 50% 50%; 89 | animation-duration: 3s; 90 | animation-name: rotate; 91 | animation-iteration-count: infinite; 92 | } 93 | 94 | @keyframes rotate { 95 | from { 96 | transform: rotate(0deg); 97 | } 98 | to { 99 | transform: rotate(360deg); 100 | } 101 | } 102 | 103 | .pyodide-output { 104 | margin: 1em 0; 105 | overflow-x: auto; 106 | } 107 | 108 | .pyodide-output-wrapper { 109 | margin-left: 0.5em; 110 | } 111 | 112 | .pyodide-stderr { 113 | background-color: rgba(194, 103, 103, 0.35); 114 | border: indianred solid 1px; 115 | display: none; 116 | } 117 | 118 | .pyodide-stdout { 119 | display: none; 120 | } 121 | 122 | .pyodide-output.embedded { 123 | border-left: 0.5em solid darkgoldenrod; 124 | border-radius: 0.4em; 125 | } 126 | 127 | .pyodide-output.live { 128 | border-left: 0.5em solid green; 129 | border-radius: 0.5em; 130 | } 131 | -------------------------------------------------------------------------------- /nbsite/pyodide/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{ name }}", 3 | "short_name": "{{ name }}", 4 | "start_url": "index.html", 5 | "icons": [ 6 | { 7 | "src": "_static/icons/icon-32x32.png", 8 | "type": "image/png", 9 | "sizes": "32x32" 10 | }, 11 | { 12 | "src": "_static/icons/icon-192x192.png", 13 | "type": "image/png", 14 | "sizes": "192x192" 15 | }, 16 | { 17 | "src": "_static/icons/icon-512x512.png", 18 | "type": "image/png", 19 | "sizes": "512x512" 20 | } 21 | ], 22 | "display": "{{ display|default('standalone', true) }}", 23 | "scope": "/", 24 | "background_color": "{{ background_color|default('#fdfdfd', true) }}", 25 | "theme_color": "{{ background_color|default('#0072B5', true) }}", 26 | "orientation": "{{ orientation|default('any', true) }}" 27 | } 28 | -------------------------------------------------------------------------------- /nbsite/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | from ._clean_dist_html import clean_dist_html 2 | from ._fix_links import fix_links 3 | 4 | __all__ = ("clean_dist_html", "fix_links") 5 | -------------------------------------------------------------------------------- /nbsite/scripts/_clean_dist_html.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """_Title.ipynb -> The Title 4 | 5 | 6 | 7 | """ 8 | 9 | import os 10 | import shutil 11 | 12 | 13 | def IGetFiles(d): 14 | for thing in os.scandir(d): 15 | if thing.is_dir(): 16 | yield from IGetFiles(thing.path) 17 | else: 18 | yield thing.path 19 | 20 | # I think it's ok to assume these exist for a sphinx site... 21 | 22 | def clean_dist_html(output, dry_run): 23 | htmldir = os.path.abspath(output) 24 | 25 | if dry_run: 26 | print("This is just a dry-run of removing files from:", htmldir) 27 | else: 28 | print("Removing files from:", htmldir) 29 | 30 | # (.doctrees in build folder by default only for sphinx<1.8) 31 | for folder in (".doctrees", "_sources"): 32 | d = os.path.join(htmldir,folder) 33 | try: 34 | if dry_run: 35 | print("would remove", folder) 36 | else: 37 | print("removing", folder) 38 | shutil.rmtree(d) 39 | except Exception: 40 | pass 41 | 42 | for path in IGetFiles(htmldir): 43 | if os.path.splitext(path)[1].lower() == '.ipynb': 44 | name = path.split(output)[-1] 45 | if dry_run: 46 | print("would remove", name) 47 | else: 48 | print("removing", name) 49 | os.remove(path) 50 | -------------------------------------------------------------------------------- /nbsite/scripts/_fix_links.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Cleans up relative cross-notebook links by replacing them with .html 4 | extension. 5 | """ 6 | import os 7 | import re 8 | import warnings 9 | 10 | from concurrent.futures import ThreadPoolExecutor 11 | from functools import partial 12 | from pathlib import Path 13 | 14 | from bs4 import BeautifulSoup 15 | 16 | # TODO: holoviews specific links e.g. to reference manual...doc & generalize 17 | 18 | #BOKEH_REPLACEMENTS = {'cell.output_area.append_execute_result': '//cell.output_area.append_execute_result', 19 | # '}(window));\n': '}(window));\n', 20 | # '\n(function(root) {': '