├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ ├── bump-lockfile.yml │ ├── ci.yml │ └── docs.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── assets │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ └── logo.png ├── getting-started │ ├── blocks.md │ ├── configuration.md │ ├── inline-notebook.md │ ├── inlined.py │ ├── installation.md │ ├── nav_notebook.py │ └── quick-start.md └── index.md ├── mkdocs.yml ├── mkdocs_marimo ├── __init__.py ├── blocks.py └── plugin.py ├── pixi.lock ├── pixi.toml ├── pyproject.toml └── tests ├── __init__.py ├── fixtures ├── __init__.py └── app.py ├── test_plugin.py └── tmp └── .gitkeep /.gitattributes: -------------------------------------------------------------------------------- 1 | # GitHub syntax highlighting 2 | pixi.lock linguist-language=YAML linguist-generated=true 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: monthly 7 | groups: 8 | actions: 9 | patterns: 10 | - "*" 11 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build package 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - '[0-9]+.[0-9]+.[0-9]+' 9 | pull_request: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 10 15 | steps: 16 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 17 | - name: Setup pixi 18 | uses: prefix-dev/setup-pixi@92815284c57faa15cd896c4d5cfb2d59f32dc43d # v0.8.3 19 | with: 20 | environments: build 21 | - name: Build package 22 | run: | 23 | pixi run -e build python -m build --no-isolation . 24 | - name: Upload package 25 | uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 26 | with: 27 | name: artifact 28 | path: dist/* 29 | if-no-files-found: error 30 | 31 | release: 32 | name: Publish package 33 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') 34 | needs: [build] 35 | runs-on: ubuntu-latest 36 | timeout-minutes: 10 37 | permissions: 38 | id-token: write 39 | contents: write 40 | environment: pypi 41 | steps: 42 | - uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9 43 | with: 44 | name: artifact 45 | path: dist 46 | - name: Publish package on PyPi 47 | uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 48 | -------------------------------------------------------------------------------- /.github/workflows/bump-lockfile.yml: -------------------------------------------------------------------------------- 1 | name: Update lockfiles 2 | 3 | permissions: 4 | contents: write 5 | pull-requests: write 6 | 7 | on: 8 | workflow_dispatch: 9 | schedule: 10 | - cron: 0 5 1 * * 11 | 12 | jobs: 13 | pixi-update: 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 5 16 | steps: 17 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 18 | - name: Set up pixi 19 | uses: prefix-dev/setup-pixi@92815284c57faa15cd896c4d5cfb2d59f32dc43d # v0.8.3 20 | with: 21 | run-install: false 22 | - name: Update lockfiles 23 | run: | 24 | set -o pipefail 25 | pixi update --json | pixi exec pixi-diff-to-markdown >> diff.md 26 | - name: Create pull request 27 | uses: peter-evans/create-pull-request@dd2324fc52d5d43c699a5636bcf19fceaa70c284 # v7.0.7 28 | with: 29 | token: ${{ secrets.GITHUB_TOKEN }} 30 | commit-message: Update pixi lockfile 31 | title: Update pixi lockfile 32 | body-path: diff.md 33 | branch: update-pixi 34 | base: main 35 | labels: pixi 36 | delete-branch: true 37 | add-paths: pixi.lock 38 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | # Automatically stop old builds on the same branch/PR 4 | concurrency: 5 | group: ${{ github.workflow }}-${{ github.ref }} 6 | cancel-in-progress: true 7 | 8 | on: 9 | push: 10 | branches: 11 | - main 12 | pull_request: 13 | 14 | jobs: 15 | test: 16 | strategy: 17 | max-parallel: 5 18 | matrix: 19 | env: 20 | - py39 21 | - py310 22 | - py311 23 | - py312 24 | - py313 25 | os: 26 | - ubuntu-latest 27 | - macos-latest 28 | # TODO: fix windows 29 | # - windows-latest 30 | runs-on: ${{ matrix.os }} 31 | timeout-minutes: 5 32 | steps: 33 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 34 | - name: Setup pixi 35 | uses: prefix-dev/setup-pixi@92815284c57faa15cd896c4d5cfb2d59f32dc43d # v0.8.3 36 | with: 37 | environments: ${{ matrix.env }} 38 | - name: Install repository 39 | run: | 40 | pixi run -e ${{ matrix.env }} postinstall 41 | - name: Run tests 42 | run: | 43 | pixi run -e ${{ matrix.env }} test --color=yes 44 | - name: Type checking 45 | run: | 46 | pixi run -e ${{ matrix.env }} mypy mkdocs_marimo 47 | 48 | lint: 49 | runs-on: ubuntu-latest 50 | timeout-minutes: 5 51 | steps: 52 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 53 | - name: Setup pixi 54 | uses: prefix-dev/setup-pixi@92815284c57faa15cd896c4d5cfb2d59f32dc43d # v0.8.3 55 | with: 56 | environments: lint default 57 | - name: pre-commit 58 | run: pixi run pre-commit-run --color=always --show-diff-on-failure 59 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy docs 2 | 3 | # Automatically stop old builds on the same branch/PR 4 | concurrency: 5 | group: ${{ github.workflow }}-${{ github.ref }} 6 | cancel-in-progress: true 7 | 8 | on: 9 | push: 10 | branches: 11 | - main 12 | pull_request: 13 | 14 | jobs: 15 | build: 16 | name: Deploy docs 17 | timeout-minutes: 5 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Download source 21 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | - name: Install Python 23 | uses: prefix-dev/setup-pixi@92815284c57faa15cd896c4d5cfb2d59f32dc43d # v0.8.3 24 | with: 25 | environments: docs 26 | - name: Install repository 27 | run: pixi run -e docs postinstall 28 | - name: Build site 29 | run: pixi run -e docs mkdocs build 30 | - name: Deploy to gh-pages 31 | # TODO: replace with a maintained action 32 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 33 | uses: oprypin/push-to-gh-pages@v3 34 | with: 35 | publish_dir: site 36 | commit_message: 'Generate docs: ' 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info/ 3 | dist/ 4 | site/ 5 | .tox/ 6 | .mypy_cache/ 7 | .pytest_cache/ 8 | .venv/ 9 | __pycache__/ 10 | .pixi 11 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: \.pixi 2 | repos: 3 | - repo: local 4 | hooks: 5 | # ensure pixi environments are up to date 6 | # workaround for https://github.com/prefix-dev/pixi/issues/1482 7 | - id: pixi-install 8 | name: pixi-install 9 | entry: pixi install -e default -e lint 10 | language: system 11 | always_run: true 12 | require_serial: true 13 | pass_filenames: false 14 | # ruff 15 | - id: ruff 16 | name: ruff 17 | entry: pixi run -e lint ruff check --fix --exit-non-zero-on-fix --force-exclude 18 | language: system 19 | types_or: [python, pyi] 20 | require_serial: true 21 | - id: ruff-format 22 | name: ruff-format 23 | entry: pixi run -e lint ruff format --force-exclude 24 | language: system 25 | types_or: [python, pyi] 26 | require_serial: true 27 | # mypy 28 | - id: mypy 29 | name: mypy 30 | entry: pixi run -e default mypy 31 | language: system 32 | types: [python] 33 | require_serial: true 34 | # taplo 35 | - id: taplo 36 | name: taplo 37 | entry: pixi run -e lint taplo format 38 | language: system 39 | types: [toml] 40 | # pre-commit-hooks 41 | - id: trailing-whitespace-fixer 42 | name: trailing-whitespace-fixer 43 | entry: pixi run -e lint trailing-whitespace-fixer 44 | language: system 45 | types: [text] 46 | - id: end-of-file-fixer 47 | name: end-of-file-fixer 48 | entry: pixi run -e lint end-of-file-fixer 49 | language: system 50 | types: [text] 51 | # typos 52 | - id: typos 53 | name: typos 54 | entry: pixi run -e lint typos --force-exclude 55 | language: system 56 | types: [text] 57 | require_serial: true 58 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Development 4 | 5 | Install `pixi`: see [pixi docs](https://pixi.sh) 6 | 7 | Run the tests: 8 | 9 | ```bash 10 | pixi run postinstall 11 | pixi run test 12 | ``` 13 | 14 | Type check: 15 | 16 | ```bash 17 | pixi run mypy mkdocs_marimo 18 | ``` 19 | 20 | Serve the documentation: 21 | 22 | ```bash 23 | pixi run docs 24 | ``` 25 | 26 | Lint and format: 27 | 28 | ```bash 29 | pixi run -e lint ruff format 30 | ``` 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MkDocs marimo Plugin 2 | 3 | > [!WARNING] 4 | > The MkDocs marimo plugin is under active development. Features and documentation are being continuously updated and expanded. 5 | 6 | This plugin allows you to embed interactive [marimo](https://github.com/marimo-team/marimo) notebooks in your MkDocs documentation. 7 | 8 | ## Installation 9 | 10 | ```bash 11 | # pip 12 | pip install mkdocs-marimo 13 | # uv 14 | uv pip install mkdocs-marimo 15 | # pixi 16 | pixi add mkdocs-marimo 17 | ``` 18 | 19 | ## Usage 20 | 21 | Create reactive and interactive Python blocks in your markdown files using [marimo](https://github.com/marimo-team/marimo). 22 | 23 | ### Embedding inline Python code and marimo elements 24 | 25 | This uses code fences to embed marimo components as [marimo islands](https://docs.marimo.io/guides/exporting/?h=#embed-marimo-outputs-in-html-using-islands). 26 | 27 | ````markdown 28 | ```python {marimo} 29 | import marimo as mo 30 | 31 | name = mo.ui.text(placeholder="Enter your name") 32 | name 33 | ``` 34 | 35 | ```python {marimo} 36 | mo.md(f"Hello, **{name.value or '__'}**!") 37 | ``` 38 | ```` 39 | 40 | ### Embedding the marimo playground 41 | 42 | For an easy way to embed marimo notebooks or applications, we recommend embedding the marimo playground. This feature uses `pymdownx.blocks` to embed marimo notebooks in your MkDocs documentation, creating iframes that render the marimo playground. 43 | 44 | ````markdown 45 | /// marimo-embed 46 | height: 400px 47 | mode: run 48 | 49 | ```python 50 | @app.cell 51 | def __(): 52 | import matplotlib.pyplot as plt 53 | import numpy as np 54 | 55 | x = np.linspace(0, 10, 100) 56 | y = np.sin(x) 57 | 58 | plt.figure(figsize=(8, 4)) 59 | plt.plot(x, y) 60 | plt.title('Sine Wave') 61 | plt.xlabel('x') 62 | plt.ylabel('sin(x)') 63 | plt.grid(True) 64 | plt.gca() 65 | return 66 | ``` 67 | /// 68 | ```` 69 | 70 | Available options for `marimo-embed`: 71 | 72 | - `height`: Named sizes (`small`, `medium`, `large`, `xlarge`, `xxlarge`) or custom pixel values (e.g. `500px`) (default: medium) 73 | - `mode`: read, edit (default: read) 74 | - `app_width`: wide, full, compact (default: wide) 75 | 76 | You can also embed marimo files directly: 77 | 78 | ````markdown 79 | /// marimo-embed-file 80 | filepath: path/to/your/file.py 81 | height: 400px 82 | mode: read 83 | show_source: true 84 | /// 85 | ```` 86 | 87 | Additional options for `marimo-embed-file`: 88 | 89 | - `filepath`: path to the marimo file to embed (required) 90 | - `show_source`: true, false (default: true) - whether to show the source code below the embed 91 | 92 | ## Examples 93 | 94 | Checkout the [documentation](https://marimo-team.github.io/mkdocs-marimo) for more examples. 95 | 96 | ## Contributions welcome 97 | 98 | Feel free to ask questions, enhancements and to contribute to this project! 99 | 100 | See [CONTRIBUTING.md](CONTRIBUTING.md) for more details. 101 | 102 | ## Credits 103 | 104 | - Repo template from [mkdocs-static-i18n](https://github.com/ultrabug/mkdocs-static-i18n) 105 | -------------------------------------------------------------------------------- /docs/assets/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marimo-team/mkdocs-marimo/4f6a7c72e4f86b3578c308e436eec0dd2629bca2/docs/assets/android-chrome-192x192.png -------------------------------------------------------------------------------- /docs/assets/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marimo-team/mkdocs-marimo/4f6a7c72e4f86b3578c308e436eec0dd2629bca2/docs/assets/android-chrome-512x512.png -------------------------------------------------------------------------------- /docs/assets/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marimo-team/mkdocs-marimo/4f6a7c72e4f86b3578c308e436eec0dd2629bca2/docs/assets/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/assets/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marimo-team/mkdocs-marimo/4f6a7c72e4f86b3578c308e436eec0dd2629bca2/docs/assets/favicon-16x16.png -------------------------------------------------------------------------------- /docs/assets/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marimo-team/mkdocs-marimo/4f6a7c72e4f86b3578c308e436eec0dd2629bca2/docs/assets/favicon-32x32.png -------------------------------------------------------------------------------- /docs/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marimo-team/mkdocs-marimo/4f6a7c72e4f86b3578c308e436eec0dd2629bca2/docs/assets/favicon.ico -------------------------------------------------------------------------------- /docs/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marimo-team/mkdocs-marimo/4f6a7c72e4f86b3578c308e436eec0dd2629bca2/docs/assets/logo.png -------------------------------------------------------------------------------- /docs/getting-started/blocks.md: -------------------------------------------------------------------------------- 1 | # Embedding marimo Notebooks with the Playground 2 | 3 | This guide covers the second approach to using marimo in your documentation: embedding full marimo notebooks using the marimo playground, [like in our own documentation](https://docs.marimo.io/api/inputs/button/). This approach is ideal when you want to: 4 | 5 | - Show complete, multi-cell notebooks 6 | - Allow users to edit and experiment with the code 7 | - Embed existing .py notebooks 8 | 9 | If you're looking for simpler, inline code examples, check out the [Quick Start](quick-start.md) guide's section on inline code. 10 | 11 | This feature uses `pymdownx.blocks` to embed marimo notebooks in your MkDocs documentation, creating iframes that render the marimo playground. 12 | 13 | ## Setup 14 | 15 | To use the marimo playground, you need to install the `pymdown-extensions` package. 16 | 17 | ```bash 18 | pip install mkdocs-marimo pymdown-extensions 19 | ``` 20 | 21 | ## Basic Example 22 | 23 | Here's a simple example of embedding a marimo notebook using blocks: 24 | 25 | ````markdown 26 | /// marimo-embed 27 | height: 400px 28 | mode: read 29 | app_width: wide 30 | 31 | ```python 32 | @app.cell 33 | def __(): 34 | import marimo as mo 35 | 36 | name = mo.ui.text(placeholder="Enter your name", debounce=False) 37 | name 38 | return 39 | 40 | @app.cell 41 | def __(): 42 | mo.md(f"Hello, **{name.value or '__'}**!") 43 | return 44 | ``` 45 | 46 | /// 47 | ```` 48 | 49 | /// marimo-embed 50 | height: 400px 51 | mode: read 52 | app_width: wide 53 | 54 | ```python 55 | @app.cell 56 | def __(): 57 | import marimo as mo 58 | 59 | name = mo.ui.text(placeholder="Enter your name", debounce=False) 60 | name 61 | return 62 | 63 | @app.cell 64 | def __(): 65 | mo.md(f"Hello, **{name.value or '__'}**!") 66 | return 67 | ``` 68 | 69 | /// 70 | 71 | ## Interactive Plot Example 72 | 73 | Here's an example with an interactive plot: 74 | 75 | ````markdown 76 | /// marimo-embed 77 | height: 800px 78 | mode: read 79 | app_width: wide 80 | 81 | ```python 82 | @app.cell 83 | def __(): 84 | import marimo as mo 85 | import numpy as np 86 | import matplotlib.pyplot as plt 87 | 88 | # Create interactive sliders 89 | freq = mo.ui.slider(1, 10, value=2, label="Frequency") 90 | amp = mo.ui.slider(0.1, 2, value=1, label="Amplitude") 91 | 92 | mo.hstack([freq, amp]) 93 | return 94 | 95 | @app.cell 96 | def __(): 97 | # Plot the sine wave 98 | x = np.linspace(0, 10, 1000) 99 | y = amp.value * np.sin(freq.value * x) 100 | 101 | plt.figure(figsize=(10, 6)) 102 | plt.plot(x, y) 103 | plt.title('Interactive Sine Wave') 104 | plt.xlabel('x') 105 | plt.ylabel('y') 106 | plt.grid(True) 107 | plt.gca() 108 | return 109 | ``` 110 | 111 | /// 112 | ```` 113 | 114 | /// marimo-embed 115 | height: 800px 116 | mode: read 117 | app_width: wide 118 | 119 | ```python 120 | @app.cell 121 | def __(): 122 | import marimo as mo 123 | import numpy as np 124 | import matplotlib.pyplot as plt 125 | 126 | # Create interactive sliders 127 | freq = mo.ui.slider(1, 10, value=2, label="Frequency") 128 | amp = mo.ui.slider(0.1, 2, value=1, label="Amplitude") 129 | 130 | mo.hstack([freq, amp]) 131 | return 132 | 133 | @app.cell 134 | def __(): 135 | # Plot the sine wave 136 | x = np.linspace(0, 10, 1000) 137 | y = amp.value * np.sin(freq.value * x) 138 | 139 | plt.figure(figsize=(10, 6)) 140 | plt.plot(x, y) 141 | plt.title('Interactive Sine Wave') 142 | plt.xlabel('x') 143 | plt.ylabel('y') 144 | plt.grid(True) 145 | plt.gca() 146 | return 147 | ``` 148 | 149 | /// 150 | 151 | ## Example with Hidden Code 152 | 153 | Here's an example that hides the code: 154 | 155 | /// marimo-embed 156 | height: 400px 157 | mode: read 158 | app_width: wide 159 | include_code: false 160 | 161 | ```python 162 | @app.cell 163 | def __(): 164 | import marimo as mo 165 | import numpy as np 166 | import matplotlib.pyplot as plt 167 | 168 | x = np.linspace(0, 10, 100) 169 | y = np.sin(x) 170 | plt.plot(x, y) 171 | plt.title('Simple Sine Wave') 172 | plt.gca() 173 | return 174 | ``` 175 | 176 | /// 177 | 178 | ## Configuration Options 179 | 180 | ### marimo-embed 181 | 182 | | Option | Description | Values | 183 | | --- | --- | --- | 184 | | `height` | Controls the height of the embed | - Named sizes: `small` (300px), `medium` (400px), `large` (600px),
`xlarge` (800px), `xxlarge` (1000px)
- Custom size: Any pixel value (e.g. `500px`) | 185 | | `mode` | Controls the interaction mode | - `read`: Read-only view (default)
- `edit`: Allows editing the code | 186 | | `app_width` | Controls the width of the marimo app | - `wide`: Standard width (default)
- `full`: Full width
- `compact`: Narrow width | 187 | | `include_code` | Controls whether code is included in the embed | - `true`: Show code (default)
- `false`: Hide code | 188 | 189 | ### marimo-embed-file 190 | 191 | The `marimo-embed-file` block is used to embed existing marimo files: 192 | 193 | /// marimo-embed-file 194 | filepath: getting-started/inlined.py 195 | height: 600px 196 | mode: read 197 | show_source: true 198 | /// 199 | 200 | | Option | Description | Default | 201 | | --- | --- | --- | 202 | | `filepath` | Path to the marimo file to embed | Required | 203 | | `show_source` | Whether to show the source code below the embed | `true` | 204 | | `include_code` | Controls whether code is included in the embed | `true` | 205 | -------------------------------------------------------------------------------- /docs/getting-started/configuration.md: -------------------------------------------------------------------------------- 1 | # Plugin Configuration 2 | 3 | The marimo plugin for MkDocs allows you to embed interactive marimo cells in your documentation. You can configure how these cells are rendered using various options. 4 | 5 | ## Global Configuration 6 | 7 | You can set global configuration options for the marimo plugin in your `mkdocs.yml` file. These options serve as defaults for all marimo code blocks but can be overridden by individual code fence options. 8 | 9 | ```yaml 10 | plugins: 11 | - marimo: 12 | enabled: true 13 | display_code: false 14 | display_output: true 15 | is_reactive: true 16 | marimo_version: '0.1.0' # Specify the version of marimo to use 17 | ``` 18 | 19 | ### Available Global Options 20 | 21 | | Option | Type | Description | Default | 22 | | -------------- | ------- | -------------------------------------------------------------------------------------------- | ----------------- | 23 | | enabled | boolean | Controls whether the marimo plugin is active. | `true` | 24 | | display_code | boolean | Controls whether the source code is displayed in the rendered output. | `false` | 25 | | display_output | boolean | Determines if the output of the code execution is included in the rendered HTML. | `true` | 26 | | is_reactive | boolean | Specifies whether code blocks will run with pyodide, making them interactive in the browser. | `true` | 27 | | marimo_version | string | Specifies the version of marimo to use. | Installed version | 28 | 29 | ## Code Fence Options 30 | 31 | When you create a marimo code fence in your markdown, you can specify options that control how the cell is rendered. These options are placed inside the opening code fence and will override the global configuration for that specific cell. 32 | 33 | ````markdown 34 | ```python {marimo display_code=true display_output=true is_reactive=false} 35 | # Your marimo code here 36 | ``` 37 | ```` 38 | 39 | ### Available Code Fence Options 40 | 41 | | Option | Type | Description | Example | 42 | | -------------- | ------- | ---------------------------------------------------------------------------------------------- | ---------------------- | 43 | | display_code | boolean | Controls whether the source code is displayed in the rendered output. | `display_code=true` | 44 | | display_output | boolean | Determines if the output of the code execution is included in the rendered HTML. | `display_output=false` | 45 | | is_reactive | boolean | Specifies whether this code block will run with pyodide, making it interactive in the browser. | `is_reactive=false` | 46 | 47 | ### Example 48 | 49 | Here's an example of a marimo code fence with all options specified: 50 | 51 | ````markdown 52 | ```python {marimo display_code=true display_output=true is_reactive=false} 53 | import marimo as mo 54 | 55 | slider = mo.ui.slider(1, 10, value=5) 56 | mo.md(f"Change the slider value: {slider}") 57 | ``` 58 | ```` 59 | 60 | This will render a marimo cell that displays the source code and shows the output, but is not interactive in the browser (overriding the global `is_reactive` setting). 61 | 62 | Remember that options specified in individual code fences will override the global settings for that specific cell. 63 | 64 | ## Releasing (for maintainers) 65 | 66 | To release a new version: 67 | 68 | 1. Bump the version: 69 | 70 | ```bash 71 | # Bump the version 72 | git add pyproject.toml 73 | git commit -m "chore: bump version to $(hatch --no-color version)" 74 | git push origin main 75 | ``` 76 | 77 | 2. Create and push a tag: 78 | 79 | ```bash 80 | git tag $(hatch --no-color version) 81 | git push origin $(hatch --no-color version) 82 | ``` 83 | 84 | This will trigger the GitHub Actions workflow to build and publish the package to PyPI. 85 | -------------------------------------------------------------------------------- /docs/getting-started/inline-notebook.md: -------------------------------------------------------------------------------- 1 | # Inline marimo notebooks 2 | 3 | !marimo_file ./inlined.py 4 | -------------------------------------------------------------------------------- /docs/getting-started/inlined.py: -------------------------------------------------------------------------------- 1 | import marimo 2 | 3 | app = marimo.App() 4 | 5 | 6 | @app.cell 7 | def __(mo): 8 | mo.md("You can also embed marimo apps inline with mkdocs!") 9 | return 10 | 11 | 12 | @app.cell 13 | def __(mo): 14 | mo.md( 15 | f""" 16 | This marimo notebook was embedded inline with mkdocs, by adding the 17 | 18 | ```markdown 19 | !marimo_file {__file__} 20 | ``` 21 | """ 22 | ) 23 | return 24 | 25 | 26 | @app.cell 27 | def __(mo): 28 | import functools 29 | 30 | import matplotlib.pyplot as plt 31 | import numpy as np 32 | 33 | @functools.cache 34 | def plotsin(amplitude, period): 35 | x = np.linspace(0, 2 * np.pi, 256) 36 | plt.plot(x, amplitude * np.sin(2 * np.pi / period * x)) 37 | plt.ylim(-2.2, 2.2) 38 | return plt.gca() 39 | 40 | period = 2 * np.pi 41 | amplitude = mo.ui.slider(1, 2, step=0.1, label="Amplitude") 42 | amplitude 43 | mo.show_code(amplitude) 44 | return 45 | 46 | 47 | @app.cell 48 | def __(mo): 49 | mo.show_code(plotsin(amplitude.value, period)) 50 | return 51 | 52 | 53 | @app.cell 54 | def __(): 55 | import marimo as mo 56 | 57 | return (mo,) 58 | 59 | 60 | if __name__ == "__main__": 61 | app.run() 62 | -------------------------------------------------------------------------------- /docs/getting-started/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | Add `mkdocs-marimo` to your `pyproject.toml` or `requirements.txt` file or simply install it using `pip` or `pixi`. The plugin will automatically install the minimally required version of `mkdocs` it is compatible with. 4 | 5 | ```bash 6 | pip install mkdocs-marimo 7 | # or 8 | uv pip install mkdocs-marimo 9 | # or 10 | pixi add mkdocs-marimo 11 | ``` 12 | 13 | ## Add the plugin to your `mkdocs.yml` 14 | 15 | ```yaml 16 | plugins: 17 | - marimo 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/getting-started/nav_notebook.py: -------------------------------------------------------------------------------- 1 | import marimo 2 | 3 | app = marimo.App() 4 | 5 | 6 | @app.cell 7 | def __(mo): 8 | mo.md(""" 9 | You can link directly to marimo notebooks, in the mkdocs.yml file. 10 | 11 | This content comes from a marimo notebook. 12 | """) 13 | return 14 | 15 | 16 | @app.cell 17 | def __(mo): 18 | mo.md( 19 | """ 20 | ```yaml 21 | nav: 22 | Examples: 23 | - Simple: simple_example.py 24 | - Complex: complex_example.py 25 | ``` 26 | """ 27 | ) 28 | return 29 | 30 | 31 | @app.cell 32 | def __(): 33 | import marimo as mo 34 | 35 | return (mo,) 36 | 37 | 38 | if __name__ == "__main__": 39 | app.run() 40 | -------------------------------------------------------------------------------- /docs/getting-started/quick-start.md: -------------------------------------------------------------------------------- 1 | # Quick Start 2 | 3 | This guide will help you get started with using marimo inside MkDocs. There are two main ways to use marimo in your documentation: 4 | 5 | 1. **Inline Code**: Write Python code directly in your Markdown files using marimo islands 6 | 2. **Playground Embed**: Embed full marimo notebooks using the marimo playground 7 | 8 | ## Prerequisites 9 | 10 | Before you begin, make sure you have: 11 | 12 | 1. Installed the marimo plugin for MkDocs (see [Installation](installation.md)) 13 | 2. Added the plugin to your `mkdocs.yml` file 14 | 15 | ## Approach 1: Inline Code with marimo Islands 16 | 17 | This is the simplest way to add interactive Python code to your documentation. Code is written directly in your Markdown files and executed in place. 18 | 19 | ### Basic Example 20 | 21 | ````markdown 22 | ```python {marimo} 23 | 2 + 2 24 | ``` 25 | ```` 26 | 27 | By default, marimo executes the code and displays the result. On page load, marimo re-hydrates the cell state and executes the code again using WebAssembly. 28 | 29 | ### Interactive Elements 30 | 31 | ````markdown 32 | ```python {marimo} 33 | import marimo as mo 34 | name = mo.ui.text(placeholder="Enter your name", debounce=False) 35 | name 36 | ``` 37 | 38 | ```python {marimo} 39 | mo.md(f"Hello, **{name.value or '__'}**!") 40 | ``` 41 | ```` 42 | 43 | This produces: 44 | 45 | ```python {marimo} 46 | import marimo as mo 47 | name = mo.ui.text(placeholder="Enter your name", debounce=False) 48 | name 49 | ``` 50 | 51 | ```python {marimo} 52 | mo.md(f"Hello, **{name.value or '__'}**!") 53 | ``` 54 | 55 | ## Approach 2: Embedding the marimo Playground 56 | 57 | For more complex notebooks or when you want to provide a full notebook experience, you can embed the marimo playground. This approach requires the `pymdown-extensions` package. 58 | 59 | ```bash 60 | pip install pymdown-extensions 61 | ``` 62 | 63 | ### Basic Playground Example 64 | 65 | ````markdown 66 | /// marimo-embed 67 | height: 400px 68 | mode: read 69 | app_width: wide 70 | 71 | ```python 72 | @app.cell 73 | def __(): 74 | import marimo as mo 75 | name = mo.ui.text(placeholder="Enter your name") 76 | name 77 | return 78 | 79 | @app.cell 80 | def __(): 81 | mo.md(f"Hello, **{name.value or '__'}**!") 82 | return 83 | ``` 84 | 85 | /// 86 | ```` 87 | 88 | See [Embedding the marimo playground](blocks.md) for more details on this approach. 89 | 90 | ## Which Approach Should You Choose? 91 | 92 | Use **Inline Code** when: 93 | 94 | - You want to show simple, focused examples 95 | 96 | Use **Playground Embed** when: 97 | 98 | - You want to show complete notebooks 99 | - You want users to be able to edit and experiment with the code 100 | - You want to embed existing .py notebooks 101 | 102 | ## Working with Data 103 | 104 | marimo integrates seamlessly with data analysis libraries. Here's an example using pandas: 105 | 106 | ````markdown 107 | ```python {marimo} 108 | import marimo as mo 109 | import pandas as pd 110 | data = pd.read_csv("https://huggingface.co/datasets/scikit-learn/Fish/resolve/main/Fish.csv") 111 | mo.ui.table(data, selection=None) 112 | ``` 113 | ```` 114 | 115 | ```python {marimo} 116 | import pandas as pd 117 | data = pd.read_csv("https://huggingface.co/datasets/scikit-learn/Fish/resolve/main/Fish.csv") 118 | mo.ui.table(data, selection=None) 119 | ``` 120 | 121 | ## Leveraging Reactivity 122 | 123 | marimo's reactivity allows cells to update automatically when inputs or code changes. Here's a visualization example: 124 | 125 | ```python {marimo display_code} 126 | import functools 127 | import numpy as np 128 | import matplotlib.pyplot as plt 129 | 130 | @functools.cache 131 | def plotsin(amplitude, period): 132 | # You can edit this code! 133 | x = np.linspace(0, 2 * np.pi, 256) 134 | plt.plot(x, amplitude * np.sin(2 * np.pi / period * x)) 135 | plt.ylim(-2.2, 2.2) 136 | return plt.gca() 137 | 138 | period = 2 * np.pi 139 | amplitude = mo.ui.slider(1, 2, step=0.1); amplitude 140 | ``` 141 | 142 | ```python {marimo display_code} 143 | plotsin(amplitude.value, period) 144 | ``` 145 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # marimo for MkDocs 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/mkdocs-marimo.svg)](https://pypi.org/project/mkdocs-marimo/) 4 | [![License](https://img.shields.io/github/license/marimo-team/mkdocs-marimo)](https://github.com/marimo-team/mkdocs-marimo/blob/main/LICENSE) 5 | [![GitHub stars](https://img.shields.io/github/stars/marimo-team/mkdocs-marimo.svg)](https://github.com/marimo-team/mkdocs-marimo) 6 | [![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://marimo.io/discord) 7 | 8 | 9 | 10 | A MkDocs plugin that brings interactive Python code execution to your documentation using [marimo](https://github.com/marimo-team/marimo). 11 | 12 | ## Features 13 | 14 | - ⚡️ Two flexible ways to embed Python code: 15 | - **Inline Code**: Write and execute Python code directly in your Markdown files 16 | - **Playground Embed**: Embed full marimo notebooks with the marimo playground 17 | - 🔄 Interactive widgets with real-time updates 18 | - 📊 Seamless integration with data visualization libraries 19 | - 🎨 Customizable styling to match your documentation theme 20 | - 🚀 Easy to set up and use 21 | 22 | ## Quick Start 23 | 24 | ### 1. Install the plugin 25 | 26 | === "pip" 27 | 28 | ```bash 29 | pip install mkdocs-marimo 30 | ``` 31 | 32 | === "uv" 33 | 34 | ```bash 35 | uv pip install mkdocs-marimo 36 | ``` 37 | 38 | === "pixi" 39 | 40 | ```bash 41 | pixi add mkdocs-marimo 42 | ``` 43 | 44 | === "conda" 45 | 46 | ```bash 47 | conda install -c conda-forge mkdocs-marimo 48 | ``` 49 | 50 | ### 2. Add the plugin to your `mkdocs.yml` 51 | 52 | ```yaml 53 | plugins: 54 | - marimo 55 | ``` 56 | 57 | ### 3. Write interactive Python code in your Markdown files 58 | 59 | ````markdown 60 | ```python {marimo} 61 | import marimo as mo 62 | 63 | name = mo.ui.text(placeholder="Enter your name") 64 | name 65 | ``` 66 | 67 | ```python {marimo} 68 | mo.md(f"Hello, **{name.value or '__'}**!") 69 | ``` 70 | ```` 71 | 72 | ## Example: Interactive Sine Wave 73 | 74 | Instead of static code blocks, create interactive visualizations: 75 | 76 | ```python {marimo} 77 | import marimo as mo 78 | import functools 79 | import numpy as np 80 | import matplotlib.pyplot as plt 81 | 82 | @functools.cache 83 | def plotsin(amplitude, period): 84 | x = np.linspace(0, 2 * np.pi, 256) 85 | plt.plot(x, amplitude * np.sin(2 * np.pi / period * x)) 86 | plt.ylim(-2.2, 2.2) 87 | return plt.gca() 88 | 89 | period = 2 * np.pi 90 | amplitude = mo.ui.slider(1, 2, step=0.1, label="Amplitude") 91 | amplitude 92 | ``` 93 | 94 | ```python {marimo} 95 | plotsin(amplitude.value, period) 96 | ``` 97 | 98 | ## Why mkdocs-marimo? 99 | 100 | - **Interactive Documentation**: Engage your readers with live, interactive code examples. 101 | - **Real-time Feedback**: Instantly see the effects of code changes and parameter adjustments. 102 | - **Enhanced Learning**: Improve understanding through hands-on experimentation within the documentation. 103 | 104 | ## Getting Started 105 | 106 | Check out our [Getting Started guide](getting-started/quick-start.md) to learn more about using marimo inside MkDocs. 107 | 108 | ## Community 109 | 110 | - [GitHub Issues](https://github.com/marimo-team/mkdocs-marimo/issues): Report bugs or request features 111 | - [Discord](https://marimo.io/discord): Ask questions and share ideas 112 | 113 | ## License 114 | 115 | mkdocs-marimo is released under the [Apache License 2.0](https://github.com/marimo-team/mkdocs-marimo/blob/main/LICENSE). 116 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: marimo mkdocs 2 | site_url: https://marimo-team.github.io/mkdocs-marimo 3 | site_author: marimo team 4 | site_description: The next generation of Python notebooks 5 | edit_uri: edit/main/docs/ 6 | 7 | repo_name: mkdocs-marimo 8 | repo_url: https://github.com/marimo-team/mkdocs-marimo 9 | 10 | docs_dir: docs/ 11 | copyright: '© marimo 2024' 12 | 13 | theme: 14 | name: material 15 | font: 16 | text: PT Sans 17 | code: Fira Mono 18 | logo: assets/android-chrome-192x192.png 19 | favicon: assets/favicon.ico 20 | features: 21 | - navigation.instant 22 | - navigation.instant.prefetch 23 | - navigation.tracking 24 | - navigation.path 25 | - navigation.footer 26 | - navigation.top 27 | - navigation.indexes 28 | - navigation.tabs 29 | - navigation.tabs.sticky 30 | - content.code.copy 31 | - content.code.annotate 32 | - toc.integrate 33 | - toc.follow 34 | - search.highlight 35 | - search.share 36 | - search.suggest 37 | - announce.dismiss 38 | palette: 39 | - media: '(prefers-color-scheme)' 40 | toggle: 41 | icon: material/brightness-auto 42 | name: Switch to light mode 43 | - media: '(prefers-color-scheme: light)' 44 | scheme: default 45 | primary: teal 46 | accent: pink 47 | toggle: 48 | icon: material/brightness-7 49 | name: Switch to dark mode 50 | - media: '(prefers-color-scheme: dark)' 51 | scheme: slate 52 | primary: deep-purple 53 | accent: pink 54 | toggle: 55 | icon: material/brightness-4 56 | name: Switch to system preference 57 | 58 | nav: 59 | - Overview: index.md 60 | - Getting Started: 61 | - Installation: getting-started/installation.md 62 | - Quick Start: getting-started/quick-start.md 63 | - Configuration: getting-started/configuration.md 64 | - Embedding the marimo playground: getting-started/blocks.md 65 | - Inline Notebook: getting-started/inline-notebook.md 66 | - Notebooks in Navigation: getting-started/nav_notebook.py 67 | 68 | extra: 69 | generator: false 70 | social: 71 | - icon: fontawesome/brands/github 72 | link: https://github.com/marimo-team/marimo 73 | - icon: fontawesome/brands/discord 74 | link: https://marimo.io/discord?ref=docs 75 | - icon: material/email-newsletter 76 | link: https://marimo.io/newsletter 77 | - icon: material/forum 78 | link: https://community.marimo.io 79 | - icon: fontawesome/brands/mastodon 80 | link: https://mastodon.social/@marimo_io 81 | - icon: fontawesome/brands/twitter 82 | link: https://twitter.com/marimo_io 83 | - icon: fontawesome/brands/linkedin 84 | link: https://www.linkedin.com/company/marimo-io 85 | - icon: fontawesome/brands/python 86 | link: https://pypi.org/project/marimo/ 87 | 88 | plugins: 89 | - search 90 | - tags 91 | - marimo: {} 92 | 93 | watch: 94 | - mkdocs_marimo 95 | 96 | # hooks: 97 | # - mkdocs_marimo/plugin.py 98 | 99 | markdown_extensions: 100 | - admonition 101 | - attr_list 102 | - def_list 103 | - footnotes 104 | - md_in_html 105 | - meta 106 | - sane_lists 107 | - toc: 108 | permalink: true 109 | - pymdownx.arithmatex: 110 | generic: true 111 | - pymdownx.betterem: 112 | smart_enable: all 113 | - pymdownx.caret 114 | - pymdownx.details 115 | - pymdownx.emoji 116 | - pymdownx.highlight: 117 | anchor_linenums: true 118 | line_spans: __span 119 | pygments_lang_class: true 120 | - pymdownx.inlinehilite 121 | - pymdownx.keys 122 | - pymdownx.magiclink 123 | - pymdownx.mark 124 | - pymdownx.smartsymbols 125 | - pymdownx.superfences: 126 | custom_fences: 127 | - class: mermaid 128 | name: mermaid 129 | - pymdownx.tabbed: 130 | alternate_style: true 131 | - pymdownx.tasklist: 132 | custom_checkbox: true 133 | - pymdownx.tilde 134 | 135 | validation: 136 | nav: 137 | omitted_files: warn 138 | not_found: warn 139 | absolute_links: warn 140 | links: 141 | not_found: warn 142 | anchors: warn 143 | absolute_links: warn 144 | unrecognized_links: warn 145 | -------------------------------------------------------------------------------- /mkdocs_marimo/__init__.py: -------------------------------------------------------------------------------- 1 | import importlib.metadata 2 | import warnings 3 | 4 | try: 5 | __version__ = importlib.metadata.version(__name__) 6 | except importlib.metadata.PackageNotFoundError as e: 7 | warnings.warn(f"Could not determine version of {__name__}\n{e!s}", stacklevel=2) 8 | __version__ = "unknown" 9 | -------------------------------------------------------------------------------- /mkdocs_marimo/blocks.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | import urllib.parse 3 | import xml.etree.ElementTree as etree 4 | from typing import Any, Dict, List, Union, cast 5 | 6 | from pymdownx.blocks import BlocksExtension # type: ignore 7 | from pymdownx.blocks.block import Block, type_boolean, type_string, type_string_in # type: ignore 8 | 9 | 10 | class BaseMarimoBlock(Block): 11 | """Base class for marimo embed blocks""" 12 | 13 | OPTIONS: Dict[str, List[Union[str, Any]]] = { 14 | "height": [ 15 | "medium", 16 | type_string, # Allow any string value for height (e.g. "500px", "small", etc.) 17 | ], 18 | "mode": ["read", type_string_in(["read", "edit"])], 19 | "include_code": [True, type_boolean], 20 | } 21 | 22 | def on_create(self, parent: etree.Element) -> etree.Element: 23 | container = etree.SubElement(parent, "div") 24 | container.set("class", "marimo-embed-container") 25 | return container 26 | 27 | def on_add(self, block: etree.Element) -> etree.Element: 28 | return block 29 | 30 | def _create_iframe(self, block: etree.Element, url: str) -> None: 31 | # Clear existing content 32 | block.text = None 33 | for child in block: 34 | block.remove(child) 35 | 36 | # Add iframe 37 | height: str = cast(str, self.options["height"]) 38 | include_code: bool = cast(bool, self.options.get("include_code", True)) 39 | iframe = etree.SubElement(block, "iframe") 40 | 41 | # Convert named sizes to pixels 42 | height_map = { 43 | "small": "300px", 44 | "medium": "400px", 45 | "large": "600px", 46 | "xlarge": "800px", 47 | "xxlarge": "1000px", 48 | } 49 | iframe_height = height_map.get(height, height) 50 | if not iframe_height.endswith("px"): 51 | iframe_height += "px" 52 | 53 | # Adjust URL to include include_code parameter 54 | url_parts = list(urllib.parse.urlparse(url)) 55 | query = dict(urllib.parse.parse_qsl(url_parts[4])) 56 | query["include-code"] = str(include_code).lower() 57 | url_parts[4] = urllib.parse.urlencode(query) 58 | final_url = urllib.parse.urlunparse(url_parts) 59 | 60 | iframe.set("class", "marimo-embed") 61 | iframe.set("src", final_url) 62 | iframe.set( 63 | "allow", 64 | "camera; geolocation; microphone; fullscreen; autoplay; encrypted-media; picture-in-picture; clipboard-read; clipboard-write", 65 | ) 66 | iframe.set("width", "100%") 67 | iframe.set("height", iframe_height) 68 | iframe.set("frameborder", "0") 69 | iframe.set("style", "display: block; margin: 0 auto;") 70 | 71 | def on_markdown(self) -> str: 72 | return "raw" 73 | 74 | 75 | class MarimoEmbedBlock(BaseMarimoBlock): 76 | NAME: str = "marimo-embed" 77 | OPTIONS: Dict[str, List[Union[str, Any]]] = { 78 | **BaseMarimoBlock.OPTIONS, 79 | "app_width": ["wide", type_string_in(["wide", "full", "compact"])], 80 | } 81 | 82 | def on_end(self, block: etree.Element) -> None: 83 | code = block.text.strip() if block.text else "" 84 | if code.startswith("```python"): 85 | code = code[9:] 86 | code = code[:-3] 87 | code = textwrap.dedent(code) 88 | 89 | app_width: str = cast(str, self.options["app_width"]) 90 | mode: str = cast(str, self.options["mode"]) 91 | url = create_marimo_app_url( 92 | code=create_marimo_app_code(code=code, app_width=app_width), 93 | mode=mode, 94 | ) 95 | self._create_iframe(block, url) 96 | 97 | 98 | class MarimoEmbedFileBlock(BaseMarimoBlock): 99 | NAME: str = "marimo-embed-file" 100 | OPTIONS: Dict[str, List[Union[str, Any]]] = { 101 | **BaseMarimoBlock.OPTIONS, 102 | "filepath": ["", type_string], 103 | "show_source": ["true", type_string_in(["true", "false"])], 104 | } 105 | 106 | def on_end(self, block: etree.Element) -> None: 107 | filepath = cast(str, self.options["filepath"]) 108 | if not filepath: 109 | raise ValueError("File path must be provided") 110 | 111 | # Read from project root 112 | try: 113 | with open(filepath, "r") as f: 114 | code = f.read() 115 | except FileNotFoundError: 116 | raise ValueError(f"File not found: {filepath}") 117 | 118 | mode: str = cast(str, self.options["mode"]) 119 | url = create_marimo_app_url(code=code, mode=mode) 120 | self._create_iframe(block, url) 121 | 122 | # Add source code section if enabled 123 | show_source: str = cast(str, self.options.get("show_source", "true")) 124 | if show_source == "true": 125 | details = etree.SubElement(block, "details") 126 | summary = etree.SubElement(details, "summary") 127 | summary.text = f"Source code for `{filepath}`" 128 | 129 | copy_paste_container = etree.SubElement(details, "p") 130 | copy_paste_container.text = "Tip: paste this code into an empty cell, and the marimo editor will create cells for you" 131 | 132 | code_container = etree.SubElement(details, "pre") 133 | code_container.set("class", "marimo-source-code") 134 | code_block = etree.SubElement(code_container, "code") 135 | code_block.set("class", "language-python") 136 | code_block.text = code 137 | 138 | 139 | def uri_encode_component(code: str) -> str: 140 | return urllib.parse.quote(code, safe="~()*!.'") 141 | 142 | 143 | def create_marimo_app_code( 144 | *, 145 | code: str, 146 | app_width: str = "wide", 147 | ) -> str: 148 | header = "\n".join( 149 | [ 150 | "import marimo", 151 | f'app = marimo.App(width="{app_width}")', 152 | "", 153 | ] 154 | ) 155 | 156 | # Only add import if not already present 157 | if "import marimo as mo" not in code: 158 | header += "\n".join( 159 | [ 160 | "", 161 | "@app.cell", 162 | "def __():", 163 | " import marimo as mo", 164 | " return", 165 | ] 166 | ) 167 | return header + code 168 | 169 | 170 | def create_marimo_app_url(code: str, mode: str = "read") -> str: 171 | encoded_code = uri_encode_component(code) 172 | return f"https://marimo.app/?code={encoded_code}&embed=true&mode={mode}" 173 | 174 | 175 | class MarimoBlocksExtension(BlocksExtension): 176 | def extendMarkdownBlocks(self, md: Any, block_mgr: Any) -> None: 177 | block_mgr.register(MarimoEmbedBlock, self.getConfigs()) 178 | block_mgr.register(MarimoEmbedFileBlock, self.getConfigs()) 179 | 180 | 181 | def makeExtension(*args: Any, **kwargs: Any) -> MarimoBlocksExtension: 182 | return MarimoBlocksExtension(*args, **kwargs) 183 | -------------------------------------------------------------------------------- /mkdocs_marimo/plugin.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import os 4 | import re 5 | from typing import Any, Dict, Optional 6 | 7 | import htmlmin 8 | import marimo 9 | from mkdocs.config.base import Config as BaseConfig 10 | from mkdocs.config.config_options import Type as OptionType 11 | from mkdocs.config.defaults import MkDocsConfig 12 | from mkdocs.plugins import BasePlugin 13 | from mkdocs.structure.files import File, Files 14 | from mkdocs.structure.pages import Page 15 | 16 | from .blocks import MarimoBlocksExtension 17 | 18 | log = logging.getLogger("mkdocs.plugins.marimo") 19 | 20 | CODE_FENCE_REGEX = re.compile(r"```python\s*{marimo([^}]*)}\n([\s\S]+?)```", flags=re.MULTILINE) 21 | 22 | 23 | def is_inside_four_backticks(markdown: str, start_pos: int) -> bool: 24 | backticks = "````" 25 | before = markdown[:start_pos] 26 | return before.count(backticks) % 2 == 1 27 | 28 | 29 | def find_marimo_code_fences(markdown: str) -> list[re.Match[str]]: 30 | matches: list[Any] = [] 31 | for match in CODE_FENCE_REGEX.finditer(markdown): 32 | if not is_inside_four_backticks(markdown, match.start()): 33 | matches.append(match) 34 | return matches 35 | 36 | 37 | def collect_marimo_code(markdown: str) -> tuple[list[str], list[re.Match[str]]]: 38 | matches = find_marimo_code_fences(markdown) 39 | code_blocks = [match.group(2).strip() for match in matches] 40 | return code_blocks, matches 41 | 42 | 43 | class MarimoPluginConfig(BaseConfig): 44 | enabled = OptionType(bool, default=True) 45 | marimo_version = OptionType(str, default=marimo.__version__) 46 | display_code = OptionType(bool, default=False) 47 | display_output = OptionType(bool, default=True) 48 | is_reactive = OptionType(bool, default=True) 49 | use_pymdown_blocks = OptionType(bool, default=True) 50 | 51 | 52 | class MarimoPlugin(BasePlugin[MarimoPluginConfig]): 53 | replacements: dict[str, list[str]] = {} 54 | 55 | def __init__(self): 56 | super().__init__() 57 | if isinstance(self.config, dict): 58 | plugin_config = MarimoPluginConfig() 59 | plugin_config.load_dict(self.config) 60 | self.config = plugin_config 61 | 62 | def on_config(self, config: MkDocsConfig) -> MkDocsConfig: 63 | if not self.config.enabled: 64 | return config 65 | 66 | if self.config.use_pymdown_blocks: 67 | # Add MarimoBlocksExtension to markdown extensions if pymdown.blocks is available 68 | try: 69 | from importlib.util import find_spec 70 | 71 | if find_spec("pymdownx.blocks") is not None: 72 | if not any( 73 | isinstance(ext, MarimoBlocksExtension) for ext in config.markdown_extensions 74 | ): 75 | config.markdown_extensions.append(MarimoBlocksExtension()) 76 | except ImportError: 77 | log.warning("[marimo] pymdown.blocks not found, skipping blocks support") 78 | 79 | return config 80 | 81 | def handle_marimo_file(self, page: Page) -> str: 82 | if page.abs_url is None: 83 | raise ValueError("Page has no abs_url") 84 | generator = marimo.MarimoIslandGenerator.from_file(page.abs_url, display_code=False) 85 | return generator.render_html(max_width="none", version_override=self.config.marimo_version) 86 | 87 | def on_page_markdown( 88 | self, markdown: str, /, *, page: Page, config: MkDocsConfig, files: Any 89 | ) -> str: 90 | if not self.config.enabled: 91 | return markdown 92 | 93 | del files 94 | 95 | if ( 96 | page.abs_url is not None 97 | and os.path.exists(page.abs_url) 98 | and _is_marimo_file(page.abs_url) 99 | ): 100 | return self.handle_marimo_file(page) 101 | 102 | log.info("[marimo] on_page_markdown " + str(page.abs_url)) 103 | 104 | # Process !marimo_file directives 105 | markdown = self.process_marimo_file_directives(markdown, page, config) 106 | 107 | if page.abs_url is None: 108 | return markdown 109 | 110 | generator = marimo.MarimoIslandGenerator() 111 | replacements: list[str] = [] 112 | self.replacements[page.abs_url] = replacements 113 | outputs: list[Any] = [] 114 | code_blocks, matches = collect_marimo_code(markdown) 115 | 116 | for code in code_blocks: 117 | outputs.append(generator.add_code(code)) 118 | 119 | asyncio.run(generator.build()) 120 | 121 | def match_equal(first: re.Match[str], second: re.Match[str]) -> bool: 122 | return first.start() == second.start() and first.end() == second.end() 123 | 124 | def marimo_repl(match: re.Match[str], outputs: list[Any]) -> str: 125 | if is_inside_four_backticks(markdown, match.start()): 126 | return match.group(0) 127 | 128 | # Get default options from plugin config 129 | default_options = { 130 | "display_code": self.config.display_code, 131 | "display_output": self.config.display_output, 132 | "is_reactive": self.config.is_reactive, 133 | } 134 | 135 | # Parse options from the code fence 136 | fence_options = self.parse_options(match.group(1)) 137 | 138 | # Merge default options with fence options, giving priority to fence options 139 | options = {**default_options, **fence_options} 140 | 141 | index = next(i for i, m in enumerate(matches) if match_equal(m, match)) 142 | output = outputs[index] 143 | 144 | # Filter out unsupported kwargs by inspecting the render method 145 | import inspect 146 | 147 | supported_kwargs = {} 148 | render_params = inspect.signature(output.render).parameters 149 | 150 | for key, value in options.items(): 151 | if key in render_params: 152 | supported_kwargs[key] = value 153 | else: 154 | log.warning(f"[marimo] Unsupported option '{key}' for render method. Ignoring.") 155 | 156 | html = output.render(**supported_kwargs) 157 | minified_html = htmlmin.minify(str(html), remove_empty_space=True) 158 | replacements.append(minified_html) 159 | return f"" 160 | 161 | return CODE_FENCE_REGEX.sub(lambda m: marimo_repl(m, outputs), markdown) 162 | 163 | def process_marimo_file_directives( 164 | self, markdown: str, page: Page, config: MkDocsConfig 165 | ) -> str: 166 | def replace_marimo_file(match: re.Match[str]) -> str: 167 | file_path = match.group(1).strip() 168 | full_path = self.resolve_marimo_file_path(file_path, page, config.docs_dir) 169 | if full_path and os.path.exists(full_path): 170 | return self.generate_marimo_body(full_path) 171 | else: 172 | log.warning(f"marimo file not found: {file_path}") 173 | return f""" 174 | !!! warning 175 | 176 | The marimo file `{full_path}` could not be found. 177 | """ 178 | 179 | pattern = r"!marimo_file\s+(.+)" 180 | return re.sub(pattern, replace_marimo_file, markdown) 181 | 182 | def resolve_marimo_file_path(self, file_path: str, page: Page, docs_dir: str) -> str: 183 | if file_path.startswith("/"): 184 | # Treat as absolute path from the docs_dir 185 | return os.path.normpath(os.path.join(docs_dir, file_path[1:])) 186 | elif page.file.abs_src_path: 187 | # Relative path, resolve from the current page's directory 188 | base_dir = os.path.dirname(page.file.abs_src_path) 189 | return os.path.normpath(os.path.join(base_dir, file_path)) 190 | return "" 191 | 192 | def generate_marimo_body(self, file_path: str) -> str: 193 | generator = marimo.MarimoIslandGenerator.from_file(file_path, display_code=False) 194 | return generator.render_body(include_init_island=True, max_width="none") 195 | 196 | def on_post_page(self, output: str, /, *, page: Page, config: MkDocsConfig) -> str: 197 | if not self.config.enabled: 198 | return output 199 | 200 | if page.abs_url is None: 201 | return output 202 | 203 | log.info("[marimo] on_post_page " + str(page.abs_url)) 204 | generator = marimo.MarimoIslandGenerator() 205 | header = generator.render_head(version_override=self.config.marimo_version) 206 | 207 | # Add the extra header to the output 208 | output = output.replace("", f"{header}\n") 209 | 210 | replacesments: list[str] = self.replacements.get(page.abs_url, []) 211 | for idx, replacement in enumerate(replacesments): 212 | output = output.replace(f"", replacement, 1) 213 | return output 214 | 215 | def parse_options(self, options_string: str) -> Dict[str, Any]: 216 | options: dict[str, Any] = {} 217 | for option in options_string.split(): 218 | if "=" in option: 219 | key, value = option.split("=", 1) 220 | options[key.strip()] = self.parse_value(value.strip()) 221 | else: 222 | options[option.strip()] = True 223 | return options 224 | 225 | def parse_value(self, value: str) -> Any: 226 | if value.lower() == "true": 227 | return True 228 | elif value.lower() == "false": 229 | return False 230 | elif value.isdigit(): 231 | return int(value) 232 | elif value.replace(".", "", 1).isdigit(): 233 | return float(value) 234 | else: 235 | return value 236 | 237 | def on_files(self, files: Files, config: MkDocsConfig) -> Files: 238 | # Process Python files in navigation 239 | nav = config.nav 240 | if nav is not None: 241 | self.process_nav_items(nav, files, config) 242 | return files 243 | 244 | def process_nav_items(self, items: list[Any], files: Files, config: MkDocsConfig) -> None: 245 | for item in items: 246 | if isinstance(item, dict): 247 | for key, value in item.items(): 248 | if isinstance(value, list): 249 | self.process_nav_items(value, files, config) 250 | elif isinstance(value, str) and value.endswith(".py"): 251 | self.process_python_file(value, files, config) 252 | # Update the navigation to point to the virtual markdown file 253 | item[key] = value[:-3] + ".md" 254 | 255 | def process_python_file(self, file_path: str, files: Files, config: MkDocsConfig) -> None: 256 | abs_src_path = os.path.join(config.docs_dir, file_path) 257 | if os.path.exists(abs_src_path) and _is_marimo_file(abs_src_path): 258 | # Create a virtual markdown file 259 | md_path = file_path[:-3] + ".md" 260 | md_content = f"!marimo_file /{file_path}" 261 | 262 | # Create a virtual File object 263 | virtual_file = VirtualFile( 264 | path=md_path, 265 | src_dir=config.docs_dir, 266 | dest_dir=config.site_dir, 267 | use_directory_urls=config.use_directory_urls, 268 | content=md_content, 269 | ) 270 | 271 | # Add the virtual markdown file to MkDocs' file collection 272 | files.append(virtual_file) 273 | 274 | # Remove the original Python file from MkDocs' file collection 275 | found_file = next((f for f in files if f.src_path == file_path), None) 276 | if found_file: 277 | files.remove(found_file) 278 | 279 | 280 | class VirtualFile(File): 281 | def __init__( 282 | self, 283 | path: str, 284 | src_dir: str, 285 | dest_dir: str, 286 | use_directory_urls: bool, 287 | content: str, 288 | ): 289 | super().__init__(path, src_dir, dest_dir, use_directory_urls) 290 | self._content = content 291 | 292 | def read_text(self) -> str: 293 | return str(self._content) 294 | 295 | @property 296 | def abs_src_path(self) -> str: 297 | # Return a fake path that doesn't exist on disk 298 | assert self.src_dir is not None 299 | return os.path.join(self.src_dir, "virtual", self.src_path) 300 | 301 | def copy_file(self, dirty: bool = False) -> None: 302 | # Don't copy the file, as it doesn't exist on disk 303 | pass 304 | 305 | 306 | # Hooks for development 307 | def on_startup(command: str, dirty: bool) -> None: 308 | log.info("[marimo][development] plugin started.") 309 | 310 | 311 | def on_page_markdown(markdown: str, page: Any, config: MkDocsConfig, files: Any) -> str: 312 | log.info("[marimo][development] plugin started.") 313 | plugin = MarimoPlugin() 314 | return plugin.on_page_markdown(markdown, page=page, config=config, files=files) 315 | 316 | 317 | def on_post_page(output: str, page: Page, config: MkDocsConfig) -> str: 318 | log.info("[marimo][development] plugin started.") 319 | plugin = MarimoPlugin() 320 | return plugin.on_post_page(output, page=page, config=config) 321 | 322 | 323 | def on_files(files: Files, config: MkDocsConfig) -> Files: 324 | log.info("[marimo][development] plugin started.") 325 | plugin = MarimoPlugin() 326 | return plugin.on_files(files, config) 327 | 328 | 329 | def _is_marimo_file(filepath: Optional[str]) -> bool: 330 | if filepath is None: 331 | return False 332 | 333 | # Handle python 334 | if filepath.endswith(".py"): 335 | with open(filepath, "rb") as file: 336 | return b"app = marimo.App(" in file.read() 337 | 338 | # Handle markdown 339 | if filepath.endswith(".md"): 340 | with open(filepath, "r") as file: 341 | return "marimo-version:" in file.read() 342 | 343 | return False 344 | -------------------------------------------------------------------------------- /pixi.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | channels = ["conda-forge"] 3 | name = "mkdocs-marimo" 4 | platforms = ["linux-64", "linux-aarch64", "osx-arm64", "osx-64", "win-64"] 5 | 6 | [tasks] 7 | postinstall = "pip install --no-deps --no-build-isolation --disable-pip-version-check -e ." 8 | 9 | [dependencies] 10 | htmlmin2 = ">=0.1.13,<0.2" 11 | marimo = ">=0.9.11" 12 | mkdocs = ">=1.6.1,<2" 13 | python = ">=3.9" 14 | 15 | [host-dependencies] 16 | hatchling = "*" 17 | pip = "*" 18 | 19 | [feature.test.dependencies] 20 | mypy = "*" 21 | pytest = "*" 22 | [feature.test.tasks] 23 | test = "pytest -xs" 24 | 25 | [feature.docs.dependencies] 26 | mkdocs = "*" 27 | mkdocs-material = "*" 28 | # For examples 29 | matplotlib-base = "*" 30 | numpy = "*" 31 | pandas = "*" 32 | [feature.docs.tasks] 33 | docs = "mkdocs serve" 34 | docs-build = "mkdocs build" 35 | 36 | [feature.lint.dependencies] 37 | pre-commit = "*" 38 | pre-commit-hooks = "*" 39 | ruff = "*" 40 | taplo = "*" 41 | typos = "*" 42 | [feature.lint.tasks] 43 | pre-commit-install = "pre-commit install" 44 | pre-commit-run = "pre-commit run -a" 45 | 46 | [feature.build.dependencies] 47 | hatchling = "*" 48 | python-build = "*" 49 | 50 | [feature.py39.dependencies] 51 | python = "3.9.*" 52 | [feature.py310.dependencies] 53 | python = "3.10.*" 54 | [feature.py311.dependencies] 55 | python = "3.11.*" 56 | [feature.py312.dependencies] 57 | python = "3.12.*" 58 | [feature.py313.dependencies] 59 | python = "3.13.*" 60 | 61 | [environments] 62 | build = { features = ["build"], no-default-feature = true } 63 | default = ["test", "py312", "docs"] 64 | docs = ["docs"] 65 | lint = { features = ["lint"], no-default-feature = true } 66 | py39 = ["test", "py39"] 67 | py310 = ["test", "py310"] 68 | py311 = ["test", "py311"] 69 | py312 = ["test", "py312"] 70 | py313 = ["test", "py313"] 71 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "hatchling.build" 3 | requires = ["hatchling"] 4 | 5 | [project] 6 | authors = [{ name = "marimo team", email = "contact@marimo.io" }] 7 | classifiers = [ 8 | "License :: OSI Approved :: MIT License", 9 | "Operating System :: POSIX :: Linux", 10 | "Programming Language :: Python", 11 | "Programming Language :: Python :: 3", 12 | "Programming Language :: Python :: 3 :: Only", 13 | "Programming Language :: Python :: 3.9", 14 | "Programming Language :: Python :: 3.10", 15 | "Programming Language :: Python :: 3.11", 16 | "Programming Language :: Python :: 3.12", 17 | "Topic :: Software Development :: Libraries :: Python Modules", 18 | ] 19 | dependencies = [ 20 | "htmlmin2>=0.1.13,<0.2", 21 | "marimo>=0.8.15", 22 | "mkdocs>=1.5.2,<2", 23 | "pymdown-extensions>=10.7", 24 | ] 25 | description = "MkDocs marimo plugin" 26 | license = "Apache-2.0" 27 | name = "mkdocs-marimo" 28 | readme = "README.md" 29 | requires-python = ">=3.9" 30 | version = "0.2.1" 31 | 32 | [project.entry-points."mkdocs.plugins"] 33 | marimo = "mkdocs_marimo.plugin:MarimoPlugin" 34 | 35 | [project.urls] 36 | Documentation = "https://github.com/marimo-team/mkdocs-marimo#readme" 37 | Homepage = "https://github.com/marimo-team/mkdocs-marimo" 38 | Source = "https://github.com/marimo-team/mkdocs-marimo" 39 | Tracker = "https://github.com/marimo-team/mkdocs-marimo/issues" 40 | 41 | [tool.hatch.build.targets.sdist] 42 | include = ["/mkdocs_marimo", "/tests"] 43 | [tool.hatch.build.targets.wheel] 44 | exclude = ["/tests"] 45 | 46 | [tool.ruff] 47 | line-length = 100 48 | [tool.ruff.format] 49 | indent-style = "space" 50 | quote-style = "double" 51 | [tool.ruff.lint] 52 | ignore = [ 53 | "F841", # https://docs.astral.sh/ruff/rules/unused-variable/ 54 | "F821", # https://docs.astral.sh/ruff/rules/undefined-name/ 55 | ] 56 | 57 | [tool.mypy] 58 | disable_error_code = "no-redef" 59 | 60 | [[tool.mypy.overrides]] 61 | ignore_missing_imports = true 62 | module = ["htmlmin"] 63 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests suite for `mkdocstrings`.""" 2 | 3 | from pathlib import Path 4 | 5 | TESTS_DIR = Path(__file__).parent 6 | TMP_DIR = TESTS_DIR / "tmp" 7 | FIXTURES_DIR = TESTS_DIR / "fixtures" 8 | -------------------------------------------------------------------------------- /tests/fixtures/__init__.py: -------------------------------------------------------------------------------- 1 | """Some fixtures for tests.""" 2 | -------------------------------------------------------------------------------- /tests/fixtures/app.py: -------------------------------------------------------------------------------- 1 | import marimo 2 | 3 | app = marimo.App() 4 | 5 | 6 | @app.cell 7 | def __(mo): 8 | mo.md("# Welcome to marimo! 🌊🍃") 9 | return 10 | 11 | 12 | @app.cell 13 | def __(mo): 14 | slider = mo.ui.slider(1, 22) 15 | return (slider,) 16 | 17 | 18 | @app.cell 19 | def __(mo, slider): 20 | mo.md( 21 | f""" 22 | marimo is a Python library for creating reactive and interactive 23 | notebooks and apps. 24 | 25 | Unlike traditional notebooks, marimo notebooks **run 26 | automatically** when you modify them or 27 | interact with UI elements, like this slider: {slider}. 28 | 29 | {"##" + "🍃" * slider.value} 30 | """ 31 | ) 32 | return 33 | 34 | 35 | @app.cell 36 | def __(mo): 37 | mo.accordion( 38 | { 39 | "A notebook or an app?": ( 40 | """ 41 | Because marimo notebooks react to changes and UI interactions, 42 | they can also be thought of as apps: click 43 | the app window icon to see an "app view" that 44 | hides code. 45 | 46 | Depending on how you use marimo, you can think 47 | of marimo programs as notebooks, apps, or both. 48 | """ 49 | ) 50 | } 51 | ) 52 | return 53 | 54 | 55 | @app.cell 56 | def __(): 57 | import marimo as mo 58 | 59 | return (mo,) 60 | 61 | 62 | if __name__ == "__main__": 63 | app.run() 64 | -------------------------------------------------------------------------------- /tests/test_plugin.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from pathlib import Path 4 | from typing import List, Match, Optional 5 | from unittest.mock import MagicMock 6 | 7 | import marimo 8 | import pytest 9 | from mkdocs.config.defaults import MkDocsConfig 10 | from mkdocs.structure.files import File, Files 11 | from mkdocs.structure.pages import Page 12 | 13 | from mkdocs_marimo.plugin import ( 14 | MarimoPlugin, 15 | MarimoPluginConfig, 16 | VirtualFile, 17 | collect_marimo_code, 18 | find_marimo_code_fences, 19 | is_inside_four_backticks, 20 | ) 21 | 22 | 23 | class MockFile(File): 24 | def __init__(self, abs_src_path: str): 25 | self.abs_src_path = abs_src_path 26 | super().__init__(abs_src_path, src_dir=None, dest_dir="", use_directory_urls=False) 27 | 28 | @property 29 | def abs_url(self) -> Optional[str]: 30 | return self.abs_src_path 31 | 32 | 33 | class MockPage(Page): 34 | def __init__(self, abs_src_path: str): 35 | self.file = MockFile(abs_src_path) 36 | super().__init__(None, self.file, MkDocsConfig()) 37 | self.abs_url = abs_src_path 38 | 39 | 40 | class TestMarimoPlugin: 41 | @pytest.fixture 42 | def plugin(self) -> MarimoPlugin: 43 | return MarimoPlugin() 44 | 45 | @pytest.fixture 46 | def mock_config(self) -> MkDocsConfig: 47 | return MkDocsConfig() 48 | 49 | @pytest.fixture 50 | def mock_page(self) -> MagicMock: 51 | page = MagicMock(spec=Page) 52 | page.abs_url = "/home" 53 | return page 54 | 55 | def test_on_page_markdown( 56 | self, plugin: MarimoPlugin, mock_config: MkDocsConfig, mock_page: MagicMock 57 | ) -> None: 58 | markdown: str = "```python {marimo}\nprint('HelloWorld!')\n```" 59 | 60 | result = plugin.on_page_markdown(markdown, page=mock_page, config=mock_config, files=None) 61 | result = plugin.on_post_page(result, page=mock_page, config=mock_config) 62 | 63 | assert "HelloWorld" in result 64 | assert "```python {marimo}" not in result 65 | 66 | def test_on_post_page( 67 | self, plugin: MarimoPlugin, mock_config: MkDocsConfig, mock_page: MagicMock 68 | ) -> None: 69 | output: str = "" 70 | result: str = plugin.on_post_page(output, page=mock_page, config=mock_config) 71 | assert " None: 74 | markdown: str = """ 75 | Some text 76 | ```python {marimo} 77 | print('Hello') 78 | ``` 79 | More text 80 | ```` 81 | ```python {marimo} 82 | print('Ignored') 83 | ``` 84 | ```` 85 | ```python {marimo-display} 86 | print('Display') 87 | ``` 88 | """ 89 | matches: List[Match[str]] = find_marimo_code_fences(markdown) 90 | assert len(matches) == 2 91 | assert matches[0].group(2).strip() == "print('Hello')" 92 | assert matches[1].group(2).strip() == "print('Display')" 93 | 94 | def test_collect_marimo_code(self) -> None: 95 | markdown: str = """ 96 | ```python {marimo} 97 | code1 98 | ``` 99 | ```python {marimo-display} 100 | code2 101 | ``` 102 | """ 103 | code_blocks, matches = collect_marimo_code(markdown) 104 | assert code_blocks == ["code1", "code2"] 105 | assert len(matches) == 2 106 | 107 | def test_is_inside_four_backticks(self): 108 | markdown = "Some text\n````\n```python {marimo}\ncode\n```\n````\nMore text" 109 | assert is_inside_four_backticks(markdown, markdown.index("```python {marimo}")) 110 | assert not is_inside_four_backticks(markdown, 0) 111 | 112 | def test_marimo_plugin_config(self): 113 | config = MarimoPluginConfig() 114 | 115 | assert config.enabled 116 | assert config.marimo_version == marimo.__version__ 117 | 118 | def test_parse_options(self, plugin: MarimoPlugin): 119 | options_string = "display_code=true width=500 height=300 is_reactive" 120 | expected = { 121 | "display_code": True, 122 | "width": 500, 123 | "height": 300, 124 | "is_reactive": True, 125 | } 126 | assert plugin.parse_options(options_string) == expected 127 | 128 | def test_parse_value(self, plugin: MarimoPlugin): 129 | assert plugin.parse_value("true") 130 | assert not plugin.parse_value("false") 131 | assert plugin.parse_value("42") == 42 132 | assert plugin.parse_value("3.14") == 3.14 133 | assert plugin.parse_value("hello") == "hello" 134 | 135 | def test_on_page_markdown_with_options(self, plugin: MarimoPlugin, mock_page: MagicMock): 136 | markdown = "```python {marimo display_code=true width=500}\nprint('Hello')\n```" 137 | 138 | result = plugin.on_page_markdown(markdown, page=mock_page, config=MagicMock(), files=None) 139 | result = plugin.on_post_page(result, page=mock_page, config=MagicMock()) 140 | 141 | assert "Hello" in result 142 | assert "```python {marimo display_code=true width=500}" not in result 143 | assert "" in result 153 | assert result.count(" None: 193 | # Create a temporary marimo file 194 | marimo_file = tmp_path / "example.py" 195 | marimo_file.write_text( 196 | """ 197 | import marimo 198 | app = marimo.App() 199 | 200 | @app.cell 201 | def __(): 202 | print('Hello from marimo!') 203 | 204 | if __name__ == "__main__": 205 | app.run() 206 | """ 207 | ) 208 | 209 | mock_page = MockPage(str(tmp_path / "current_file.md")) 210 | 211 | markdown = f"Some text\n!marimo_file {marimo_file.name}\nMore text" 212 | 213 | result = plugin.process_marimo_file_directives(markdown, mock_page, config=MkDocsConfig()) 214 | 215 | # The actual content of the generated HTML will depend on your implementation 216 | # Here we're just checking that the directive was replaced with some HTML 217 | assert result.startswith("Some text\n<") 218 | assert result.endswith(">\nMore text") 219 | assert "marimo" in result.lower() 220 | 221 | def test_resolve_marimo_file_path(self, plugin: MarimoPlugin): 222 | mock_page = MockPage("/path/to/current/file.md") 223 | 224 | file_path = "../example.py" 225 | 226 | result = plugin.resolve_marimo_file_path(file_path, mock_page, "/path/to") 227 | expected = os.path.normpath("/path/to/example.py") 228 | assert result == expected 229 | 230 | def test_generate_marimo_html(self, plugin: MarimoPlugin, tmp_path: Path) -> None: 231 | file_content = """ 232 | import marimo 233 | app = marimo.App() 234 | 235 | @app.cell 236 | def __(): 237 | print('Hello from marimo!') 238 | 239 | if __name__ == "__main__": 240 | app.run() 241 | """ 242 | file_path = tmp_path / "example.py" 243 | file_path.write_text(file_content) 244 | 245 | result = plugin.generate_marimo_body(str(file_path)) 246 | 247 | assert "Hello%20from%20marimo!" in result 248 | assert "" in result1 305 | assert "data-reactive=false" in result1 306 | 307 | # Test case 2: Override global config with code fence options 308 | markdown2 = "```python {marimo display_code=false display_output=true is_reactive=true}\n'World'\n```" 309 | result2 = plugin.on_page_markdown( 310 | markdown2, page=mock_page, config=MkDocsConfig(), files=None 311 | ) 312 | result2 = plugin.on_post_page(result2, page=mock_page, config=MkDocsConfig()) 313 | 314 | assert "World" in result2 315 | # Not visible code 316 | assert "
'World'
' 320 | in result2 321 | ) 322 | assert "data-reactive=true" in result2 323 | 324 | # Test case 3: Partially override global config 325 | markdown3 = "```python {marimo display_output=true}\n'Partial'\n```" 326 | result3 = plugin.on_page_markdown( 327 | markdown3, page=mock_page, config=MkDocsConfig(), files=None 328 | ) 329 | result3 = plugin.on_post_page(result3, page=mock_page, config=MkDocsConfig()) 330 | 331 | assert "Partial" in result3 332 | # Visible code 333 | assert "
'Partial'
' 337 | in result3 338 | ) 339 | assert "data-reactive=false" in result3 340 | 341 | def test_plugin_enabled(self, tmp_path: Path): 342 | # Test when plugin is enabled (default) 343 | plugin = MarimoPlugin() 344 | markdown = "```python {marimo}\nprint('Hello')\n```" 345 | mock_page = MockPage(str(tmp_path / "test_page.md")) 346 | result = plugin.on_page_markdown( 347 | markdown, page=mock_page, config=MkDocsConfig(), files=None 348 | ) 349 | assert "